Collection
Java 集合框架和分为 Collection 和 Map 两大类。其中 Collection 又由 List、Set、Queue 三类组成。
本节主要讲线程不安全的容器,线程安全的容器见 Thread-safe Collection。

List 概述

存取有序,有索引,可以根据索引来进行取值,元素可以重复。主要实现有 ArrayList、LinkedList、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
存取无序,基于哈希表实现。
通过hashCode和equals方法来共同保证元素唯一的。底层也维护了一个数组。
根据存储的元素计算出hashCode值,然后根据计算得出的hashCode值和数组的长度进行计算出存储的下标;如果下标的位置无元素,那么直接存储;如果有元素,那么使用要存入的元素和该元素进行equals方法,如果结果为真,则已经有相同的元素了,所以直接不存;如果结果假,那么进行存储,以链表的形式存储。
LinkedHashSet
基于链表和哈希表共同实现的,所以具有存取有序,元素唯一。
TreeSet
存取无序,可以进行排序(排序是在添加的时候进行排序)。基于二叉树的数据结构。
TreeSet 保证元素的唯一性是有两种方式:
存取的元素必须实现
Comparable接口。在创建
TreeSet的时候向构造器中传入一个Comparator对象,Comparator对象用于比较存取元素的类型。
Queue
队列通常是指“先进先出”(FIFO)的容器。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。
通常,队列不允许随机访问队列中的元素。
Map 概述
Map保存的是键值对,键要求保持唯一性,值可以重复。

LinkedHashMap
基于链表和哈希表结构的,所以具有存取有序。
TreeMap
TreeMap 底层使用的二叉树,其中存放进去的所有数据都需要排序,所以要求对象具备比较功能。与 TreeSet 类似,存取的元素需要实现Comparable接口,或者给 TreeMap 集合传递一个Comparator接口对象。
HashTable
HashTable 是一个线程安全的 Map 实现,其它与 HashMap(非线程安全)类似。
HashTable 不允许Null作为 Key 和 Value,但是 HashMap 可以。
HashMap(源码分析)
基于哈希表结构实现的,将键的 hash 值通过 hash 函数映射到数组下标,所以存储自定义对象作为键时,必须重写hasCode和equals方法。存取无序的。
会出现 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 次方,这样可以保证索引位置处于数组范围内。
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?
