java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java map常用方法

Java Map常用方法和实现类的核心原理

作者:百锦再@新空间创想科技

在Java集合框架中,Map是最核心、最常用的数据结构之一,本文将从Map接口的设计哲学出发,深入剖析HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap等主要实现类的底层原理、源码实现、性能特性,感兴趣的朋友跟随小编一起看看吧

前言

在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的核心特性

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 重要成员变量

// 默认初始容量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是时间与空间的折中选择:

为什么容量必须是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方法是理解其工作原理的关键入口:

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会进行扩容:

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;
}

扩容优化点

2.5 线程安全问题

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

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

解决方案:

第三章 LinkedHashMap:保持插入顺序

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

3.1 数据结构特点

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)的时间从旧到新迭代
// 指定访问顺序
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)缓存

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 性能特点

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

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

4.1 排序机制

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

// 自然排序:键必须实现Comparable
TreeMap<Integer, String> naturalMap = new TreeMap<>();
// 定制排序:提供Comparator
TreeMap<String, Integer> customMap = new TreeMap<>(
    (s1, s2) -> s2.compareTo(s1) // 降序
);

4.2 核心方法

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

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等操作中都会用到:

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值,数字字符串排序时需注意
    // 错误:字符串排序按字典序,"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就存在的古老实现类,具有以下特点:

Hashtable<String, Integer> table = new Hashtable<>();
table.put("key", 1);
// table.put(null, 2); // 运行时异常

性能对比

5.2 Properties:处理配置文件

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

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");
}

常用方法:

第六章 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分段锁机制:

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保证可见性)
// 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的迭代器是弱一致性的:

6.5 批量操作

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

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遍历(最常用)

for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

7.3.2 keySet + get遍历

for (String key : map.keySet()) {
    System.out.println(key + ": " + map.get(key));
}
// 缺点:每次get都需要二次查找,效率较低

7.3.3 values遍历(仅需值时)

for (Integer value : map.values()) {
    System.out.println(value);
}

7.3.4 Iterator遍历(支持remove)

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(最简洁)

map.forEach((key, value) -> System.out.println(key + ": " + value));

7.3.6 Stream API遍历(支持链式操作)

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

// 如果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方法

// 合并操作:如果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

// 仅在key不存在时放入
map.putIfAbsent("key", "value");

7.4.4 replace / replaceAll

// 替换指定key的值(仅当存在时)
map.replace("key", "newValue");
// 对所有entry应用替换函数
map.replaceAll((k, v) -> v.toUpperCase());

7.5 Java 9的Map.of工厂方法

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

// 创建不可变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 核心特性对比

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

8.2 时间复杂度对比

操作HashMapLinkedHashMapTreeMapHashtableConcurrentHashMap
getO(1)O(1)O(log n)O(1)O(1)
putO(1)O(1)O(log n)O(1)O(1)
removeO(1)O(1)O(log n)O(1)O(1)
containsKeyO(1)O(1)O(log n)O(1)O(1)
containsValueO(n)O(n)O(n)O(n)O(n)

8.3 选型建议

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

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

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

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

场景4:实现LRU缓存

场景5:高并发共享数据

场景6:处理配置文件

8.4 性能测试数据参考

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

操作HashMapLinkedHashMapTreeMapHashtable
插入100万条1420ms1512ms3845ms797ms
读取1000万条188ms201ms892ms265ms

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

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

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

// 错误示例
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

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

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

解决方案

// 方式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 最佳实践总结

Map<String, Integer> map = new HashMap<>(expectedSize * 4 / 3 + 1);
// 老式
if (!map.containsKey(key)) {
    map.put(key, new ArrayList<>());
}
map.get(key).add(value);
// 新式
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);

结语

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

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

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

到此这篇关于Java Map常用方法和实现类深度详解的文章就介绍到这了,更多相关java map常用方法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文