该文章为面试精华版,如果是初学者,建议先从专栏学习:Java集合专栏

一、 主要容器概述

注意Collection是一个接口,是List和Set的公共接口,Collections是一个工具类

Java容器主要有三个

  • List:是一个有序集合,可以放重复的数据
  • Set:是一个无序集合,不允许放重复的数据
  • Map:是一个无序集合,集合中包含一个键对象,-一个值对象,键对象不允许重复,值对象可以重复(身份证号-姓名)

size (),length,length()区别?

  • size()是集合中使用,统计集合中元素的个数
  • length是数组的一个成员属性
  • length()是String的一个方法

Iterator

方法 描述
boolean hasNext() 判断集合中是否还有下一个元素
Object next() 返回下一个元素
void remove() 从集合中删除一个由 next()方法返回的元素

二、 List

  • ArrayList:查询数据比较快,添加和删除数据比较慢(基于可变数组)
  • LinkedList:查询数据比较慢,添加和删除数据比较快(基于链表数据结构)

1. ArrayList

查找比较快,增删比较慢

  • 底层是基于数组。
  • 默认容量是10
  • 每次扩容1.5倍
  • 如果增加 0.5 倍后的新容量超过限制的容量,则用所需的最小容量与限制的容量进行判断,超过则指定为 Integer 的最大值
  • 通过数组的复制将原数据复制到一个更大(新的容量大小)的数组

如何保证线程安全?

  • 继承ArrayList,重写其中的方法
  • List list = Collections.synchronizedList(new ArrayList());

2. LinkedList

增删比较快,查找比较慢

  • 基于链表的,除了存放数据,还需要存放指针,所以占用的空间会大一些

  • 它实现了 Deque 接口,使得 LinkedList 类也具有队列的特性

  • 不是线程安全的

3. Vector

Vector与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一 个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。

4. ArrayList 与 LinkedList 异同

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

  • 底层数据结构: Arraylist 底层使用的是 Object 数组; LinkedList 底层使用的是双向链表数据结构JDK1.6之前为循环链表, JDK1.7 取消了循环。注意双向链表和双向循环链表的区别:节约空间);

  • 插入和删除是否受元素位置的影响

    • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 add(Ee) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
  • LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。

  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。

5. ArrayList和Vector的区别

  • Vector 类的所有方法都是同步的。可以由两个线程安全地访问一个 Vector 对象、但是一个线程访问 Vector的话代码要在同步操作上耗费大量的时间。
  • Arraylist 不是同步的,所以在不需要保证线程安全时时建议使用 ArrayList。【CopyOnWriteArrayList 是同步的】。
  • 扩容倍数不一样:Vector 每次扩容请求其大小的 2 倍空间 ,而 ArrayList 是 1.5 倍。

6. System.arraycopy()和 Arrays.copyOf()

  1. arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置
  2. copyOf()是系统自动在内部新建一个数组,并返回该数组。

7. CopyOnWriteArrayList

