Java Map常用方法和实现类深度详解

文章目录

    • 前言
    • [第一章 Map接口概述](#第一章 Map接口概述)
      • [1.1 Map的继承体系](#1.1 Map的继承体系)
      • [1.2 Map的核心特性](#1.2 Map的核心特性)
      • [1.3 存储结构的理解](#1.3 存储结构的理解)
    • [第二章 HashMap:最常用的Map实现](#第二章 HashMap:最常用的Map实现)
      • [2.1 底层数据结构演进](#2.1 底层数据结构演进)
      • [2.2 核心源码深度解析](#2.2 核心源码深度解析)
        • [2.2.1 重要成员变量](#2.2.1 重要成员变量)
        • [2.2.2 设计哲学解读](#2.2.2 设计哲学解读)
      • [2.3 put方法执行流程](#2.3 put方法执行流程)
      • [2.4 扩容机制(resize)](#2.4 扩容机制(resize))
      • [2.5 线程安全问题](#2.5 线程安全问题)
    • [第三章 LinkedHashMap:保持插入顺序](#第三章 LinkedHashMap:保持插入顺序)
      • [3.1 数据结构特点](#3.1 数据结构特点)
      • [3.2 两种排序模式](#3.2 两种排序模式)
      • [3.3 实现LRU缓存](#3.3 实现LRU缓存)
      • [3.4 性能特点](#3.4 性能特点)
    • [第四章 TreeMap:基于红黑树的排序Map](#第四章 TreeMap:基于红黑树的排序Map)
      • [4.1 排序机制](#4.1 排序机制)
      • [4.2 核心方法](#4.2 核心方法)
      • [4.3 源码分析:compare方法](#4.3 源码分析:compare方法)
      • [4.4 注意事项](#4.4 注意事项)
    • [第五章 Hashtable与Properties](#第五章 Hashtable与Properties)
      • [5.1 Hashtable:古老的线程安全Map](#5.1 Hashtable:古老的线程安全Map)
      • [5.2 Properties:处理配置文件](#5.2 Properties:处理配置文件)
    • [第六章 ConcurrentHashMap:并发编程的利器](#第六章 ConcurrentHashMap:并发编程的利器)
      • [6.1 设计哲学](#6.1 设计哲学)
      • [6.2 JDK 7实现:分段锁](#6.2 JDK 7实现:分段锁)
      • [6.3 JDK 8+实现:CAS + synchronized](#6.3 JDK 8+实现:CAS + synchronized)
      • [6.4 弱一致性迭代器](#6.4 弱一致性迭代器)
      • [6.5 批量操作](#6.5 批量操作)
    • [第七章 Map常用方法详解](#第七章 Map常用方法详解)
      • [7.1 基础操作方法](#7.1 基础操作方法)
      • [7.2 查询方法](#7.2 查询方法)
      • [7.3 遍历方法](#7.3 遍历方法)
        • [7.3.1 entrySet遍历(最常用)](#7.3.1 entrySet遍历(最常用))
        • [7.3.2 keySet + get遍历](#7.3.2 keySet + get遍历)
        • [7.3.3 values遍历(仅需值时)](#7.3.3 values遍历(仅需值时))
        • [7.3.4 Iterator遍历(支持remove)](#7.3.4 Iterator遍历(支持remove))
        • [7.3.5 Java 8 forEach(最简洁)](#7.3.5 Java 8 forEach(最简洁))
        • [7.3.6 Stream API遍历(支持链式操作)](#7.3.6 Stream API遍历(支持链式操作))
      • [7.4 Java 8+新增的默认方法](#7.4 Java 8+新增的默认方法)
        • [7.4.1 computeIfAbsent / computeIfPresent](#7.4.1 computeIfAbsent / computeIfPresent)
        • [7.4.2 merge方法](#7.4.2 merge方法)
        • [7.4.3 putIfAbsent](#7.4.3 putIfAbsent)
        • [7.4.4 replace / replaceAll](#7.4.4 replace / replaceAll)
      • [7.5 Java 9的Map.of工厂方法](#7.5 Java 9的Map.of工厂方法)
    • [第八章 实现类对比与选型指南](#第八章 实现类对比与选型指南)
      • [8.1 核心特性对比](#8.1 核心特性对比)
      • [8.2 时间复杂度对比](#8.2 时间复杂度对比)
      • [8.3 选型建议](#8.3 选型建议)
      • [8.4 性能测试数据参考](#8.4 性能测试数据参考)
    • [第九章 常见陷阱与最佳实践](#第九章 常见陷阱与最佳实践)
      • [9.1 陷阱一:可变对象作为键](#9.1 陷阱一:可变对象作为键)
      • [9.2 陷阱二:自定义类未重写hashCode和equals](#9.2 陷阱二:自定义类未重写hashCode和equals)
      • [9.3 陷阱三:并发修改导致ConcurrentModificationException](#9.3 陷阱三:并发修改导致ConcurrentModificationException)
      • [9.4 最佳实践总结](#9.4 最佳实践总结)
    • 结语

前言

在Java集合框架中,Map是最核心、最常用的数据结构之一。与Collection体系下的List、Set不同,Map采用**键值对(Key-Value)**的存储方式,每个键映射到一个值,键在同一个Map中不可重复。这种设计使得Map特别适合需要通过键快速查找值的场景,如缓存系统、配置管理、数据索引等。

本文将从Map接口的设计哲学出发,深入剖析HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap等主要实现类的底层原理、源码实现、性能特性,并结合Java 8+的新特性,帮助读者全面掌握Map的使用技巧和选型策略。


第一章 Map接口概述

1.1 Map的继承体系

Java中的Map体系是一个独立于Collection的并行框架,其核心继承结构如下:

复制代码
Map (interface)
├── HashMap (class)
│   └── LinkedHashMap (class)
├── TreeMap (class)
├── Hashtable (class)
│   └── Properties (class)
└── ConcurrentMap (interface)
    └── ConcurrentHashMap (class)

1.2 Map的核心特性

  • 键唯一性 :每个键最多映射到一个值,键的不可重复性通过equals()hashCode()保证
  • 值可重复:不同的键可以对应相同的值
  • 元素无序:大部分Map实现(如HashMap)不保证元素的顺序
  • 允许null:HashMap允许一个null键和多个null值,但Hashtable和ConcurrentHashMap不允许

1.3 存储结构的理解

从数据结构角度看,Map的存储可以分为三个层面:

  1. key视角 :所有key构成一个Set集合 → 无序、不可重复,key所在的类必须重写equals()hashCode()
  2. value视角 :所有value构成一个Collection集合 → 无序、可重复,value所在的类需要重写equals()
  3. entry视角 :每个key-value对构成一个Entry对象,所有entry构成一个Set集合 → 无序、不可重复

这种设计体现了Map与Set、List的内在联系,也为后续的遍历操作奠定了基础。


第二章 HashMap:最常用的Map实现

HashMap是基于哈希表 实现的Map,它根据键的hashCode值存储数据,具有O(1)的平均查找时间,是日常开发中使用频率最高的Map实现。

2.1 底层数据结构演进

HashMap的底层实现经历了从JDK 7到JDK 8的重要优化:

版本 底层结构 节点类型 特点
JDK 7 数组 + 链表 Entry 头插法,扩容时可能产生循环链表
JDK 8+ 数组 + 链表 + 红黑树 Node/TreeNode 尾插法,链表长度>8且数组长度>64时树化

2.2 核心源码深度解析

2.2.1 重要成员变量
java 复制代码
// 默认初始容量16,必须是2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表转红黑树阈值
static final int TREEIFY_THRESHOLD = 8;

// 红黑树转链表阈值
static final int UNTREEIFY_THRESHOLD = 6;

// 树化最小数组容量
static final int MIN_TREEIFY_CAPACITY = 64;
2.2.2 设计哲学解读

为什么默认负载因子是0.75?

负载因子表示散列表的空间使用程度。0.75是时间与空间的折中选择:

  • 过高(如1):空间利用率高,但Hash碰撞概率增加,链表变长,查询效率下降
  • 过低(如0.5):Hash碰撞减少,查询快,但空间浪费严重

为什么容量必须是2的n次幂?

这涉及HashMap的核心优化:

  1. 高效取模 :计算数组下标时,(n - 1) & hash等价于hash % n,位运算速度远快于取模
  2. 均匀分布:2^n-1的二进制全是1,与运算结果能充分利用hash值的所有位,减少碰撞
  3. 扩容优化:扩容后元素的新位置要么在原位置,要么在原位置+旧容量,只需看hash值新增位是0还是1

为什么链表转红黑树的阈值是8?

这是基于泊松分布 的概率统计。在理想随机hashCode下,链表节点数出现的概率遵循泊松分布,节点数为8的概率接近千万分之六,此时链表查询性能已经很差,转为红黑树可以挽回性能。而树节点占用的空间是普通节点的两倍,当节点数降到6时再转回链表,避免频繁转换。

2.3 put方法执行流程

HashMap的put方法是理解其工作原理的关键入口:

java 复制代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    // 1. 数组延迟初始化:首次put时创建数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    // 2. 计算下标,如果该位置为空直接插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 3. 处理Hash冲突
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // 第一个节点就是要找的key
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 红黑树插入
        else {
            // 链表遍历
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null); // 尾插法
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash); // 检查是否需要树化
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 4. 找到相同key,替换value
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 5. 检查是否需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

执行流程总结

  1. 计算key的hash值(扰动函数:高16位与低16位异或)
  2. 通过(n - 1) & hash计算数组下标
  3. 如果该位置为空,直接插入
  4. 如果该位置不为空,遍历链表或红黑树
  5. 找到相同key则替换value,否则插入新节点
  6. 检查是否需要树化或扩容

2.4 扩容机制(resize)

当元素个数超过threshold = capacity * loadFactor时,HashMap会进行扩容:

java 复制代码
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    // 计算新容量
    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; // 阈值也翻倍
    }
    // ... 初始化逻辑
    
    // 创建新数组
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    
    // 数据迁移
    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 {
                    // 链表拆分:保持原顺序
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 关键优化:根据hash值新增位判断新位置
                        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;
                        }
                        e = next;
                    } while (e != null);
                    
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead; // 原索引位置
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead; // 原索引+旧容量
                    }
                }
            }
        }
    }
    return newTab;
}

扩容优化点

  • JDK 8采用尾插法,避免JDK 7头插法在多线程环境下产生的循环链表问题
  • 元素迁移时,无需重新计算hash,只需看e.hash & oldCap是否为0,为0则留在原位,否则移到原位置+oldCap
  • 链表保持原顺序,不会倒置

2.5 线程安全问题

HashMap是线程不安全的,多线程环境下可能出现以下问题:

  1. 数据覆盖:两个线程同时put,计算出的下标相同,一个线程插入的数据可能被另一个覆盖
  2. size不准确++size操作非原子性,多个线程同时put可能导致size偏小
  3. JDK 7扩容死循环:头插法在并发扩容时可能形成环形链表,导致CPU 100%

解决方案:

  • 使用Collections.synchronizedMap(new HashMap<>())
  • 使用ConcurrentHashMap推荐

第三章 LinkedHashMap:保持插入顺序

LinkedHashMap继承自HashMap,在HashMap基础上通过双向链表维护元素的顺序。

3.1 数据结构特点

java 复制代码
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after; // 前驱和后继指针
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

LinkedHashMap在HashMap的Node基础上增加了beforeafter指针,构成了一个双向链表,用于记录元素的插入顺序或访问顺序。

3.2 两种排序模式

LinkedHashMap支持两种迭代顺序:

  1. 插入顺序(默认):按元素首次插入Map的顺序迭代
  2. 访问顺序:按元素最近被访问(get/put)的时间从旧到新迭代
java 复制代码
// 指定访问顺序
Map<String, String> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("a", "1");
map.put("b", "2");
map.get("a"); // 访问a,a会被移动到链表尾部
// 迭代顺序:b, a(最近访问的在最后)

3.3 实现LRU缓存

利用访问顺序模式,可以轻松实现LRU(Least Recently Used)缓存

java 复制代码
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int maxCapacity;
    
    public LRUCache(int maxCapacity) {
        super(16, 0.75f, true); // 启用访问顺序
        this.maxCapacity = maxCapacity;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxCapacity; // 超过容量时移除最久未访问的元素
    }
}

3.4 性能特点

  • 遍历速度:只与元素个数有关,与HashMap容量无关,因此当HashMap容量大而实际元素少时,LinkedHashMap遍历更快
  • 插入性能:略低于HashMap,因为需要维护双向链表
  • 内存占用:比HashMap多两个指针的开销

第四章 TreeMap:基于红黑树的排序Map

TreeMap实现了SortedMapNavigableMap接口,底层基于红黑树实现,能够对键进行排序。

4.1 排序机制

TreeMap要求键要么实现Comparable接口(自然排序),要么在构造时提供Comparator(定制排序):

java 复制代码
// 自然排序:键必须实现Comparable
TreeMap<Integer, String> naturalMap = new TreeMap<>();

// 定制排序:提供Comparator
TreeMap<String, Integer> customMap = new TreeMap<>(
    (s1, s2) -> s2.compareTo(s1) // 降序
);

4.2 核心方法

TreeMap提供了丰富的导航方法:

java 复制代码
TreeMap<Integer, String> map = new TreeMap<>();
map.put(1, "one");
map.put(3, "three");
map.put(5, "five");
map.put(7, "seven");

Integer firstKey = map.firstKey();        // 1
Integer lastKey = map.lastKey();          // 7
Integer lowerKey = map.lowerKey(5);       // 3(小于5的最大键)
Integer floorKey = map.floorKey(4);       // 3(小于等于4的最大键)
Integer ceilingKey = map.ceilingKey(4);   // 5(大于等于4的最小键)
Integer higherKey = map.higherKey(5);     // 7(大于5的最小键)

// 子Map视图
SortedMap<Integer, String> headMap = map.headMap(5);   // 键<5的部分
SortedMap<Integer, String> tailMap = map.tailMap(5);   // 键>=5的部分
SortedMap<Integer, String> subMap = map.subMap(3, 6);  // 3<=键<6

4.3 源码分析:compare方法

TreeMap的核心是比较逻辑,它在putgetremove等操作中都会用到:

java 复制代码
final int compare(Object k1, Object k2) {
    return comparator == null ? 
        ((Comparable<? super K>)k1).compareTo((K)k2) : 
        comparator.compare((K)k1, (K)k2);
}

如果既没有提供Comparator,键也没有实现Comparable,在插入时会抛出ClassCastException

4.4 注意事项

  1. 键不能为null:因为无法比较null

  2. compareTo与equals需一致 :当两个键比较结果为0时,TreeMap认为它们相等,即使equals返回false

  3. 字符串键的特殊性 :字符串的compareTo基于Unicode值,数字字符串排序时需注意

    java 复制代码
    // 错误:字符串排序按字典序,"22"会排在"5"前面
    TreeMap<String, Integer> map = new TreeMap<>();
    map.put("5", 1);
    map.put("22", 2); // 实际顺序:22, 5
    
    // 正确:转为整数比较
    TreeMap<String, Integer> map = new TreeMap<>(
        (a, b) -> Integer.parseInt(a) - Integer.parseInt(b)
    );

第五章 Hashtable与Properties

5.1 Hashtable:古老的线程安全Map

Hashtable是JDK 1.0就存在的古老实现类,具有以下特点:

  • 线程安全 :所有方法都用synchronized修饰
  • 不允许null键和null值:否则抛出NullPointerException
  • 初始容量11 ,扩容为2*old+1
  • 性能较低:全表锁导致并发性能差
java 复制代码
Hashtable<String, Integer> table = new Hashtable<>();
table.put("key", 1);
// table.put(null, 2); // 运行时异常

性能对比

  • 写入速度:Hashtable可能比HashMap快(测试数据:1420ms vs 797ms)
  • 读取速度:HashMap比Hashtable快(188ms vs 265ms)

5.2 Properties:处理配置文件

Properties继承自Hashtable,专门用于处理配置文件,键和值都是String类型。

java 复制代码
Properties props = new Properties();
props.setProperty("url", "jdbc:mysql://localhost:3306/db");
props.setProperty("username", "root");
props.setProperty("password", "123456");

// 加载配置文件
try (InputStream input = new FileInputStream("config.properties")) {
    props.load(input);
    String url = props.getProperty("url");
    String username = props.getProperty("username");
}

常用方法:

  • load(InputStream) / store(OutputStream):加载/存储配置文件
  • getProperty(String key, String defaultValue):获取属性,可指定默认值
  • list(PrintStream):打印所有属性

第六章 ConcurrentHashMap:并发编程的利器

ConcurrentHashMap是Java并发包(java.util.concurrent)中提供的线程安全且高性能的Map实现。

6.1 设计哲学

ConcurrentHashMap的设计目标是:在保证线程安全的同时,提供比Hashtable更高的并发性能

实现类 锁策略 并发度 性能
Hashtable 全表锁 极低
Collections.synchronizedMap 全表锁 极低
ConcurrentHashMap JDK 7 分段锁 16
ConcurrentHashMap JDK 8+ CAS + synchronized + 细粒度锁 极高 非常高

6.2 JDK 7实现:分段锁

JDK 7的ConcurrentHashMap采用Segment分段锁机制:

  • 将整个Map分成多个Segment(默认16个)
  • 每个Segment独立加锁,相当于一个小型的HashMap
  • 不同Segment的写操作可以并发执行
  • 读操作几乎不加锁(volatile保证可见性)
java 复制代码
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile HashEntry<K,V>[] table;
    // ...
}

6.3 JDK 8+实现:CAS + synchronized

JDK 8对ConcurrentHashMap进行了重大重构:

  1. 放弃分段锁 ,改用CAS + synchronized实现
  2. 与HashMap结构对齐:数组+链表+红黑树
  3. 锁粒度更细:只锁住链表或红黑树的头节点
  4. 读操作完全无锁(volatile保证可见性)
java 复制代码
// putVal核心片段
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ... 非空校验等
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // 初始化,CAS保证线程安全
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 该位置为空,CAS尝试插入
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f); // 帮助扩容
        else {
            V oldVal = null;
            synchronized (f) { // 锁住链表头节点
                // 链表或红黑树操作
            }
        }
    }
}

6.4 弱一致性迭代器

ConcurrentHashMap的迭代器是弱一致性的:

  • 迭代器创建后,如果Map发生修改,不会抛出ConcurrentModificationException
  • 迭代器反映的是创建时刻或之后某个时刻的数据快照
  • 迭代过程中修改Map,迭代器可能看到,也可能看不到修改结果
  • 适用于高并发场景,避免了快速失败机制带来的问题

6.5 批量操作

ConcurrentHashMap提供了强大的批量操作API:

java 复制代码
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// forEach:遍历每个元素
map.forEach(1, (k, v) -> System.out.println(k + ":" + v));

// search:查找第一个符合条件的元素
String result = map.search(1, (k, v) -> v > 100 ? k : null);

// reduce:累加操作
Integer sum = map.reduceValues(1, Integer::sum);

// 用作频率统计(MultiSet)
ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>();
freqs.computeIfAbsent("word", k -> new LongAdder()).increment();

parallelismThreshold参数控制并行度:小于阈值时串行执行,大于阈值时并行执行。


第七章 Map常用方法详解

7.1 基础操作方法

方法 描述 返回值说明
put(K key, V value) 添加键值对 返回该key之前的value,如果没有则返回null
get(Object key) 根据key获取value 存在则返回value,否则返回null
remove(Object key) 删除键值对 返回被删除的value
clear() 清空所有键值对 void
size() 返回键值对数量 int
isEmpty() 判断是否为空 boolean

7.2 查询方法

方法 描述
containsKey(Object key) 判断是否包含指定键
containsValue(Object value) 判断是否包含指定值(HashMap中效率较低,需遍历)
getOrDefault(Object key, V defaultValue) 获取值,不存在则返回默认值

7.3 遍历方法

Map的遍历方式多样,可根据场景选择:

7.3.1 entrySet遍历(最常用)
java 复制代码
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
7.3.2 keySet + get遍历
java 复制代码
for (String key : map.keySet()) {
    System.out.println(key + ": " + map.get(key));
}
// 缺点:每次get都需要二次查找,效率较低
7.3.3 values遍历(仅需值时)
java 复制代码
for (Integer value : map.values()) {
    System.out.println(value);
}
7.3.4 Iterator遍历(支持remove)
java 复制代码
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Integer> entry = iterator.next();
    if (entry.getValue() < 0) {
        iterator.remove(); // 安全删除
    }
}
7.3.5 Java 8 forEach(最简洁)
java 复制代码
map.forEach((key, value) -> System.out.println(key + ": " + value));
7.3.6 Stream API遍历(支持链式操作)
java 复制代码
map.entrySet().stream()
    .filter(entry -> entry.getValue() > 10)
    .forEach(entry -> System.out.println(entry.getKey()));

7.4 Java 8+新增的默认方法

Java 8在Map接口中增加了多个实用默认方法,极大地简化了代码:

7.4.1 computeIfAbsent / computeIfPresent
java 复制代码
// 如果key不存在,则通过函数计算value并放入Map
map.computeIfAbsent("key", k -> new ArrayList<>()).add("value");

// 经典用法:实现多值Map
Map<String, List<String>> multiMap = new HashMap<>();
multiMap.computeIfAbsent("group1", k -> new ArrayList<>()).add("item1");

// 如果key存在,则根据原值计算新值
map.computeIfPresent("key", (k, v) -> v * 2);
7.4.2 merge方法
java 复制代码
// 合并操作:如果key不存在则放入给定值,存在则通过合并函数计算新值
map.merge("key", 1, Integer::sum); // 统计功能

// 经典用法:单词计数
String text = "apple banana apple orange apple";
Map<String, Integer> wordCount = new HashMap<>();
for (String word : text.split(" ")) {
    wordCount.merge(word, 1, Integer::sum);
}
// 结果:{apple=3, banana=1, orange=1}
7.4.3 putIfAbsent
java 复制代码
// 仅在key不存在时放入
map.putIfAbsent("key", "value");
7.4.4 replace / replaceAll
java 复制代码
// 替换指定key的值(仅当存在时)
map.replace("key", "newValue");

// 对所有entry应用替换函数
map.replaceAll((k, v) -> v.toUpperCase());

7.5 Java 9的Map.of工厂方法

Java 9提供了更简洁的Map初始化方式:

java 复制代码
// 创建不可变Map(最多支持10对键值)
Map<String, Integer> map1 = Map.of(
    "a", 1,
    "b", 2,
    "c", 3
);

// 任意数量键值对
Map<String, Integer> map2 = Map.ofEntries(
    Map.entry("a", 1),
    Map.entry("b", 2),
    Map.entry("c", 3),
    Map.entry("d", 4)
);

第八章 实现类对比与选型指南

8.1 核心特性对比

特性 HashMap LinkedHashMap TreeMap Hashtable ConcurrentHashMap
顺序 无序 插入/访问顺序 键排序 无序 无序
null键 允许1个 允许1个 不允许 不允许 不允许
null值 允许 允许 允许 不允许 不允许
线程安全 是(全表锁) 是(分段/CAS)
性能 最高 略低于HashMap 较低(log n) 读慢写快 高并发下最优
底层结构 数组+链表+红黑树 数组+链表+红黑树+双向链表 红黑树 数组+链表 CAS+数组+链表+红黑树
适用场景 通用缓存 需保持顺序 需排序/范围查询 遗留系统 高并发共享数据

8.2 时间复杂度对比

操作 HashMap LinkedHashMap TreeMap Hashtable ConcurrentHashMap
get O(1) O(1) O(log n) O(1) O(1)
put O(1) O(1) O(log n) O(1) O(1)
remove O(1) O(1) O(log n) O(1) O(1)
containsKey O(1) O(1) O(log n) O(1) O(1)
containsValue O(n) O(n) O(n) O(n) O(n)

8.3 选型建议

根据不同的业务场景,选择合适的Map实现:

场景1:通用缓存,无特殊顺序要求

  • 首选:HashMap(性能最高)
  • 如果线程安全要求:ConcurrentHashMap

场景2:需要保持插入顺序

  • 首选:LinkedHashMap
  • 案例:实现FIFO队列、记录操作日志

场景3:需要按键排序或范围查询

  • 首选:TreeMap
  • 案例:排行榜、日程表、字典序输出

场景4:实现LRU缓存

  • 首选:LinkedHashMap(访问顺序模式)
  • 案例:内存缓存、最近访问记录

场景5:高并发共享数据

  • 首选:ConcurrentHashMap
  • 案例:全局配置、在线用户统计

场景6:处理配置文件

  • 首选:Properties
  • 案例:读取application.properties

8.4 性能测试数据参考

根据实际测试(百万级数据):

操作 HashMap LinkedHashMap TreeMap Hashtable
插入100万条 1420ms 1512ms 3845ms 797ms
读取1000万条 188ms 201ms 892ms 265ms

注:Hashtable插入快可能是由于其初始容量较小,扩容频率高导致的测试偏差,实际应用中HashMap综合性能最优。


第九章 常见陷阱与最佳实践

9.1 陷阱一:可变对象作为键

java 复制代码
// 错误示例
Map<List<String>, String> map = new HashMap<>();
List<String> key = new ArrayList<>();
key.add("a");
map.put(key, "value1");

key.add("b"); // 键被修改,hashCode改变
map.get(key); // 返回null,再也找不到
map.containsKey(key); // false

解决方案 :使用不可变对象作为键,如String、Integer,或自定义不可变类。

9.2 陷阱二:自定义类未重写hashCode和equals

java 复制代码
class User {
    String name;
    // 没有重写hashCode和equals
}

Map<User, Integer> map = new HashMap<>();
User u1 = new User("Alice");
User u2 = new User("Alice");
map.put(u1, 100);
map.get(u2); // 返回null,虽然内容相同

解决方案 :作为键的类必须正确重写hashCode()equals()

9.3 陷阱三:并发修改导致ConcurrentModificationException

java 复制代码
Map<String, Integer> map = new HashMap<>();
// ... 填充数据
for (String key : map.keySet()) {
    if (key.startsWith("temp")) {
        map.remove(key); // 抛出ConcurrentModificationException
    }
}

解决方案

java 复制代码
// 方式1:使用Iterator的remove
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
    String key = it.next();
    if (key.startsWith("temp")) {
        it.remove();
    }
}

// 方式2:使用removeIf(Java 8+)
map.keySet().removeIf(key -> key.startsWith("temp"));

// 方式3:使用ConcurrentHashMap(允许并发修改)

9.4 最佳实践总结

  1. 预估初始容量:如果能预知数据规模,指定初始容量避免频繁扩容

    java 复制代码
    Map<String, Integer> map = new HashMap<>(expectedSize * 4 / 3 + 1);
  2. 使用泛型:指定键值类型,避免运行时类型转换异常

  3. 优先使用Java 8+默认方法:让代码更简洁

    java 复制代码
    // 老式
    if (!map.containsKey(key)) {
        map.put(key, new ArrayList<>());
    }
    map.get(key).add(value);
    
    // 新式
    map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
  4. 选择合适的实现:根据业务需求而非习惯选择

  5. 注意线程安全:多线程环境优先使用ConcurrentHashMap

  6. 避免使用Hashtable:除非维护遗留代码


结语

Java Map体系经过多年的演进,从最早的Hashtable,到JDK 1.2引入的HashMap,再到JDK 1.5的ConcurrentHashMap,以及后续的各种优化,已经形成了一套功能完备、性能卓越的数据结构家族。

理解Map的核心原理,不仅有助于写出更高效的代码,还能在遇到复杂业务场景时做出正确的技术选型。本文从源码层面剖析了各个Map实现类的底层机制,并结合实际场景给出了使用建议。在实际开发中,建议遵循"面向接口编程"的原则,根据具体需求选择最合适的Map实现,同时注意线程安全和键的不可变性等关键问题。

Map的学习是一个循序渐进的过程,掌握基础用法后,深入理解其设计思想和源码实现,才能真正做到"知其然,知其所以然"。

相关推荐
_codemonster1 小时前
JavaWeb开发系列(九)idea配置jdbc
java·ide·intellij-idea
Hx_Ma161 小时前
测试题(六)
java·tomcat·mybatis
码云数智-大飞1 小时前
.NET 10 & C# 14 新特性详解:扩展成员 (Extension Members) 全面指南
java·数据库·算法
Anastasiozzzz1 小时前
阿亮随手录-SpringBoot启动流程、三级缓存要求、BeanFactory与FactoryBean、AutoWired与Resource、不推荐字段注入
java·spring
枫叶丹41 小时前
【Qt开发】Qt界面优化(五)-> Qt样式表(QSS) 子控件选择器
c语言·开发语言·数据库·c++·qt
Never_Satisfied2 小时前
在c#中,实现把图片文件拖动到pictureBox控件上
开发语言·c#
独自破碎E2 小时前
BISHI61 小q的数列
java·开发语言
计算机毕设VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue连锁门店管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
Dylan的码园2 小时前
从软件工程师看计算机是如何工作的
java·jvm·windows·java-ee