Java基础 map集合

Java Map集合核心知识点总结(HashMap/TreeMap/Hashtable/ConcurrentHashMap)

在Java集合框架中,Map是与Collection并列的顶级接口,代表键值对(key-value) 形式的存储结构,键(key)唯一不可重复,值(value)可重复,且一个键只能映射一个值。本文将从底层原理、源码解析、性能对比、面试陷阱等维度深入梳理Map的四大核心实现类,帮你彻底掌握Map集合的使用与选型。

一、HashMap:最常用的无序哈希表实现

1. 核心特点

  • 底层基于哈希表 实现,JDK1.7为数组+单向链表 ,JDK1.8优化为数组+链表+红黑树
  • 无序:不保证元素的插入顺序,也不保证顺序随时间不变
  • 允许一个null键多个null值(null键固定存放在数组索引0的位置)
  • 线程不安全:多线程环境下操作会出现并发修改异常,甚至导致数据结构损坏
  • 查找、插入、删除的平均时间复杂度为O(1),是综合性能最高的Map实现

2. 底层原理与核心机制(重点)

(1)存储结构(JDK1.8)
  • 数组(Node<K,V>[] table):哈希表的主体,初始默认容量为16,必须是2的幂次方
  • 链表:解决哈希冲突,当多个key的哈希值相同时,以链表形式存储在同一个数组位置
  • 红黑树:当链表长度≥8且数组容量≥64时,链表自动转换为红黑树,将查询时间复杂度从O(n)优化为O(logn);当红黑树节点数≤6时,自动退化为链表
(2)哈希计算与索引定位
java 复制代码
// JDK1.8哈希计算:key的hashCode()异或其高16位,减少哈希冲突
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 索引定位:哈希值 & (数组长度 - 1),等价于取模运算但效率更高
int index = hash(key) & (table.length - 1);
(3)扩容机制
  • 触发条件:当元素数量(size)超过容量 × 负载因子(默认0.75)时触发扩容
  • 扩容规则:新容量为原容量的2倍(保证2的幂次方特性)
  • 扩容过程:创建新的数组,将原数组的所有键值对重新哈希并分配到新数组中,这是HashMap的主要性能瓶颈
  • 特殊情况:如果原数组容量已达到MAXIMUM_CAPACITY(2^30),则不再扩容,将负载因子设置为1.0
(4)put方法核心流程
  1. 计算key的哈希值,定位数组索引
  2. 如果数组该位置为空,直接创建新节点插入
  3. 如果该位置已有节点:
    • 若key已存在(哈希值相同且equals返回true),则覆盖旧值
    • 若节点是红黑树节点,调用红黑树的插入方法
    • 若节点是链表节点,遍历链表插入尾部;若插入后链表长度≥8,触发树化检查

3. 常用方法与示例

(1)添加与修改元素
java 复制代码
HashMap<String, Integer> map = new HashMap<>();
// 添加键值对,若key已存在则覆盖旧值,返回旧值
map.put("Tom", 100);
map.put("Jim", 90);
// 批量添加另一个Map的所有键值对
HashMap<String, Integer> map1 = new HashMap<>();
map1.put("Sam", 91);
map.putAll(map1);
// 仅当key不存在时才添加,返回null表示添加成功
map.putIfAbsent("Tom", 95);
// 替换指定key的value,返回旧值
map.replace("Jim", 95);
(2)查询元素
java 复制代码
// 根据key获取value,不存在则返回null
int score = map.get("Tom");
// 根据key获取value,不存在则返回默认值
int defaultScore = map.getOrDefault("Bob", 0);
// 判断是否包含指定key
boolean hasTom = map.containsKey("Tom");
// 判断是否包含指定value
boolean has90 = map.containsValue(90);
// 获取键值对数量
int size = map.size();
// 判断是否为空
boolean isEmpty = map.isEmpty();
(3)删除元素
java 复制代码
// 根据key删除键值对,返回被删除的value
map.remove("Tom");
// 仅当key和value都匹配时才删除,返回boolean
map.remove("Jim", 90);
// 清空所有键值对
map.clear();
(4)三种遍历方式
java 复制代码
// 1. 遍历key集合(适合只需要key的场景)
for (String key : map.keySet()) {
    System.out.println(key + " = " + map.get(key));
}

// 2. 遍历value集合(适合只需要value的场景)
for (Integer value : map.values()) {
    System.out.println(value);
}

// 3. 遍历entrySet(推荐,性能最高,避免二次get)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