是一种读写分离的结构

  • 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
  • 写操作需要加锁,防止并发写入时导致写入数据丢失。
  • 写操作结束之后需要把原始数组指向新的复制数组。
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
final void setArray(Object[] a) {
    array = a;
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。但是 CopyOnWriteArrayList 有其缺陷:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

三、 Set

在Set家族中,常用的有三种

  • HashSet: 采用 Hashmap 的 key 来储存元素,主要特点是无序的,基本操作都是 O(1) 的时间复杂度,很快。
  • LinkedHashSet: 这个是一个 HashSet + LinkedList 的结构,特点就是既拥有了 O(1) 的时间复杂度,又能够保留插入的顺序。
  • TreeSet: 采用红黑树结构,特点是可以有序,可以用自然排序或者自定义比较器来排序;缺点就是查询速度没有 HashSet 快。

1. 哈希表

哈希表是一种数据结构,哈希表能够提供快速存取操作。哈希表是基于数组的,所以也存在缺点,数
组一旦创建将不能扩展。

正常的数组,如果需要查询某个值,需要对数组进行遍历,只是一种线性查找,查找的速度比较慢。
如果数组中的元素值和下标能够存在明确的对应关系,那么通过数组元素的值就可以换算出数据元素的下
**标,通过下标就可以快数定位数组元素,这样的数组就是哈希表。**一张哈希表:

元素值 10 11 12 13 14 15 16 17 18
元素下标 0 1 2 3 4 5 6 7 8

以上我们的示例元素值和下标的关系为

  • 元素下标=元素值-10,此时的示例 hashcode 就是和数组下标一致了,取得 hashcode 方法如下:

    //取得 hashCode
    pubic int hashCode(int value) {
    	return value – 10;
    }
    

2. HashSet

  • HashSet中的数据是无序的不可重复的。
  • 采用 Hashmap 的 key 来储存元素

3. hashCode()

覆盖了 equals 和 hashCode,当 hashCode 相同,它会调用 equals 进行比较,如果 equals 比较相等将不加把此元素加入到 Set 中

但 equals 比较不相等会重新根据 hashCode 换算位置仍然会将该元素加入进去的。

hashCode() 与 equals() 的相关规定

  • 如果两个对象相等,则 hashcode 一定也是相同的
  • 两个对象相等,对两个对象分别调用 equals 方法都返回 true
  • 两个对象有相同的 hashcode 值,它们也不一定是相等的
  • 因此, equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
  • hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

再次强调:特别是向 HashSet 或 HashMap 中加入数据时必须同时覆盖 equals 和 hashCode 方法,应该
养成一种习惯覆盖 equals 的同时最好同时覆盖 hashCode

Java 要求

  • 两个对象 equals 相等,那么它的 hashcode 相等
  • 两个对象 equals 不相等,那么它的 hashcode 并不要求它不相等,但一般建议不相等hashcode 相等不代表两个对象相等(采用 equals 比较)

为什么重写 equals() 就必须要重写 hashCode()?

  • 当key的hashCode()计算的数值相同时,就会出现hash冲突

处理 hash 冲突有哪些方法?Java 中用的哪一种?为什么?另一种方法你在工作中用过吗?在什么情况下用得多?

见:https://xzzz2020.gitee.io/post/d0otbdSTy

4. TreeSet

  • TreeSet 可以对 Set 集合进行排序, 默认自然排序(即升序)
  • 引用类型实现排序,需要实现Comparator或者Comparable

Comparable 和Comparator 的区别?

  • Comparable是自然排序,需要实体类实现,需要修改源代码
  • Comparator 是定制排序,不需要更改源代码,定义一个比较规则,传递给需要调用的方法中,将比较策略与数据分离,体现了策略模式
  • Comparator 比Comparable 优先级更高

四、 Map

关于HashMap的全部面试题见:https://xzzz2020.gitee.io/post/hashmap

Map 中可以放置键值对,也就是每一个元素都包含键对象和值对象, Map 实现较常用的为 HashMap,HashMap 对键对象的存取和 HashSet 一样,仍然采用的是哈希算法,所以如果使用自定类作为 Map 的键对象,必须复写 equals 和 hashCode 方法

Map中常用的三个

  • HashMap: 与 HashSet 对应,也是无序的,O(1)。
  • LinkedHashMap: 这是一个「HashMap + 双向链表」的结构,落脚点是 HashMap,所以既拥有 HashMap 的所有特性还能有顺序。
  • TreeMap: 是有序的,本质是用二叉搜索树来实现的。

1. HashMap

实现原理

  • 对于HashMap中的每个key,根据一个Hash函数,计算出一个Hash值,对应就是桶的编号,桶实际上是用数组实现的
  • Hash函数跟HashMap的容量有关系

如果不同的元素的key算出了相同的哈希值,那么该怎么存放呢?

  • 这就是哈希碰撞,即多个 key 对应了同一个桶。

HashMap 中是如何保证元素的唯一性的呢?即相同的元素会不会算出不同的哈希值呢?

  • 通过 hashCode() 和 equals() 方法来保证元素的唯一性。

2. HashTable

是一个线程安全的,但是锁住了全部数据效率低下

HashMap 和 Hashtable 的区别 ?

  1. 线程是否安全: HashMap 是非线程安全的, HashTable 是线程安全的; HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  2. 效率:因为线程安全的问题, HashMap 要比 HashTable 效率高一点。另外, HashTable 基本被淘汰,不要在代码中使用它;
  3. 对 Null key 和 Null value 的支持: HashMap 中, null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
  4. HashMap 的迭代器是 fail-fast 迭代器,HashTable是用的Enumeration(枚举)的迭代器
  5. 初始容量大小和每次扩充容量大小的不同
    ①创建时如果不指定容量初始值, Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。
    HashMap 默认的初始化大小为 16。 之后每次扩充,容量变为原来的 2 倍。
    ②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
  6. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默
    认为 8)时,将链表转化为红黑树,以减少搜索时间。 Hashtable 没有这样的机制。

