Java集合框架:Set、List、Map及HashMap底层实现详解
一、集合框架概述
Java集合框架主要分为三大类接口:
List(列表):有序集合,元素可重复
实现类:ArrayList、LinkedList、Vector
Set(集):无序集合,元素不可重复
实现类:HashSet、LinkedHashSet、TreeSet
Map(映射):键值对集合
实现类:HashMap、LinkedHashMap、TreeMap、Hashtable
二、List接口及实现类
1. ArrayList
底层结构:动态数组
特点:
随机访问快(O(1))
插入删除慢(需要移动元素,O(n))
线程不安全
扩容机制:
默认初始容量:10
扩容公式:newCapacity = oldCapacity + (oldCapacity >> 1)(即1.5倍)
扩容时调用Arrays.copyOf()复制数组
2. LinkedList
底层结构:双向链表
特点:
插入删除快(O(1))
随机访问慢(需要遍历,O(n))
实现了Deque接口,可用作队列或栈
3. Vector
底层结构:动态数组(类似ArrayList)
特点:
线程安全(方法使用synchronized修饰)
性能较差
扩容默认增长为原来的2倍
三、Set接口及实现类
1. HashSet
底层结构:基于HashMap实现
特点:
元素无序
允许null值
线程不安全
实现原理:
// 实际使用HashMap存储 private transient HashMap<E,Object> map; // 值使用固定Object对象 private static final Object PRESENT = new Object();public boolean add(E e) {return map.put(e, PRESENT)==null; }
2. LinkedHashSet
底层结构:继承HashSet,基于LinkedHashMap实现
特点:
维护插入顺序
性能略低于HashSet
3. TreeSet
底层结构:基于TreeMap实现(红黑树)
特点:
元素有序(自然顺序或Comparator)
不允许null值
基本操作时间复杂度O(log n)
四、Map接口及实现类
1. HashMap(重点详解)
底层结构(JDK8及以后)
数组+链表+红黑树的混合结构
默认初始容量:16
负载因子(loadFactor):0.75(默认)
链表转红黑树阈值:8
红黑树转链表阈值:6
核心字段
transient Node<K,V>[] table; // 哈希桶数组
transient int size; // 键值对数量
int threshold; // 扩容阈值(capacity * loadFactor)
final float loadFactor; // 负载因子
存储结构图示
数组索引:
0 -> null
1 -> Node<K,V> -> Node<K,V> -> null
2 -> TreeNode<K,V> -> TreeNode<K,V> -> null
...
15 -> null
扩容机制
触发条件:
当前size >= threshold
链表长度 >= TREEIFY_THRESHOLD(8)但数组长度 < MIN_TREEIFY_CAPACITY(64)
扩容过程:
创建新数组(大小为原数组2倍)
重新计算所有元素的哈希位置(非常耗时的操作)
JDK8优化:元素在新数组中的位置要么不变,要么是原位置+原数组长度
源码分析(简化版):
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 1. 计算新容量和阈值if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // 双倍}// ... 其他初始化情况处理// 2. 创建新数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 3. 迁移数据if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else {// 链表优化重hashNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}
哈希冲突解决
链表法:哈希冲突时,在数组对应位置形成链表
红黑树优化:当链表长度>=8且数组长度>=64时,链表转为红黑树
重要特性
允许null键和null值
线程不安全
迭代时使用fail-fast机制
2. LinkedHashMap
底层结构:继承HashMap,增加双向链表维护插入顺序
特点:
可预测的迭代顺序
性能略低于HashMap
3. TreeMap
底层结构:红黑树
特点:
按键的自然顺序或Comparator排序
基本操作时间复杂度O(log n)
4. Hashtable
底层结构:类似JDK7的HashMap(数组+链表)
特点:
线程安全(方法使用synchronized修饰)
不允许null键和null值
初始容量11,扩容为2n+1
五、HashMap常见问题
1. 为什么链表长度超过8转为红黑树?
根据泊松分布,哈希冲突达到8的概率极低(小于千万分之一),此时使用红黑树(查询时间O(log n))比链表(O(n))更高效。
2. 为什么初始容量是16?
2的幂次方有利于通过位运算计算索引:index = hash & (length-1)
16是经验值,在大多数情况下提供了良好的性能平衡
3. 为什么负载因子是0.75?
时间和空间的折中:
过高(如1.0):减少空间开销,但增加查询成本
过低(如0.5):减少哈希冲突,但增加扩容频率
4. 如何设计好的键对象?
重写hashCode()和equals()方法
保证不可变性(否则可能导致哈希值变化)
实现Comparable接口(如需排序)
六、总结对比
集合类 | 底层结构 | 线程安全 | 有序性 | 允许null |
---|---|---|---|---|
ArrayList | 动态数组 | 否 | 插入顺序 | 是 |
LinkedList | 双向链表 | 否 | 插入顺序 | 是 |
Vector | 动态数组 | 是 | 插入顺序 | 是 |
HashSet | HashMap | 否 | 无 | 是 |
LinkedHashSet | LinkedHashMap | 否 | 插入顺序 | 是 |
TreeSet | 红黑树 | 否 | 自然顺序 | 否 |
HashMap | 数组+链表+红黑树 | 否 | 无 | 是 |
LinkedHashMap | HashMap+双向链表 | 否 | 插入/访问顺序 | 是 |
TreeMap | 红黑树 | 否 | 自然顺序 | 否 |
Hashtable | 数组+链表 | 是 | 无 | 否 |
理解这些集合类的底层实现和特性,有助于在实际开发中选择最合适的数据结构,编写出更高效的Java代码。