2020JAVA集合容器面试题

集合和数组的区别

数组是固定长度的;集合可变长度的。
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

collection集合和map集合结构图

在这里插入图片描述

说说Java中常见的集合吧。

  • Map接口和Collection接口是所有集合框架的父接口
  • Collection接口的子接口包括:Set接口和List接口
  • Map接口的实现类主要有:Hashtable、HashMap、LinkedHashMap、ConcurrentHashMap、TreeMap、Properties等
  • Set接口的实现类主要有:HashSet、LinkedHashSe、TreeSet等
  • List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

哪些集合类是线程安全的?

vector:使用了 Synchronized 来实现线程同步,因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。

statck:堆栈类,先进后出。

hashtable:就比hashmap多了个线程安全。

enumeration:枚举,相当于迭代器。

Java集合的快速失败机制 “fail-fast”?

是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hasNext()/next()遍历下一个元素之前,都会检测modCount变量是否expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

解决办法
在遍历过程中,对涉及到改变modCount值得地方全部加上synchronized。

使用CopyOnWriteArrayList来代替ArrayList

fail-fast原理分析

ArrayList

public class ArrayListTest {

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(10);
        arrayList.add(11);

        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();		//遍历,产生错误
            if (next == 11) {
                arrayList.remove(next);
            }
        }
    }

}

结果是:
在这里插入图片描述从结果看出,在单线程下,在使用迭代器进行遍历的情况下,如果调用 ArrayList 的 remove 方法,此时会报 ConcurrentModificationException 的错误,从而产生 fail-fast 机制

错误信息告诉我们,发生在 iterator.next() 这一行,继续点进去,定位到 checkForComodification() 这一行

public E next() {
    checkForComodification();		// 错误在这儿
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

继续点进去,可以在 ArrayList 的 Itr 这个内部类中找到该方法的详细定义,这里涉及到两个变量,modCount 和 expectedModCount,modCount 是在 ArrayList 的父类 AbstractList 中进行定义的,初始值为 0,而 expectedModCount 则是在 ArrayList 的 内部类中进行定义的,在执行 arrayList.iterator() 的时候,首先会实例化 Itr 这个内部类,在实例化的同时也会对 expectedModCount 进行初始化,将 modCount 的值赋给 expectedModCount

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    
    // modCount 初始值为 0
    protected transient int modCount = 0;
    
}

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	public Iterator<E> iterator() {
		// 实例化内部类 Itr
        return new Itr();
    }

	private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        // expectedModCount 一开始等于 modCount
        int expectedModCount = modCount;

		// 检查是否发生异常
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }	
}

知道了这两个变量是从何而来之后,我们来看 checkForComodification() 这个方法,如果 modCount 和 expectedModCount 不等,就会抛出 ConcurrentModificationException 这个异常,换句话说,一般情况下,这两个变量是相等的,那么啥时候起?这两个变量会不等呢?

经过观察,发现 ArrayList 在增加、删除(根据对象删除集合元素)、清除等操作中,都有 modCount++ 这一步骤,即代表着,每次执行完相应的方法,modCount 这一变量就会加 1

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

// 根据传入的对象来删除,而不是根据位置
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);		// modCount++
                return true;
            }
    }
    return false;
}

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;
    
    size = 0;
}

分析到这儿,似乎有些明白了,我们来完整的分析一下整个过程,在没有执行删除操作之前,ArrayList 中的 modCount 变量和迭代器中的 expectedModCount 的值一直都是相同的。在迭代的过程中,调用了 ArrayList 的 remove(Object o) 方法,使得 ArrayList 的 modCount 这个变量发生变化(删除成功一次加1),一开始和 modCount 相等的 expectedModCount 是属于内部类的,它直到迭代结束都没能发生变化。在迭代器执行下一次迭代的时候,因为这两个变量不等,所以便会抛出 ConcurrentModificationException 异常,即产生了 fail-fast 异常

HashMap

public class HashMapTest {

    public static void main(String[] args) {
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "QQQ");
        hashMap.put(2, "JJJ");
        hashMap.put(3, "EEE");

        Set<Map.Entry<Integer, String>> entries = hashMap.entrySet();
        Iterator<Map.Entry<Integer, String>> iterator = entries.iterator();

        while (iterator.hasNext()) {
            Map.Entry<Integer, String> next = iterator.next(); //错误产生地方
            if (next.getKey() == 2) {
                hashMap.remove(next.getKey());
            }
        }

        System.out.println(hashMap);
    }

}

输出结果是:
在这里插入图片描述根据错误的提示,找到出错的位置,也是在 Map.Entry<Integer, String> next = iterator.next() 这一行,继续寻找源头,定位到了 HashMap 中的内部类 EntryIterator 下的 next() 方法

private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() {
        return nextEntry();
    }
}

