Java容器,简而言之就是装载对象的器皿。它们被广泛应用于数据操作的各种场景中:如Web交互服务器返回的列表数据,数据库操作的返回数据等。
Java容器按照其族谱,可以分为两类:一类是 Collection 族,相关实现类直接用于存取多个对象;另一类是 Map 族,其实现类会以键值对(K-V)的结构来存取对象。
从上图可以看到 Collection 族主要的容器类别有三种:List、Set、Queue。下面就深入介绍一下 List 和 Set 这两类常用容器。
【List】
List接口用来存放有序的对象,且允许元素重复。List 最常用的实现类是 ArrayList 和 LinkedList。
1. ArrayList
- 数据结构。ArrayList 数据结构本质为动态数组。在内存中开辟的是连续空间。
- 扩容机制。其默认长度为 DEFAULT_CAPACITY = 10 ,当装载的元素个数超过数组当前的长度时,数组会自动扩容为原来长度的 1.5 倍!
- 性能&特性。在性能方面:【查询和修改】(修改的本质就是查询+替换):可以通过数组的索引直接获取,效率极高。【新增】:如未超过数组容量,直接添加,效率也很高,但是若发生了扩容,会将已装载的元素复制到扩容后的数组,然后再添加新元素,综合下来的效率就较低了。【删除】:删除数组尾部元素可以直接操作,效率很高,但若要删除前面的元素,则剔除元素后,需要将剩下的数组重新拼接起来,这里也会发生数组复制的操作(System.arraycopy()),效率因此会比较低。
- 安全性。ArrayList是线程不安全的,需要线程安全的场景下,可以使用Vector。
2. LinkedList
- 数据结构。LinkedList 数据结构为双向链表。
- 扩容机制。长度不固定也无限制,每个元素都包含当前值、上一个节点、下一个节点。
- 性能&特性。【查询和修改】:必须通过循环的方式遍历链表,因此效率不高。【新增和删除】:因为链表的特点(内存中不一定连续,通过节点指向串联),直接修改前后节点的指向即可,效率很高。
- 安全性。LinkedList是线程不安全的。
总结
| 容器 | 数据结构 | 扩容机制 | CRUD性能 | 是否支持随机访问(实现RandomAcess接口) |
|---|---|---|---|---|
| ArrayList | 动态数组 | 1.5倍扩容 | 查改快,增删慢 | 支持 |
| LinkedList | 双向链表 | 无限制 | 查改慢,增删快 | 不支持 |
【Set】
Set接口相较List的最大区别是:它是无序的,且不允许元素重复。Set 常用的实现类为 HashSet 和 TreeSet。
1. HashSet HashSet 数据结构是哈希表,它本质就是基于 HashMap 的改造。其存放的元素通过 hash 算法决定其存储位置,而不是按插入顺序,而且发生扩容时,位置可能会发生变化,因此不是有序的。在Map族的介绍中,我们会深入介绍HashMap,因此这里我们就不再对HashSet展开了。
2. TreeSet TreeSet 数据结构是二叉树。它自带排序(默认排序方式是:使用元素的equals方式比较,升序排列),且不允许放入null值。
【Map】
Map接口采用K-V的键值对形式存储数据,方便实现快速的搜索查询。其键K各不相同,且允许为null。Map 的常用实现类有: HashMap、HashTable。
1. HashMap
- 数据结构。HashMap 在JDK1.8之后的数据结构是数组+链表 / 红黑树(JDK1.7及之前,数据结构为数组+链表)。
- 扩容机制。默认容量为
DEFAULT_INITIAL_CAPACITY = 1<<4,即16。而且构造时传入的任意初始容量都会将其扩充为2的n次幂大小(即:若传入30的初始容量,构造出来的hashmap容量为2^5=32)。 之所以将容量定义为2的n次幂,是因为在存取时,为了决定确定存储位置,会计算key的hash,然后对容量length取余,当length为2的n次幂时,取余的计算就可以用位运算来替换(hash%length==hash&(length-1)),以提高运算的效率。另外,hashmap中还定义了一个默认负载因子LOAD_FACTOR = 0.75,当前元素数量达到 容量length*负载因子LOAD_FACTOR = 12时,HashMap会自动完成扩容的操作,扩容后容量为原来容量的 2 倍(移位运算)!HashMap扩容后,length发生变化,进而触发rehash的操作,即重新计算所有元素的hash及其在数组中的位置,然后重新进行存储过程。这一过程十分消耗性能。 - 存储过程。存储过程如下图所示:
在JDK1.7时,若hash冲突严重,链表可能会非常长,链表查询的效率会降低(查询性能O(N))。因而在JDK1.8中对此进行了优化:当链表长度达到8时,链表会转换为红黑树(查询性能O(logN))。阈值设置为8是根据泊松分布的统计规律设定的,可以取得在链表和红黑树之间查询性能的最优方案! - 安全性。HashMap 是线程不安全的,需要线程安全的场景,一般使用ConcurrentHashMap。HashMap之所以线程不安全,是因为多线程操作rehash过程时,hash冲突可能会导致链表变形为环形链表,而导致遍历死循环(详情可参考疫苗:JAVA HASHMAP的死循环)。
2. HashTable
- 性能&特性。HashTable的数据结构可扩容机制均同HashMap,但是为了避免线程同步的问题,对HashTable的存和取的操作均添加了同步锁,效率较低。
- 安全性。HashTable是线程安全的,但是其put和get方法均采用了同步锁机制,属于全表锁,效率极低,不推荐使用。
3. ConcurrentHahsMap
- 数据结构。ConcurrentHahsMap 在JDK1.8之后的数据结构是数组+链表 / 红黑树(JDK1.7及之前,数据结构为分段式数组+链表)。
- 性能&特性。ConcurrentHahsMap的扩容机制同HashMap。而相比HashTable,ConcurrentHahsMap优化了锁机制,效率更高。
- 安全性。HashMap 是线程不安全的,需要线程安全的场景,一般使用ConcurrentHashMap。ConcurrentHashMap在JDK1.7中实现线程安全的机制是:分段式锁(Segment),即将数组分成若干段,多线程同步时加锁,只锁定当前一个Segment,不同Segment的访问不存在竞争。而到了JDK1.8之后,对原先的分段式锁方式进行了优化,采用CAS+synchronized的方式来保证线程安全,每次加锁只锁定当前数组位置的链表或红黑树的首节点,在hash不冲突的情况下,也不会产生并发。而且CAS自旋锁→synchronized的锁升级方式也进一步优化了效率。