Java集合框架:Set、List、Map及HashMap底层实现详解

一、集合框架概述

Java集合框架主要分为三大类接口:

  1. ​List(列表)​​:有序集合,元素可重复

    • 实现类:ArrayList、LinkedList、Vector
  2. ​Set(集)​​:无序集合,元素不可重复

    • 实现类:HashSet、LinkedHashSet、TreeSet
  3. ​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
扩容机制
  1. ​触发条件​​:

    • 当前size >= threshold

    • 链表长度 >= TREEIFY_THRESHOLD(8)但数组长度 < MIN_TREEIFY_CAPACITY(64)

  2. ​扩容过程​​:

    • 创建新数组(大小为原数组2倍)

    • 重新计算所有元素的哈希位置(非常耗时的操作)

    • JDK8优化:元素在新数组中的位置要么不变,要么是原位置+原数组长度

  3. ​源码分析​​(简化版):

    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 {
                     // 链表优化重hash
                     Node<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;
                             else
                                 loTail.next = e;
                             loTail = e;
                         }
                         else {
                             if (hiTail == null)
                                 hiHead = e;
                             else
                                 hiTail.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;

    }

哈希冲突解决
  1. ​链表法​​:哈希冲突时,在数组对应位置形成链表

  2. ​红黑树优化​​:当链表长度>=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代码。