Map详解

117 阅读5分钟

Java Map实现类包括HashMap(哈希表高效查询)、LinkedHashMap(维护插入/访问顺序)、TreeMap(红黑树排序)、Hashtable(同步低效)及ConcurrentHashMap(分段锁/CAS高并发),适用于不同场景如缓存、排序和多线程环境。

一、Map 接口核心特性

  • 键值对存储:每个元素包含一个键(Key)和一个值(Value)。
  • 键唯一性:键不可重复(通过 equals()hashCode() 判断)。
  • 无序性(默认):不保证元素的插入顺序(LinkedHashMapTreeMap 例外)。
  • 允许 null:大多数实现类允许键或值为 nullConcurrentHashMap 不允许)。

二、Map 主要实现类对比

实现类底层数据结构线程安全顺序特性时间复杂度(插入/查询)适用场景
HashMap数组+链表/红黑树(JDK8+)非线程安全无序O(1)(平均)快速键值查找、无需顺序
LinkedHashMap哈希表 + 双向链表非线程安全插入顺序/访问顺序O(1)需要保留插入顺序或 LRU 缓存
TreeMap红黑树非线程安全自然排序/定制排序O(log n)需要排序或范围查询
Hashtable数组+链表线程安全无序O(1)(平均)遗留代码、简单同步需求
ConcurrentHashMap分段锁/CAS(JDK8+)线程安全无序O(1)(平均)高并发读写场景

三、各实现类详解

1. HashMap
  • 底层数据结构

    • JDK7:数组 + 链表(哈希冲突通过链表解决)。
    • JDK8+ :数组 + 链表/红黑树(链表长度 ≥8 时树化,<6 时退化为链表)。
  • 哈希计算与索引定位

    // JDK 17 哈希扰动函数
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    // 索引计算:i = (n - 1) & hash
    
  • 扩容机制

    • 初始容量:默认 16,负载因子 0.75。
    • 触发条件:元素数量 > 容量 × 负载因子。
    • 扩容规则:容量翻倍,旧元素重新哈希到新数组。
    • 优化(JDK8+) :通过高位掩码判断元素迁移位置(避免重新计算哈希)。
  • 线程安全:非线程安全,需通过 Collections.synchronizedMap() 包装或使用 ConcurrentHashMap

  • 优点

    • 随机访问速度极快(平均 O(1))。
    • 内存占用合理(数组紧凑)。
  • 缺点

    • 哈希冲突可能影响性能(链表过长或树化)。
    • 不保证顺序。

2. LinkedHashMap
  • 底层数据结构:继承 HashMap,通过双向链表维护顺序。

    // LinkedHashMap.Entry 扩展 Node
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after; // 双向链表指针
    }
    
  • 顺序模式

    • 插入顺序(默认):按元素添加顺序遍历。
    • 访问顺序accessOrder=true):最近访问的元素移动到链表尾部(适合实现 LRU 缓存)。
  • LRU 缓存实现示例

    public class LRUCache<K, V> extends LinkedHashMap<K, V> {
        private final int maxCapacity;
        
        public LRUCache(int maxCapacity) {
            super(maxCapacity, 0.75f, true);
            this.maxCapacity = maxCapacity;
        }
        
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > maxCapacity;
        }
    }
    
  • 优点

    • 保留插入或访问顺序。
    • 适合实现缓存淘汰策略。
  • 缺点

    • 内存占用略高于 HashMap(需维护链表指针)。

3. TreeMap
  • 底层数据结构:基于红黑树(自平衡二叉搜索树)。

    // 红黑树节点定义
    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color; // 红黑标记
    }
    
  • 排序规则

    • 自然排序:键实现 Comparable 接口。
    • 定制排序:通过 Comparator 指定排序逻辑。
  • 范围查询

    // 获取键在 [fromKey, toKey) 范围内的子映射
    SortedMap<K, V> subMap = treeMap.subMap(fromKey, toKey);
    
  • 优点

    • 支持排序和范围查询。
    • 插入、删除、查询稳定在 O(log n)。
  • 缺点

    • 内存占用高(红黑树节点结构复杂)。
    • 性能低于 HashMap