4. 常见陷阱与注意事项

  1. 必须重写key的hashCode()和equals()方法:HashMap通过这两个方法判断key是否相等,若不重写会导致相同内容的key被视为不同对象
  2. 避免使用可变对象作为key:如果key的内容发生变化,其hashCode也会变化,导致无法找到对应的value
  3. JDK1.7 HashMap死循环问题:JDK1.7采用头插法扩容,多线程环境下可能导致链表成环,引发死循环;JDK1.8改为尾插法解决了该问题,但仍不保证线程安全
  4. 初始容量优化 :如果提前知道元素数量,创建HashMap时指定初始容量(建议设置为预计元素数 / 0.75 + 1),避免频繁扩容

二、TreeMap:基于红黑树的有序Map实现

1. 核心特点

  • 底层基于红黑树实现,所有键值对按照key的排序规则存储
  • 有序:支持自然排序和自定义排序,是唯一实现了SortedMap接口的常用Map
  • 不允许为null(需要参与排序比较),值可以为null
  • 线程不安全:多线程环境下操作会出现并发修改异常
  • 查找、插入、删除的时间复杂度均为O(logn),性能低于HashMap

2. 排序规则

TreeMap提供两种排序方式,优先级:自定义排序 > 自然排序

  1. 自然排序 :key必须实现Comparable接口,重写compareTo()方法。String、Integer等包装类已默认实现该接口
  2. 自定义排序 :创建TreeMap时传入Comparator接口的实现类,重写compare()方法

3. 常用方法与示例

(1)基本使用
java 复制代码
// 自然排序:String按字典序,Integer按数值升序
TreeMap<String, Integer> map = new TreeMap<>();
map.put("orange", 1);
map.put("apple", 2);
map.put("pear", 3);
System.out.println(map); // 输出:{apple=2, orange=1, pear=3}

// 自定义排序:按key的长度升序
TreeMap<String, Integer> customMap = new TreeMap<>((s1, s2) -> s1.length() - s2.length());
customMap.put("banana", 4);
customMap.put("apple", 2);
customMap.put("pear", 3);
System.out.println(customMap); // 输出:{pear=3, apple=2, banana=4}
(2)特有方法(有序性相关)
java 复制代码
// 获取第一个(最小)key和最后一个(最大)key
String firstKey = map.firstKey();
String lastKey = map.lastKey();
// 获取第一个和最后一个键值对
Map.Entry<String, Integer> firstEntry = map.firstEntry();
Map.Entry<String, Integer> lastEntry = map.lastEntry();
// 获取小于等于指定key的最大键值对
Map.Entry<String, Integer> floorEntry = map.floorEntry("orange");
// 获取大于等于指定key的最小键值对
Map.Entry<String, Integer> ceilingEntry = map.ceilingEntry("orange");
// 获取子Map:[fromKey, toKey)
SortedMap<String, Integer> subMap = map.subMap("apple", "pear");
// 获取小于toKey的子Map
SortedMap<String, Integer> headMap = map.headMap("orange");
// 获取大于等于fromKey的子Map
SortedMap<String, Integer> tailMap = map.tailMap("orange");

4. 注意事项

  • 排序规则必须与equals()方法保持一致,否则会出现"equals返回true但compareTo返回非0"的情况,导致TreeMap行为异常
  • 由于基于红黑树实现,插入和删除操作需要维护红黑树的平衡,因此性能低于HashMap,仅在需要有序遍历的场景下使用

三、Hashtable:已过时的线程安全哈希表

1. 核心特点

  • 底层基于哈希表实现,结构与JDK1.7 HashMap类似(数组+单向链表)
  • 线程安全 :所有方法都添加了synchronized同步锁,但锁粒度大,性能极差
  • 键和值都不允许为null ,否则会抛出NullPointerException
  • 默认初始容量为11,负载因子0.75,扩容规则为新容量 = 旧容量 × 2 + 1
  • 不建议使用 :属于JDK1.0的遗留类,设计老旧且性能差,如需线程安全的Map,推荐使用ConcurrentHashMap替代

2. 与HashMap的核心区别

特性 HashMap Hashtable
线程安全 不安全 安全(方法级加锁)
null键值 允许1个null键,多个null值 不允许任何null键和值
初始容量 16 11
扩容规则 2倍 2倍 + 1
底层结构 数组+链表+红黑树(JDK1.8) 数组+链表
性能 低(同步开销大)
迭代器类型 快速失败(fail-fast) 快速失败(fail-fast)

3. 常用方法(了解即可)

Hashtable的方法与HashMap基本一致,同时提供了一些历史遗留方法:

  • elements():返回value的Enumeration迭代器
  • keys():返回key的Enumeration迭代器
  • contains(Object value):功能等价于containsValue(),命名不规范

四、ConcurrentHashMap:高并发场景的首选

1. 核心原理

