一、Map 接口概述
Map 接口是 Java 集合框架中非常重要的一员,位于 java.util
包下。它提供了一种键值对(Key-Value)的存储方式,用于存储具有映射关系的数据,允许通过键(key)来高效地查找对应的值(value)。这种数据结构在编程中非常常见,特别适合需要快速查找和检索的场景。
Map 接口的主要特点
1. 键唯一性
Map 中的每个键(key)都必须是唯一的,不允许出现重复的键。如果向 Map 中放入一个已经存在的键,新的值会覆盖原来的值。
示例代码:
Map<String, String> map = new HashMap<>();
map.put("name", "Alice"); // 第一次放入键"name"
map.put("name", "Bob"); // 覆盖键"name"的值
System.out.println(map.get("name")); // 输出: Bob
2. 值可重复
与键不同,值(value)可以重复,多个不同的键可以对应相同的值。
示例代码:
Map<String, String> map = new HashMap<>();
map.put("firstName", "John"); // 键"firstName"对应值"John"
map.put("lastName", "John"); // 键"lastName"也对应值"John"
System.out.println(map); // 输出: {firstName=John, lastName=John}
3. 键值对映射
每个键都精确地映射到一个具体的值,通过键可以快速获取对应的值(时间复杂度通常为O(1))。
示例代码:
Map<String, Integer> ageMap = new HashMap<>();
ageMap.put("Alice", 25);
ageMap.put("Bob", 30);
int aliceAge = ageMap.get("Alice"); // 25
int bobAge = ageMap.get("Bob"); // 30
Map 接口的层次结构
Map 接口
├── HashMap(无序,基于哈希表实现,性能最好)
├── TreeMap(有序,基于红黑树实现,按key排序)
├── LinkedHashMap(有序,保持插入顺序)
└── Hashtable(线程安全,但已基本被ConcurrentHashMap取代)
应用场景建议
- 快速查找场景:首选HashMap,因其O(1)的平均时间复杂度
- 需要排序的场景:选择TreeMap,保证按键排序
- 需要保持插入顺序的场景:使用LinkedHashMap
- 多线程环境:使用ConcurrentHashMap而非Hashtable
- 缓存实现:常使用LinkedHashMap的访问顺序特性实现LRU缓存
LRU缓存示例:
final int MAX_ENTRIES = 100;
Map<String, String> lruCache = new LinkedHashMap<String, String>(MAX_ENTRIES, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
};
二、Map 接口的常用实现类
(一)HashMap
HashMap 是 Map 接口最常用的实现类之一,它基于哈希表实现,具有以下特点:
-
存储结构:
- HashMap 内部使用数组和链表(JDK 1.8 及以上版本在链表长度达到一定阈值时会转为红黑树)来存储键值对
- 哈希表通过哈希函数将键映射到数组的索引位置,从而实现快速的查找、插入和删除操作
- 示例:当存储键值对("name", "张三")时,会先计算"name"的hashCode(),然后通过哈希算法确定数组索引位置
-
性能:
- 查找、插入和删除操作的平均时间复杂度为O(1)
- 在哈希冲突严重的情况下,性能可能会下降到O(n)
- 扩容机制:默认初始容量为16,负载因子为0.75,当元素数量超过容量*负载因子时会进行扩容
-
线程安全性:
- 非线程安全
- 多线程环境下如果同时进行修改,可能会导致数据不一致或其他异常
- 解决方案:可以使用Collections.synchronizedMap()或ConcurrentHashMap
-
排序特性:
- 元素是无序的
- 不保证键值对的存储顺序和遍历顺序一致
- 应用场景:适合需要快速查找而不关心顺序的场景
(二)TreeMap
TreeMap 基于红黑树实现,它具有以下特点:
-
排序性:
- 元素会按照键的自然顺序(实现Comparable接口)或自定义比较器(通过Comparator实现)进行排序
- 遍历TreeMap时可以得到有序的结果
- 示例:存储("b",2),("a",1),("c",3)会按照"a","b","c"的顺序遍历
-
性能:
- 查找、插入和删除操作的时间复杂度为O(log n)
- 相比HashMap性能稍低
- 适合需要排序的场景
-
线程安全性:
- 非线程安全
- 多线程环境下需要额外的同步措施
- 可以使用Collections.synchronizedSortedMap()包装
-
特殊方法:
- 提供firstKey(), lastKey()等方法获取边界元素
- 提供subMap(), headMap(), tailMap()等方法获取范围视图
(三)LinkedHashMap
LinkedHashMap 是 HashMap 的子类,具有以下特点:
-
有序性:
- 在HashMap基础上增加了一个双向链表
- 可以保证遍历顺序与插入顺序一致(默认情况)
- 或者按照访问顺序排序(通过构造函数设置accessOrder为true)
- 应用场景:适合需要保持插入顺序或实现LRU缓存的场景
-
性能:
- 性能与HashMap相近
- 由于需要维护双向链表,内存开销稍大
- 查找操作时间复杂度仍为O(1)
-
线程安全性:
- 非线程安全
- 多线程环境下需要额外同步
-
实现细节:
- 重写了HashMap的节点类,增加了before和after指针
- 通过重写新节点创建和访问节点的方法来维护链表
(四)Hashtable
Hashtable 是一个古老的 Map 实现类,特点如下:
-
线程安全性:
- 是线程安全的
- 方法大多被synchronized关键字修饰
- 多线程环境下可以安全使用
- 但性能相对较低
-
存储限制:
- 不允许键或值为null
- 而HashMap允许键和值为null(但键只能有一个null)
- 示例:Hashtable.put(null, "value")会抛出NullPointerException
-
性能:
- 由于线程安全的开销,性能不如HashMap
- 在JDK1.0引入,现在一般推荐使用ConcurrentHashMap
-
继承体系:
- 是Dictionary类的子类
- 与HashMap不同,不继承AbstractMap
- 方法命名风格与早期Java集合类一致
三、Map 接口的核心方法
Map 接口是 Java 集合框架中用于存储键值对的核心接口,它定义了一系列用于操作键值对的方法。Map 中的键必须是唯一的,而值可以重复。以下是对 Map 接口常用方法的详细说明和实际应用场景:
(一)添加元素
V put(K key, V value)
- 功能:将指定的键值对放入 Map 中
- 返回值:
- 如果该键已存在,则返回原来的值,并将新值覆盖原来的值
- 如果键不存在,则返回 null
- 示例:
Map<String, Integer> map = new HashMap<>(); map.put("Apple", 10); // 返回 null map.put("Banana", 20); // 返回 null Integer oldValue = map.put("Apple", 15); // 返回 10
void putAll(Map<? extends K, ? extends V> m)
- 功能:将另一个 Map 中的所有键值对添加到当前 Map 中
- 特点:相当于批量 put 操作
- 示例:
Map<String, Integer> map1 = new HashMap<>(); map1.put("Apple", 10); map1.put("Banana", 20); Map<String, Integer> map2 = new HashMap<>(); map2.put("Orange", 30); map2.put("Grape", 40); map1.putAll(map2); // map1 现在包含4个键值对
(二)获取元素
V get(Object key)
- 功能:根据指定的键获取对应的值
- 返回值:如果键不存在,则返回 null
- 注意:由于返回 null 可能表示键不存在或值为 null,可以使用 containsKey 方法区分
- 示例:
Map<String, Integer> map = new HashMap<>(); map.put("Apple", 10); Integer value = map.get("Apple"); // 返回 10 Integer notExist = map.get("Pear"); // 返回 null
boolean containsKey(Object key)
- 功能:判断 Map 中是否包含指定的键
- 应用场景:在调用 get 方法前先检查键是否存在
- 示例:
if (map.containsKey("Apple")) { // 执行相关操作 }
boolean containsValue(Object value)
- 功能:判断 Map 中是否包含指定的值
- 特点:需要遍历所有值,时间复杂度较高
- 示例:
if (map.containsValue(10)) { // 执行相关操作 }
int size()
- 功能:返回 Map 中键值对的数量
- 示例:
int count = map.size(); // 返回当前键值对数量
boolean isEmpty()
- 功能:判断 Map 是否为空(即 size 是否为 0)
- 示例:
if (map.isEmpty()) { // Map为空时的处理逻辑 }
(三)删除元素
V remove(Object key)
- 功能:根据指定的键删除对应的键值对
- 返回值:返回被删除的值。如果键不存在,则返回 null
- 示例:
Integer removed = map.remove("Apple"); // 删除键为Apple的键值对
void clear()
- 功能:清空 Map 中的所有键值对
- 特点:执行后 size 将变为 0
- 示例:
map.clear(); // 清空整个Map
(四)获取集合视图
Set<K> keySet()
- 功能:返回 Map 中所有键组成的 Set 集合
- 特点:返回的是视图,对 Map 的修改会反映在返回的 Set 上
- 应用场景:遍历所有键
- 示例:
for (String key : map.keySet()) { System.out.println(key); }
Collection<V> values()
- 功能:返回 Map 中所有值组成的 Collection 集合
- 特点:返回的是视图,可能包含重复值
- 示例:
for (Integer value : map.values()) { System.out.println(value); }
Set<Map.Entry<K, V>> entrySet()
- 功能:返回 Map 中所有键值对(Entry 对象)组成的 Set 集合
- 特点:
- Entry 是 Map 接口的内部接口
- Entry 包含 getKey() 和 getValue() 方法用于获取键和值
- 效率最高的遍历方式
- 示例:
for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
这些方法构成了 Map 接口的核心功能,为开发人员提供了灵活操作键值对的能力。在实际应用中,根据具体需求选择合适的方法可以大大提高代码的效率和可读性。
四、Map 的遍历方式
遍历 Map 是开发中经常遇到的操作,常见的遍历方式有以下几种:
(一)通过 keySet() 遍历
先获取所有键的集合,然后通过键来获取对应的值。这种方法适用于需要单独处理键或值的场景,比如只需要检查某些键是否存在。示例代码如下:
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
// 遍历键集合
for (String key : map.keySet()) {
// 通过键获取值
Integer value = map.get(key);
System.out.println("key: " + key + ", value: " + value);
// 可以在此处对键进行其他操作
if (key.startsWith("a")) {
System.out.println("Found key starting with 'a': " + key);
}
}
这种方式的缺点是当 Map 较大时,通过get(key)获取值可能会有性能损耗(对于 HashMap 影响不大,但对于 TreeMap 等基于树结构的 Map 来说,每次获取值都需要进行一次查找)。在性能敏感的场景下,建议使用entrySet()。
(二)通过 entrySet() 遍历
获取所有键值对的集合,然后直接遍历键值对。这是最推荐的遍历方式,因为它在所有Map实现中都能保持较好的性能。特别适合需要同时处理键和值的场景。示例代码如下:
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
// 直接遍历键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("key: " + key + ", value: " + value);
// 可以同时访问键值对
if (value > 1) {
System.out.println(key + " has value greater than 1");
}
}
(三)通过 values() 遍历
如果只需要遍历值而不需要键,可以使用这种方式。常见于统计、求和等场景。示例代码如下:
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
// 仅遍历值
int sum = 0;
for (Integer value : map.values()) {
System.out.println("value: " + value);
sum += value;
}
System.out.println("Total sum: " + sum);
(四)使用迭代器遍历
迭代器遍历可以在遍历过程中安全地进行删除操作,避免了ConcurrentModificationException异常。特别适合需要条件删除的场景。示例代码如下:
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
// 使用迭代器遍历
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("key: " + key + ", value: " + value);
// 安全删除符合条件的元素
if (value < 2) {
iterator.remove();
System.out.println("Removed: " + key);
}
}
// 验证删除结果
System.out.println("Remaining items: " + map);
(五)Java 8+ 的 forEach 方法
在Java 8及更高版本中,可以使用更简洁的lambda表达式遍历:
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
// 使用lambda表达式遍历
map.forEach((key, value) -> {
System.out.println("key: " + key + ", value: " + value);
// 可以在此处添加其他处理逻辑
});
五、Map 的排序
(一)TreeMap 的自然排序与自定义排序
TreeMap 是基于红黑树(Red-Black tree)实现的有序映射集合,它默认会按照键的自然顺序(natural ordering)进行排序。这种排序机制要求键必须实现 Comparable
接口。
自然排序示例
-
String 类型键:按照字典顺序(lexicographical order)排序
TreeMap<String, Integer> stringMap = new TreeMap<>(); stringMap.put("zebra", 1); stringMap.put("apple", 2); stringMap.put("banana", 3); // 遍历顺序:apple → banana → zebra
-
Integer 类型键:按照数值大小排序
TreeMap<Integer, String> intMap = new TreeMap<>(); intMap.put(100, "century"); intMap.put(5, "five"); intMap.put(30, "thirty"); // 遍历顺序:5 → 30 → 100
自定义排序
当需要按照非自然顺序排序时,可以在构造 TreeMap 时传入 Comparator
对象:
// 自定义比较器:按字符串长度降序排序
Comparator<String> lengthComparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
// 先按长度降序,长度相同则按字典序
int lengthCompare = Integer.compare(s2.length(), s1.length());
return lengthCompare != 0 ? lengthCompare : s1.compareTo(s2);
}
};
TreeMap<String, Integer> customSortedMap = new TreeMap<>(lengthComparator);
customSortedMap.put("apple", 1);
customSortedMap.put("banana", 2);
customSortedMap.put("pear", 3);
customSortedMap.put("kiwi", 4);
/*
* 遍历顺序:
* banana(6)
* apple(5)
* kiwi(4)
* pear(4)
*/
(二)HashMap 和 LinkedHashMap 的排序处理
HashMap 的无序性
HashMap 是基于哈希表实现的,不保证任何顺序(既不是插入顺序,也不是自然顺序)。它的迭代顺序可能随着时间变化而变化。
LinkedHashMap 的有序性
LinkedHashMap 默认保持键值对的插入顺序(insertion-order),但这不是排序顺序。构造时可通过参数设置为访问顺序(access-order)。
// 保持插入顺序的LinkedHashMap(默认)
LinkedHashMap<String, Integer> insertionOrderMap = new LinkedHashMap<>();
insertionOrderMap.put("apple", 1);
insertionOrderMap.put("banana", 2);
insertionOrderMap.put("pear", 3);
// 遍历顺序:apple → banana → pear
// 访问顺序的LinkedHashMap(最近最少使用)
LinkedHashMap<String, Integer> accessOrderMap = new LinkedHashMap<>(16, 0.75f, true);
accessOrderMap.put("apple", 1);
accessOrderMap.put("banana", 2);
accessOrderMap.put("pear", 3);
accessOrderMap.get("apple"); // 访问后,apple会移到末尾
// 遍历顺序:banana → pear → apple
排序处理方法
对于需要排序的场景,可以先将Map转换为List再进行排序:
1.按键排序:
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("zebra", 1);
hashMap.put("apple", 2);
hashMap.put("banana", 3);
// 转换为Entry列表
List<Map.Entry<String, Integer>> entryList = new ArrayList<>(hashMap.entrySet());
// 按键的自然顺序排序
Collections.sort(entryList, (e1, e2) -> e1.getKey().compareTo(e2.getKey()));
/*
* 排序结果:
* apple=2
* banana=3
* zebra=1
*/
2.按值排序:
// 按数值降序排序
Collections.sort(entryList, (e1, e2) -> e2.getValue().compareTo(e1.getValue()));
/*
* 排序结果:
* banana=3
* zebra=1
* apple=2
*/
3.Java 8+ 的流式处理:
// 按键排序
hashMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(System.out::println);
// 按值排序
hashMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.forEach(System.out::println);
注意:这种排序方式会生成新的排序结果,但不会改变原始Map的结构。如果需要保持排序结果,可以考虑将排序后的结果存入新的LinkedHashMap中。
六、注意事项
(一)键的选择
重写 hashCode() 和 equals() 方法
在使用自定义对象作为 Map 的键时,必须正确重写 hashCode() 和 equals() 方法,原因如下:
- 哈希定位机制:HashMap 等基于哈希表的 Map 实现首先调用键的 hashCode() 方法计算哈希值,然后通过哈希值确定存储位置(桶)
- 相等性判断:当发生哈希冲突时,Map 会调用 equals() 方法判断键是否真正相等
- 不重写的后果:
- 默认的 hashCode() 方法(Object 类提供)基于对象地址计算
- 默认的 equals() 方法使用 == 比较对象引用
- 可能导致相同的逻辑键被当作不同键处理,无法正确查找或删除键值对
示例:
class Person {
private String name;
private int age;
// 必须重写 hashCode 和 equals
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person other = (Person) obj;
return age == other.age && Objects.equals(name, other.name);
}
}
键的不可变性
推荐使用不可变对象作为 Map 的键,原因包括:
-
哈希值稳定性:如果键对象被修改,其 hashCode() 值可能改变,导致:
- 无法通过 get() 方法找到原本存储的值(因为计算出的新哈希值指向不同的桶)
- 可能造成内存泄漏(旧值无法被访问但仍存在于 Map 中)
-
推荐键类型:
- String
- Integer 等包装类
- 其他不可变类
- 如果要使用自定义可变对象,必须确保不会在作为键期间修改其关键字段
不良实践示例:
Map<List<String>, String> map = new HashMap<>();
List<String> key = new ArrayList<>();
key.add("a");
map.put(key, "value");
key.add("b"); // 修改了键对象,导致哈希值改变
System.out.println(map.get(key)); // 可能返回null,无法找到原本存储的值
(二)线程安全问题
线程安全概况
-
非线程安全实现:
- HashMap
- TreeMap
- LinkedHashMap
- 多线程并发修改可能导致:
- 数据不一致
- 无限循环(在JDK7及之前版本的HashMap中可能发生)
- ConcurrentModificationException
-
线程安全解决方案:
-
使用并发容器:
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
- ConcurrentHashMap 特点:
- 分段锁设计(JDK7)或 CAS + synchronized(JDK8+)
- 高并发性能优于 Hashtable
- 不会锁定整个表
- ConcurrentHashMap 特点:
-
使用同步包装器:
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
- 特点:
- 所有方法都使用同步块保护
- 遍历时仍需要手动同步
- 性能低于 ConcurrentHashMap
- 特点:
-
最佳实践:
- 优先选择 ConcurrentHashMap
- 如果只需要读多写少的并发访问,考虑使用 ConcurrentSkipListMap(基于跳表实现)
(三)null 值的处理
各实现类对null的处理策略
Map实现类 | 允许null键 | 允许null值 | 说明 |
---|---|---|---|
HashMap | 是 | 是 | 只能有一个null键 |
LinkedHashMap | 是 | 是 | 继承自HashMap |
TreeMap | 否 | 是 | 因为需要键可比较 |
Hashtable | 否 | 否 | 设计如此 |
ConcurrentHashMap | 否 | 否 | 并发安全限制 |
IdentityHashMap | 是 | 是 | 特殊实现 |
注意事项:
- 使用 TreeMap 时,如果尝试 put null 键会抛出 NullPointerException
- 即使允许 null 值,也要注意避免 NPE:
String value = map.get("nonexistent"); value.toUpperCase(); // 可能抛出NPE
- 建议使用 getOrDefault() 方法提供默认值:
String value = map.getOrDefault("nonexistent", "");
(四)遍历过程中修改 Map
安全遍历和修改策略
-
常见错误:
Map<String, Integer> map = new HashMap<>(); map.put("a", 1); map.put("b", 2); for (String key : map.keySet()) { if (key.equals("a")) { map.remove(key); // 抛出ConcurrentModificationException } }
-
正确做法:
- 使用迭代器的 remove() 方法:
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Integer> entry = it.next(); if (entry.getKey().equals("a")) { it.remove(); // 安全删除 } }
- Java 8+ 可以使用 removeIf():
map.entrySet().removeIf(entry -> entry.getKey().equals("a"));
- 如果需要添加元素,可以先收集要添加的键值对,遍历结束后再添加
- 使用迭代器的 remove() 方法:
-
并发修改检测机制:
- 大多数集合类使用 modCount 字段记录结构性修改次数
- 迭代器会检查预期的 modCount 与实际是否一致
- 不一致则抛出 ConcurrentModificationException
(五)初始容量和加载因子(针对 HashMap)
性能优化参数
-
关键参数:
- 初始容量:哈希表创建时的桶数量,默认为16
- 加载因子:哈希表扩容的阈值比例,默认为0.75
- 当元素数量达到 capacity * loadFactor 时触发扩容
- 扩容通常将容量翻倍(newCapacity = oldCapacity << 1)
-
设置建议:
- 预估元素数量为n时:
int initialCapacity = (int) (n / 0.75f) + 1; Map<String, String> map = new HashMap<>(initialCapacity);
- 加载因子权衡:
- 较高值(如0.9):减少内存使用,但增加哈希冲突
- 较低值(如0.5):减少哈希冲突,但增加内存使用
- 预估元素数量为n时:
-
扩容开销:
- 需要重新计算所有键的哈希值
- 重建哈希表结构
- 在性能敏感场景应避免频繁扩容
-
现代JDK优化:
- JDK8+ 在哈希冲突严重时会将链表转为红黑树
- 优化了哈希算法减少冲突
- 但合理设置初始容量仍有重要意义
性能测试示例:
// 测试不同初始容量对put性能的影响
int elements = 1_000_000;
// 不指定初始容量
long start = System.nanoTime();
Map<Integer, Integer> map1 = new HashMap<>();
for (int i = 0; i < elements; i++) {
map1.put(i, i);
}
System.out.println("Default: " + (System.nanoTime() - start) / 1_000_000 + " ms");
// 合理设置初始容量
start = System.nanoTime();
Map<Integer, Integer> map2 = new HashMap<>((int)(elements / 0.75f) + 1);
for (int i = 0; i < elements; i++) {
map2.put(i, i);
}
System.out.println("Optimized: " + (System.nanoTime() - start) / 1_000_000 + " ms");