继续往下找,来到了 HashMap 下的另一个私有内部类 HashIterator,该内部类也有 expectedModCount,modCount 是直接定义在 HashMap 中的,初始值为 0,expectedModCount 直接定义在 HashMap 的内部类中,当执行 arrayList.iterator() 这段代码的时候,便会初始化 HashIterator 这个内部类,同时调用构造函数 HashIterator(),将 modCount 的值赋给 expectedModCount

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
	// 初始值为 0
	transient int modCount;

	// HashMap 的内部类 HashIterator
	private abstract class HashIterator<E> implements Iterator<E> {
        Entry<K,V> next;        // next entry to return
        // 期待改变的值,初始值为 0
        int expectedModCount;   // For fast-fail
        int index;              // current slot
        Entry<K,V> current;     // current entry

        HashIterator() {
            // expectedModCount 和 modCount 一样,初始值为 0
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }

  		...
    }
}

来看抛出异常的 nextEntry() 这个方法,只要 modCount 和 expectedModCount 不等,便会抛出 ConcurrentModificationException 这个异常,即产生 fast-fail 错误

同样,我们看一下 modCount 这个变量在 HashMap 的哪些方法中使用到了,和 ArrayList 类似,也是在增加、删除、修改等方法中,对 modCount 这个变量进行了加 1 操作

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    // 将 modCount 加 1
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);		//该方法里面,如果删除成功,则将 modCount 加 1
    return (e == null ? null : e.value);
}

public void clear() {
    // 将 modCount 加 1
    modCount++;
    Arrays.fill(table, null);
    size = 0;
}

我们来捋一下整个过程,在对 HashMap 和 Iterator 进行初始化之后,没有执行 remove 方法之前,HashMap 中的 modCount 和内部类 HashIterator 中的 expectedModCount 一直是相同的。在 HashMap 调用 remove(Object key) 方法时,如果删除成功,则会将 modCount 这个变量加 1,而 expectedModCount 是在内部类中的,一直没有发生变化,当进行到下一次迭代的时候(执行 next 方法),因为 modCount 和 expectedModCount 不同,所以抛出 ConcurrentModificationException 这个异常

怎么确保一个集合不能被修改?

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
示例代码如下:

List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());

迭代器 Iterator 是什么?

Iterator 接口提供遍历任何 Collection集合 的接口。我们可以从一个 Collection集合 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

Iterator 怎么使用?有什么特点?

Iterator 使用代码如下:

List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
  String obj = it. next();
  System. out. println(obj);
}

Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

如何边遍历边移除 Collection集合 中的元素?

边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();
}

一种最常见的错误代码如下:

for(Integer i : list){
   list.remove(i)
}

运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

Iterator 和 ListIterator 有什么区别?

IteratorListIterator
即可以遍历list又可以遍历set只能遍历list
只能向前遍历可以向前和向后遍历
继承自Iterator

数组和List集合之间的转换

  • 数组>>>List:使用 Arrays. asList(array) 进行转换。(转换后不能对集合结构进行进行修改,如:增加、删除操作,因为本质上还是一个数组)
  • List 转数组:使用 List toArray() 方法,返回一个object【】,强转会出现classcastexception
package niuke;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ConverTest {
    public static void main(String[] args) {
        // list集合转换成数组
        ArrayList<String> list =  new ArrayList<>();
        list.add("zhangsan");
        list.add("lisi");
        list.add("yangwenqiang");
        Object[] arr = list.toArray();
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println("---------------");
        // 数组转换为list集合
        String[] arr2 = {"niuke", "alibaba"};
        List<String> asList = Arrays.asList(arr2);
        for (int i = 0; i < asList.size(); i++) {
            System.out.println(asList.get(i));
        }

    }
}

ArrayList 和 LinkedList 的区别是什么?

ArrayListLinkedList
底层使用动态数组实现底层使用双相链表实现
随机存取效率高,增删节点效率低随机存取效率低,增删节点效率高
初始化时会预留一定空间以防扩容使用初始化是存放数据和指针信息
都不是线程安全的都不是线程安全的
都实现了list接口都实现了list接口

ArrayList 和 Vector 的区别是什么?

ArrayListVector
都实现了list接口都实现了list接口
不是线程安全的是线程安全的
性能更优已经被淘汰了

多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {
    System.out.println(synchronizedList.get(i));
}

遍历一个 List 和set有哪些不同的方式?

list遍历方式有以下几种:

  • 普通for,基于计数器。
  • 迭代器遍历,Iterator。
  • 增强for遍历。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中修改集合结构,例如:增加、删除、替换。

set遍历方式有以下几种:

  • 增强for
  • 迭代器遍历,itrator

List 和 Set 的区别

ListSet
有序(存入和取出元素顺序一致)不序(存入和取出元素顺序不一致)
元素可以重复元素不能重复

说一下 HashSet 的实现原理?

HashSet 底层是基于 HashMap 实现的,使用HashMap进行存储。HashSet的value存放于HashMap的key上,HashMap的value统一为PRESENT(private static final Object PRESENT = new Object();)。因此 HashSet 的实现比较简单,HashSet 的相关操作,基本上都是直接调用底层 HashMap 的相关方法来完成。HashSet 不允许重复的值。

HashSet如何检查重复?HashSet是如何保证数据不可重复的?

以下是HashSet 部分源码:

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {
    map = new HashMap<>();
}

public boolean add(E e) {
    // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
	return map.put(e, PRESENT)==null;
}

HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key

HashSet与HashMap的区别

HashMapHashSet
实现了Map接口实现Set接口
存储键值对仅存储对象
调用put()向map中添加元素调用add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode,来查找value的位置HashSet使用成员对象来计算hashcode值
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象HashSet较HashMap来说比较慢

treemap底层数据结构?

treemap底层采用红黑树实现,存储的key-value按照key来排序,因此它是有序的集合,每一个key-value都作为在红黑树的节点进行存储。当key是string、integer时候,直接按照实际的大小进行排序;当key是自定义类型时有两种方式。

自然比较器:comparable接口,重写compareTo()

// 方式一:定义该类的时候,就指定比较规则
class User implements Comparable{
    @Override
    public int compareTo(Object o) {
        // 在这里边定义其比较规则
        return 0;
    }
}

定制比较器:comparator接口,重写compare()

public static void main(String[] args) {
    // 方式二:创建TreeMap的时候,可以指定比较规则
    new TreeMap<User, Integer>(new Comparator<User>() {
        @Override
        public int compare(User o1, User o2) {
            // 在这里边定义其比较规则
            return 0;
        }
    });

TreeSet、treemap比较规则

它有2种比较器,如果集合中存放的是integer类型或者string类型则直接按照实际值大小进行排序;如果存放的是用户自定义类型,需要使用比较器:
comparable自然比较器,重写comparato()
comparator定制比较器,重写compare()

TreeSet与HashSet的区别

TreeSetHashSet
底层基于treemap实现,因此也是基于红黑树实现hashset底层基于hashmap实现
treeset是无序的(指存入和取出元素顺序不一致)但是它会自动将存入集合中的元素排序好输出hashset是无序(指存入元素和取出元素顺序不一致)但是它会自动将存入的元素排序好输出
treeset保证元素具有唯一性hashset保证元素具有唯一性元素唯一性是通过重写hashcode()和equals()实现的

什么是LRU算法?LinkedHashMap如何实现LRU算法?

LRU(least recently used)通过记录元素的访问历史记录来淘汰数据,如果元素最近被访问那么将来被访问的概率也很大

package pak2;

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUTest {

    private static int size = 5;

    public static void main(String[] args) {
        Map<String, String> map = new LinkedHashMap<String, String>(size, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                return size() > size;
            }
        };
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        map.put("4", "4");
        map.put("5", "5");
        System.out.println(map.toString());

        map.put("6", "6");
        System.out.println(map.toString());
        map.get("3");
        System.out.println(map.toString());
        map.put("7", "7");
        System.out.println(map.toString());
        map.get("5");
        System.out.println(map.toString());
    }
}

JDK1.8主要解决了哪些问题

  • resize 进行扩容优化
  • 引入了红黑树,避免单条链表过长而影响查询效率,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
  • 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。

JDK1.7与JDK1.8的区别

在这里插入图片描述

ConcurrentHashMap 和 Hashtable 的区别?(重要)

ConcurrentHashMapHashtable
1.8之前采用segemnt数组实现,每个segment数组类似于hashmap,里面包含一个hashentry数组,hashentry是一个链表结构的元素;1.8之后摒弃了segment的概念使用node数组+链表(长度>8)+红黑树类似于1.8之前的hashmap结构数组+链表实现
1.8之前使用segment分段锁实现,每个segemnt都是一把锁,多线程访问容器里面的不同数据段的时候根据自己需要获取相应的锁执行多线程可以并发操作执行效率比较高;1.8之后摒弃了segment的概念使用node数组+链表+红黑树,并发控制使用synchronized+CAS实现hashtble使用synchronized锁住整个容器来保证线程安全,执行效率非常低只允许一个线程进行同步操作

实现线程安全的方式(重要): ① 在JDK1.8之前,ConcurrentHashMap使用分段锁实现线程安全,每个segment相当于一把锁,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
到了 JDK1.8之后已经摒弃了Segment的概念,而是直接用 Node 数组+链表(长度>8转变为)红黑树的数据结构来实现,并发控制使用 synchronized +CAS 来操作。② Hashtable使用 synchronized锁住整个容器的来保证线程安全,执行效率非常低下,只允许有一个线程进行同步操作。

在这里插入图片描述
在这里插入图片描述

如何决定使用 HashMap 还是 TreeMap?

  • 如果是对map集合进行插入、删除操作使用hashmap就可以;
    而treemap是具有对保存元素进行排序的一个功能,按照key排序,如果存入的数据是integer、string类型直接按照实际值排序,如果是用户自定义类型有2种方式:comparable自然排序和comparator定制排序

comparable 和 comparator的区别?

comparablecomparator
自然排序:comparable接口用户自定义类实现该接口并重写compareTo()用来进行排序定制排序:comparator接口,通常在treeset或者treemap创建对象的时候传入一个该定制排序比较器,重写compare()

Collection 和 Collections 有什么区别?

CollectionCollections
是一个集合接口,提供了对集合对象进行基本操作的通用接口方法是集合的一个工具类,用于对集合元素进行查找、排序、实现线程安全等操作
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前撤步登哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值