ConcurrentHashMap是java.util.concurrent包下的线程安全Map实现,不同JDK版本的实现差异较大:

  • JDK1.7:采用**分段锁(Segment)**机制,将哈希表分为16个段,每个段独立加锁,支持16个线程并发写
  • JDK1.8 :取消分段锁,采用数组+链表+红黑树+CAS+synchronized机制,锁粒度降低到单个数组节点,并发性能大幅提升

2. 核心特性

  • 线程安全:通过CAS和synchronized保证并发操作的安全性
  • 高并发:读操作完全无锁,写操作仅锁定当前节点,支持大量线程并发访问
  • 不允许null键和null值(与Hashtable一致)
  • 初始容量16,负载因子0.75,扩容规则与HashMap相同(2倍)
  • 迭代器是弱一致性迭代器,遍历过程中允许修改集合,不会抛出并发修改异常

3. 优缺点与适用场景

  • 优点 :并发性能远高于Hashtable和Collections.synchronizedMap,是高并发场景下的标准选择
  • 缺点:弱一致性迭代器只能保证看到遍历开始时已存在的数据,不保证看到后续的修改
  • 适用场景:高并发读多写少场景,如缓存、计数器、路由表等

五、Map集合核心总结与选型对比

1. Map接口通用特性

  • 存储键值对,键唯一不可重复,值可重复
  • 一个键只能映射一个值,重复添加相同键会覆盖旧值
  • 支持通过key快速查找value,是查找效率最高的数据结构之一
  • 所有实现类都提供了添加、删除、查询、遍历的基本方法

2. 四大实现类对比表

特性 HashMap TreeMap Hashtable ConcurrentHashMap
底层数据结构 数组+链表+红黑树(JDK1.8) 红黑树 数组+链表 数组+链表+红黑树+CAS(JDK1.8)
有序性 无序 有序(按key排序) 无序 无序
null键值 允许1个null键,多个null值 不允许null键,允许null值 不允许任何null键和值 不允许任何null键和值
线程安全 不安全 不安全 安全(方法级加锁) 安全(细粒度锁)
平均时间复杂度 O(1) O(logn) O(1) O(1)
并发性能 极低 极高
适用场景 单线程通用场景 需要有序遍历的场景 无(仅维护老代码) 高并发读多写少场景
综合性能 最高 中等 最低 高(并发场景)

3. 补充注意事项

  1. 并发安全方案
    • 轻度并发场景:使用Collections.synchronizedMap(Map map)包装HashMap
    • 高并发场景:必须使用ConcurrentHashMap,绝对不要使用Hashtable
  2. 遍历性能选择
    • 所有Map实现类都推荐使用entrySet()遍历,避免使用keySet()遍历后再调用get()获取value
    • 不要在遍历过程中使用Map的remove()方法删除元素,应使用迭代器的remove()方法
  3. Collections工具类常用方法
    • Collections.unmodifiableMap(Map map):返回不可修改的Map视图,防止误修改
    • Collections.synchronizedMap(Map map):返回线程安全的Map包装类
    • Collections.emptyMap():返回一个空的不可变Map

结论

Map集合是Java开发中处理键值对数据的核心工具,掌握其底层原理和选型技巧能显著提升代码质量和运行性能。日常开发中,绝大多数单线程场景优先使用HashMap;当需要对键值对进行有序排序和遍历的时候,选择TreeMap;Hashtable由于性能和设计缺陷,除非是维护遗留老代码,否则坚决不要使用;在高并发场景下,ConcurrentHashMap是唯一的标准选择。

相关推荐
凤山老林2 小时前
从0到1搭建企业级权限管理系统:Spring Boot + JWT + RBAC实战指南
java·spring boot·后端·权限管理·rbac
逍遥德2 小时前
AI时代,计算机专业大学生学习指南
java·javascript·人工智能·学习·ai编程
Maiko Star2 小时前
让 AI 开口说话:Spring AI Alibaba 语音合成(TTS)实战
java·人工智能·spring·springai
programhelp_3 小时前
Pinterest OA 题库大公开|Programhelp 独家整理(最新高频)
java·开发语言
likerhood3 小时前
Fastjson中的JSON.parseObject()详细讲解
java·json
KNeeg_3 小时前
黑马点评完整代码(RabbitMQ优化)+简历编写+面试重点 ⭐
java·redis·后端·spring·面试·职场和发展·黑马点评
铁皮哥4 小时前
【后端/Agent 开发】给你的项目配置一套 .claude/ 工作流:别再裸用 Claude Code 了!
java·windows·python·spring·github·maven·生活
乐之者v4 小时前
AI编程 -- codex添加代码,在intellij Idea中没有显示,如何处理?
java·ide·intellij-idea
2401_878820474 小时前
Sa-Token基础篇
java·spring boot·后端·sa-token