4. Hashtable
  • 底层数据结构:数组 + 链表(类似 JDK7 的 HashMap)。

  • 线程安全:通过 synchronized 方法实现全局锁。

    public synchronized V put(K key, V value) { ... }
    
  • 缺点

    • 高并发下性能差(锁竞争激烈)。
    • 不允许 null 键或值。
  • 现状:已被 ConcurrentHashMap 取代,不推荐新项目使用。


5. ConcurrentHashMap
  • 底层数据结构

    • JDK7:分段锁(Segment 数组 + HashEntry 链表)。
    • JDK8+ :数组 + 链表/红黑树,使用 synchronized + CAS 实现细粒度锁。
  • JDK8+ 核心机制

    • CAS 操作:初始化、节点插入等无锁化。
    • 锁粒度:仅锁定哈希桶的头节点(链表或红黑树)。
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // ...
        synchronized (f) { // 锁住头节点
            if (tabAt(tab, i) == f) {
                // 处理链表或红黑树插入
            }
        }
    }
    
  • 扩容优化

    • 多线程协助扩容:线程在操作时发现正在扩容,会协助迁移数据。
    • 步长分割:每个线程处理固定步长的桶区间。
  • 优点

    • 高并发下性能接近非同步的 HashMap
    • 锁粒度细,减少竞争。
  • 缺点

    • 实现复杂,内存占用较高。

四、扩容机制对比

实现类初始容量负载因子扩容规则触发条件
HashMap160.75容量翻倍size > capacity × loadFactor
LinkedHashMap160.75同 HashMap同 HashMap
TreeMap无固定容量-动态扩展红黑树节点无需显式扩容
Hashtable110.75容量翻倍 +1同 HashMap
ConcurrentHashMap160.75容量翻倍,分段迁移同 HashMap

五、线程安全方案对比

实现类线程安全实现方式锁粒度性能影响
Hashtable方法级同步(synchronized)粗粒度(全局锁)高并发下性能差
Collections.synchronizedMap对象锁(synchronized)粗粒度中等性能开销
ConcurrentHashMapCAS + synchronized(JDK8+)细粒度(桶级别)高并发下性能优异

六、数据结构图示(Mermaid)

1. Map 类图
«interface»Map+put()+get()+remove()«interface»SortedMap+firstKey()+subMap()«interface»ConcurrentMap+putIfAbsent()HashMapLinkedHashMapTreeMapHashtableConcurrentHashMap
2. HashMap 数据结构(JDK8+)
HashMap
桶1: 链表
数组
桶2: 红黑树
桶N: ...

七、选择建议

  • 单线程场景

    • 快速键值查找HashMap
    • 需要插入顺序或 LRU 缓存LinkedHashMap
    • 需要排序或范围查询TreeMap
  • 高并发场景

    • 读写均衡ConcurrentHashMap
    • 遗留系统兼容Hashtable(不推荐新项目)。
  • 避免使用:直接使用 Collections.synchronizedMap() 包装的 HashMap(除非简单同步需求)。


八、示例代码:ConcurrentHashMap 高并发操作

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(4);

// 并发添加元素
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> {
        String key = "key-" + ThreadLocalRandom.current().nextInt(100);
        map.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
    });
}

executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println(map); // 输出各 key 的计数

九、总结

  • HashMap:性能最优的无序键值对存储。
  • LinkedHashMap:适合需要顺序或缓存淘汰策略的场景。
  • TreeMap:唯一支持排序和范围查询的 Map。
  • ConcurrentHashMap:高并发场景的首选线程安全 Map。
  • Hashtable:遗留类,性能差,不推荐使用。