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

相关推荐
苏supper2 分钟前
记一次Nacos鉴权报错unknown user,403排查|SecurityProxy源码,fastjson2扩展包缺失
java·后端
\光辉岁月/4 分钟前
6.mybatisplus学习-BaseMapper、Service、常用注解
java·mybatis·mybatisplus
微信开发api5 分钟前
基于WTAPI构建社群运营平台:自动拉群与会话承接链路设计
java·大数据·网络·数据库·微信·自动化
小七在进步11 分钟前
类和对象(五)
java·开发语言·算法
驭渊的小故事36 分钟前
SpringBoot 配置文件详解:properties 与 yml 从入门到实战
java·开发语言·笔记
Wx-bishekaifayuan38 分钟前
Springcloud中学在线学习平台58883-计算机课程设计、毕业设计
java·vue.js·spring boot·学习·spring·spring cloud·课程设计
一嘴一个橘子43 分钟前
java 中使用 lua 脚本
java
码智社1 小时前
Spring Framework 全方位技术解析
java
wangqiaowq1 小时前
AST 安全求值
java·开发语言
Java_2017_csdn2 小时前
OFC、ERP、OMS、WMS、TMS 对比-详解
java