Collection

Java 集合框架和分为 Collection 和 Map 两大类。其中 Collection 又由 ListSetQueue 三类组成。

本节主要讲线程不安全的容器,线程安全的容器见 Thread-safe Collection

Collection 类继承关系

List 概述

存取有序,有索引,可以根据索引来进行取值,元素可以重复。主要实现有 ArrayListLinkedList、Vector。

其中 Vector 已经过时,被ArrayList取代了。线程安全,通过在每个方法上加synchronized实现。

ArrayList(源码分析)

底层是使用数组实现,实现了自动扩容数组大小。实现了如下接口:

  • List:列表。

  • Cloneable:可以克隆。

  • Serializable:可以实现序列化。

  • RandomAccess:是一个标志接口,表示可以快速随机访问。

下面是对 ArrayList 的源码分析,Java 1.11。

属性

可以看到数据元素 elementData 被 transient 修饰了,但是 ArrayList 又实现了 Serializable 接口,这样不是矛盾的吗?

原因:ArrayList 可以自动扩容,所以不是数组的每个元素都存储了数据,所以不能序列化整个数组。ArrayList 有两个方法 writeObject、readObject,ObjectInputStream/ObjectOutputStream 在序列化对象时会通过反射调用这两个方法。

构造函数

ArrayList 新增元素时,若超过数组大小,数组会扩容,导致内存复制,所以最好指定合理的初始化容量。

新增元素

有两个新增元素的方法,一个是将元素增加到末尾,一个是增加到任意位置。都会先确定容量大小,若大小不够,则先扩容 1.5 倍。

添加元素到指定位置的方法,会使改位置之后的所有元素都移动。

若 ArrayList 在初始化时指定了容量,并且每次添加都在末尾,那么新增元素的速度比 LinkedList 高。

删除元素

同样每次删除元素都会挪动一些元素,删除位置越靠前,挪动的范围越大。

获取元素

由于是基于数组实现,所以获取元素很快。

LinkedList(源码分析)

是基于双向链表结构实现的,所以查询速度慢,增删速度快,提供了特殊的方法,对头尾的元素操作(进行增删查)。定义了 Node 结构:

在 Java 1.7 之前,LinkedList 只包含了一个Entry 结构的 header 属性;1.7 之后做了优化,Entry 结构换成了 Node,header 属性变成了 first 和 last 两个属性。

LinkedList 实现了这些接口:Deque、Cloneable、Serializable,没有实现 RandomAccess 接口。

属性

与 ArrayList 一样,属性也被 transient 修饰,也有 readObject、writeObject 方法。

新增元素

将元素添加到队尾:

将元素添加到任意位置,相对于 ArrayList,不需要移动元素:

删除元素

注意 node 方法,作用是找到元素位置,若位置处于前半段,则从前往后找,若处于后半段,则从后往前找。

所以删除靠前或靠后的元素效率很高,删除中间部分的元素效率相对较低。

获取元素

同样也是调用 node 方法,区分前半段和后半段。

所以建议遍历 LinkedList 时,使用 Iterator 的方式。

Iterator(源码分析)

如下 remove1 方法是正确的,remove2 是不正确的,会抛出 ConcurrentModificationException。

因为虽然 foreach 方法是语法糖,会转换成 Iterator 方法,但是第 15 行还是调用的 List 的 remove 方法,而不是 Iterator 的 remove 方法。

Iterator 有属性 expectedModCount,当不匹配时会抛出异常。List 的 remove 方法不会修改该值,造成不匹配,所以抛出异常。

Set

元素不可以重复。

HashSet

存取无序,基于哈希表实现。

通过hashCodeequals方法来共同保证元素唯一的。底层也维护了一个数组。

根据存储的元素计算出hashCode值,然后根据计算得出的hashCode值和数组的长度进行计算出存储的下标;如果下标的位置无元素,那么直接存储;如果有元素,那么使用要存入的元素和该元素进行equals方法,如果结果为真,则已经有相同的元素了,所以直接不存;如果结果假,那么进行存储,以链表的形式存储。

LinkedHashSet

基于链表哈希表共同实现的,所以具有存取有序,元素唯一。

TreeSet

存取无序,可以进行排序(排序是在添加的时候进行排序)。基于二叉树的数据结构。

TreeSet 保证元素的唯一性是有两种方式:

  1. 存取的元素必须实现Comparable接口。

  2. 在创建TreeSet的时候向构造器中传入一个Comparator对象,Comparator对象用于比较存取元素的类型。

Queue

队列通常是指“先进先出”(FIFO)的容器。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。

通常,队列不允许随机访问队列中的元素。

Map 概述

Map保存的是键值对,键要求保持唯一性,值可以重复。

Map 类继承关系

LinkedHashMap

基于链表哈希表结构的,所以具有存取有序

TreeMap

TreeMap 底层使用的二叉树,其中存放进去的所有数据都需要排序,所以要求对象具备比较功能。与 TreeSet 类似,存取的元素需要实现Comparable接口,或者给 TreeMap 集合传递一个Comparator接口对象。

HashTable

HashTable 是一个线程安全的 Map 实现,其它与 HashMap(非线程安全)类似。

HashTable 不允许Null作为 Key 和 Value,但是 HashMap 可以。

HashMap(源码分析)

基于哈希表结构实现的,将键的 hash 值通过 hash 函数映射到数组下标,所以存储自定义对象作为键时,必须重写hasCodeequals方法。存取无序的。

会出现 hash 冲突的情况,解决方式有:开放寻址再哈希函数链表法等。Java 的 HashMap 采用的是链表法,采用数组+链表的数据结构实现。

HashMap 在多线程使用时可能会出现 CPU 100% 的情况,原因是 JDK1.7 出现 Hash 冲突时,采用的是头插法,会改变节点顺序,会造成循环链表,发生死循环。

属性

有三个重要的属性:

  • table:存储数据,有 next 属性指向 hash 冲突的下一节点。

  • threshold:默认 12,Node 数量超过 threshold 就会调用 resize 方法。

  • loadFactor:加载因子,默认 0.75。

新增元素

hash 函数:先无符号向右位移 16 位,即取 int 类型的一般,将二进制的 int 类型对半切开;再异或运算。

(n - 1) & hash:hash 表习惯将数组长度 n 设为 2 的 x 次方,这样可以保证索引位置处于数组范围内。

为什么要 2 的 x 次方?因为 n - 1 后,每一位都是 1,进行 & 运算可以减少冲突。若有些位数是 0,那么有些位置永远都不能存放数据了。

JDK 1.8 引入了红黑树来提升链表的查询效率,链表长度超过 8 后,红黑树的效率比链表高。

获取元素

没有冲突时效率最高,有冲突是链表的效率 O(n),树化后为 O(logn)。

扩容

JDK 1.7:依次取出数组的每个元素,元素是链表,链表的头是最后添加的,重新计算 hash,然后依次插入新数组,所以原来 hash 冲突的链表会倒置顺序。

JDK 1.8:由于扩容是 2 倍关系,即 tableSize 就是左移一位,所以原来的 hash 值和新的 tableSize 按位与计算,0 索引不变,1 索引加上原来数组大小。

Last updated

Was this helpful?