3. LinkedHashMap

保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序 。

  • 内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序

如何实现LRU 缓存 ?

构造方法提供了一个参数accessOrder,默认是false,表示按照插入顺序

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

而当accessOrder 为true时,表示按照访问顺序,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除, MAX_ENTRIES默认为3

需要继承LinkedHashMap,并且覆盖 removeEldestEntry() 方法实现

class LRUCache<k,v> extends LinkedHashMap<k,v> {
    private final static int MAX_CACHE_SIZE = 100;

    private final int limitCacheSize;

    public LRUCache() {
        this(MAX_CACHE_SIZE);
    }


    public LRUCache(int cacheSize){
        super(cacheSize, 0.75f, true);
        this.limitCacheSize = cacheSize;
    }

    /** * 判断什么时候删除缓存 */
    @Override
    protected boolean removeEldestEntry(Map.Entry<k, v> eldest) {
        return this.size()>this.limitCacheSize;
    }


}

五、 Collections常用方法

排序

void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由 Comparator 控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当 distance 为正数时,将 list 后 distance 个元素整体移到前面。当 distance 为负数时,将 list 的前 distance 个元素整体移到后面。 

查找,替换操作

int binarySearch(List list, Object key)//对 List 进行二分查找,返回索引,注意 List 必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。
int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由 Comparatator 类控制。
int min(Collection coll, Comparator c) void fill(List list, Object obj)//用指定的元素代替指定 list 中
所有元素。 int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计 target 在 list 中第一次出现的索引,找不到则返回-1,
int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素  

同步控制

Collections 提供了多个 synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而解决多
线程并发访问集合时的线程安全问题。

我们知道 HashSet, TreeSet, ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。 Collections 提供了多个静态方法可以把他们包装成线程同步的集合

synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(线程安全的)collection。
synchronizedList(List<T> list) //返回指定列表支持的同步(线程安全的) List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的) Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的) set。

设置不可变集合

emptyXxx(): 返回一个空的、不可变的集合对象,此处的集合既可以是 List,也可以是 Set,还可以是 Map。

singletonXxx(): 返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以
是: List, Set, Map。

unmodifiableXxx(): 返回指定集合对象的不可变视图,此处的集合可以是: List, Set, Map。 上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本

六、Fail-Fast 和 Fail-Safe

  • Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。

  • java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。

  • 快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

快速失败原理

  • modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

  • 在进行序列化或者迭代或者remove()等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出
    ConcurrentModificationException。

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException {
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();
        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);
        // Write out all elements in the proper order.
        for (int i = 0; i < size; i++) {
            s.writeObject(elementData[i]);
        }
        if(modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

解决fail-fast的原理

在返回一个迭代器的时候,拷贝数据,因此,对容器内容的修改不影响遍历。

private COWIterator(Object[] elements, int initialCursor) {
    cursor = initialCursor;
    snapshot = elements;
}
  • 常见的的使用fail-safe方式遍历的容器有ConcerrentHashMapCopyOnWriteArrayList等。