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是唯一的标准选择。

相关推荐
better_liang10 分钟前
每日Java面试场景题知识点之-如何设计分布式锁
java·redis·zookeeper·面试·分布式锁
战族狼魂11 分钟前
集 “自动飞行、智能识别、实时预警、勤务联动” 于一体的高速公路应急车道无人机检测系统方案
java·人工智能·大模型·无人机
一只鹿鹿鹿18 分钟前
信息化项目管理规范(参考Word文件)
java·大数据·运维·开发语言·数据库
Java小白笔记19 分钟前
Linux 手动部署 Oracle JDK 17 完全指南
java·linux·oracle
夕除20 分钟前
实战--2
java·spring boot·spring
Chase_______26 分钟前
【Java杂项】final 关键字详解:变量、方法、类限制与引用可变性
java·开发语言·python
凤山老林1 小时前
DDD(领域驱动设计)在复杂业务系统中的落地指南
java·开发语言·数据库·ddd·领域驱动
JEECG低代码平台1 小时前
JimuChatBI — 首款免费开源的 Java 智能问数ChatBI平台,零成本接入,AI对话式智能分析
java·人工智能·开源·aigc·人工智能低代码
星梦清河2 小时前
Java—异步编程
java·开发语言
GIS数据转换器2 小时前
智慧能源管理平台
java·大数据·运维·人工智能·无人机