一. Map接口概述
1.1 Map的核心概念
Java的Map
是集合框架中的核心接口之一,用于存储键值对(Key-Value Pairs),通过键(Key)快速查找对应的值(Value)。与List
和Set
不同,Map
是一种双列集合,其核心特点是:
- 键(Key)的唯一性:同一个
Map
中不能有两个相同的键。 - 值(Value)的可重复性:不同的键可以映射到相同的值。
- 键值对的无序性(除非使用特定实现类如
LinkedHashMap
或TreeMap
)。
1.2 Map与 Collection的区别
特性 | Map | Collection |
---|---|---|
数据结构 | 键值对(Key-Value Pairs) | 单列集合(仅存储值) |
唯一性 | 键唯一,值可重复 | Set 唯一,List 可重复 |
存取方式 | 通过键快速查找值 | 通过索引或迭代器访问元素 |
排序 | 通常无序(TreeMap 可排序) | List 有序,Set 无序(TreeSet 可排序) |
二、Map的常见实现类
2.1 HashMap
-
底层结构:基于哈希表(数组 + 链表/红黑树)。
-
特点:
- 非线程安全:适合单线程环境。
- 允许
null
键和null
值。 - 无序性:键值对的存储顺序与插入顺序无关。
- 性能高:平均时间复杂度为
O(1)
。
-
适用场景:对性能要求高且不需要线程安全的场景(如缓存、数据映射)。
-
HashMap 的优化:
-
链表转红黑树:JDK 8 中,当链表长度超过阈值(默认 8)且哈希表容量达到 64 时,链表会转换为红黑树,查询效率从
O(n)
提升至O(log n)
。 -
初始容量与负载因子:
- 初始容量:默认 16,可以通过构造函数指定。
- 负载因子:默认 0.75,表示当元素数量超过
容量 * 负载因子
时扩容。
-
示例代码:
Map<String, Integer> map = new HashMap<>(); map.put("apple", 5); // 添加键值对 map.put("banana", 3); System.out.println(map.get("apple")); // 输出 5
-
2.2 TreeMap
-
底层结构:基于红黑树(平衡二叉搜索树)。
-
特点:
- 按键自然顺序排序(或自定义比较器)。
- 不允许
null
键(因为null
无法比较大小)。 - 操作时间复杂度为
O(log n)
。
-
适用场景:需要按键排序的场景(如排行榜、区间查询)。
-
自定义排序:
Map<String, Integer> treeMap = new TreeMap<>(Comparator.reverseOrder()); treeMap.put("apple", 5); treeMap.put("banana", 3); System.out.println(treeMap); // 输出 {banana=3, apple=5}
2.3 LinkedHashMap
-
底层结构:继承自
HashMap
,维护一个双向链表。 -
特点:
- 保持插入顺序(或访问顺序)。
- 性能接近
HashMap
。
-
适用场景:需要保持插入顺序或实现 LRU 缓存的场景。
-
LRU 缓存示例:
Map<String, Integer> lruCache = new LinkedHashMap<>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) { return size() > 3; // 保留最多 3 个元素 } }; lruCache.put("apple", 5); lruCache.put("banana", 3); lruCache.put("cherry", 2); lruCache.put("date", 1); // 超出容量,删除最久未使用的 "apple"
2.4 Hashtable
- 底层结构:基于哈希表。
- 特点:
- 线程安全:所有方法都同步。
- 不允许
null
键和null
值。 - 性能较低:由于全表加锁。
- 适用场景:遗留代码或需要线程安全但性能要求不高的场景。
2.5 ConcurrentHashMap
-
底层结构:基于分段锁(Java 8 后优化为 CAS + synchronized)。
-
特点:
- 线程安全:支持高并发。
- 允许
null
值(但不允许null
键)。 - 性能优于
Hashtable
。
-
适用场景:多线程环境下需要高性能的并发访问。
-
并发场景示例:
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("apple", 5); concurrentMap.computeIfAbsent("banana", k -> 3); // 如果键不存在,则计算并插入
2.6 EnumMap
-
底层结构:
- 基于数组:
EnumMap
的底层是一个数组,数组的索引由枚举常量的ordinal()
值决定(即枚举声明的顺序)。 - 类型绑定:
EnumMap
的所有键必须是同一个枚举类型,数组长度等于该枚举类型的常量数量。 - 高效存储:无需计算哈希码或处理哈希冲突,直接通过
ordinal()
值定位数组下标。
- 基于数组:
-
特点:
- 高性能:
- 插入/查找时间复杂度为
O(1)
:通过枚举的ordinal()
值直接访问数组。 - 内存紧凑:数组长度等于枚举常量数量,无冗余空间。
- 插入/查找时间复杂度为
- 有序性:
- 键值对按照枚举常量的声明顺序存储。
- 类型安全:
- 键必须是预定义的枚举类型,编译期检查避免运行时错误。
- 不允许
null
键:- 插入
null
键会抛出NullPointerException
。
-非线程安全: - 与
HashMap
一样,EnumMap
不是线程安全的。
- 插入
- 高性能:
-
适用场景:
- 枚举键的高效映射:当键是枚举类型且需要快速访问时(如状态机、配置映射)。
- 需要保持枚举顺序:例如,按枚举声明顺序遍历数据(如报表生成、流程步骤)。
- 资源受限环境:因内存效率高,适合嵌入式系统或性能敏感场景。
-
示例代码:
enum Season { WINTER, SPRING, SUMMER, FALL } public class EnumMapExample { public static void main(String[] args) { EnumMap<Season, String> seasonMap = new EnumMap<>(Season.class); seasonMap.put(Season.WINTER, "Cold"); seasonMap.put(Season.SPRING, "Warm"); seasonMap.put(Season.SUMMER, "Hot"); seasonMap.put(Season.FALL, "Cool"); // 按枚举声明顺序遍历 for (Season season : Season.values()) { System.out.println(season + ": " + seasonMap.get(season)); } } }
2.7 Properties
-
底层结构:
- 继承自
Hashtable
(Java 8 及之前),Java 9+ 改为基于HashMap
。 - 键值对类型限制:
- 键和值必须是
String
类型(否则会抛出ClassCastException
)。
- 键和值必须是
- 支持属性文件操作:
- 通过
load(InputStream)
从.properties
文件加载键值对。 - 通过
store(OutputStream, String)
将键值对保存到文件。
- 通过
- 继承自
-
特点:
- 线程安全(Java 8 及之前):
- 继承自
Hashtable
,所有方法同步。Java 9+ 改用HashMap
,但Properties
仍通过同步方法保证线程安全。
- 继承自
- 支持注释:
- 属性文件中可使用
#
或!
添加注释。
- 属性文件中可使用
- 默认值支持:
- 可通过
defaults
属性设置默认值,键不存在时自动查找。
- 可通过
- 键值对格式:
- 支持
key=value
、key:value
、key value
等格式。
- 支持
- 不允许
null
键或值:- 插入
null
键/值会抛出NullPointerException
。
- 插入
- 线程安全(Java 8 及之前):
-
适用场景:
- 配置管理:
- 存储和读取应用程序配置(如数据库连接、日志配置、国际化资源)。
- 持久化属性:
- 需要将键值对保存到文件并读取的场景(如用户偏好设置)。
- 跨平台兼容性:
- 属性文件为纯文本,适合跨语言或跨平台的配置共享。
- 配置管理:
-
示例代码:
import java.io.*; import java.util.Properties; public class PropertiesExample { public static void main(String[] args) throws IOException { Properties props = new Properties(); // 从文件加载属性 try (InputStream in = new FileInputStream("config.properties")) { props.load(in); } // 获取属性值 String dbUrl = props.getProperty("database.url"); System.out.println("Database URL: " + dbUrl); // 设置新属性并保存到文件 props.setProperty("new.key", "new.value"); try (OutputStream out = new FileOutputStream("config.properties")) { props.store(out, "Updated config"); } } }
三、Map的常用方法
3.1 添加/更新键值对
-
V put(K key, V value)
插入键值对,若键存在则覆盖旧值,返回旧值。map.put("apple", 5);
-
void putAll(Map<? extends K, ? extends V> m)
合并另一个Map
的键值对。map.putAll(anotherMap);
3.2 获取值
-
V get(Object key)
返回指定键对应的值,若键不存在则返回null
。int count = map.get("banana");
-
V getOrDefault(Object key, V defaultValue)
返回指定键对应的值,若键不存在则返回默认值。int count = map.getOrDefault("grape", 0);
3.3 删除键值对
-
V remove(Object key)
删除指定键的键值对,返回被删除的值。map.remove("apple");
-
void clear()
清空所有键值对。map.clear();
3.4 查询操作
-
boolean containsKey(Object key)
判断是否包含指定键。boolean hasApple = map.containsKey("apple");
-
boolean containsValue(Object value)
判断是否包含指定值。boolean hasFive = map.containsValue(5);
-
int size()
返回键值对数量。int size = map.size();
-
boolean isEmpty()
判断是否为空。boolean isEmpty = map.isEmpty();
3.5 遍历操作
-
Set<K> keySet()
返回所有键的集合。for (String key : map.keySet()) { System.out.println(key); }
-
Collection<V> values()
返回所有值的集合。for (Integer value : map.values()) { System.out.println(value); }
-
Set<Map.Entry<K, V>> entrySet()
返回所有键值对的集合。for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " -> " + entry.getValue()); }
3.6 Java 8 新增方法
-
default void forEach(BiConsumer<? super K, ? super V> action)
对每个键值对执行操作。map.forEach((key, value) -> System.out.println(key + ": " + value));
-
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
如果键不存在,则计算并插入值。map.computeIfAbsent("pear", k -> 7);
四、使用Map的注意事项
4.1 键的唯一性和一致性
- 键必须正确实现
hashCode()
和equals()
方法,否则可能导致哈希冲突或查找失败。 - 自定义键的示例:
class CustomKey { private String name; private int id; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CustomKey that = (CustomKey) o; return id == that.id && Objects.equals(name, that.name); } @Override public int hashCode() { return Objects.hash(name, id); } }
4.2 线程安全
- 非线程安全的 Map(如
HashMap
)在多线程环境下需使用Collections.synchronizedMap
包装或选择ConcurrentHashMap
。Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
4.3 性能优化
- 初始容量和负载因子:预估数据量,设置合适的初始容量和负载因子以减少扩容次数。
- 避免哈希冲突:确保键的
hashCode()
和equals()
方法合理,减少链表或红黑树的使用。
4.4 遍历顺序
HashMap
和TreeMap
的遍历顺序与插入顺序无关。LinkedHashMap
保持插入顺序或访问顺序(通过构造方法指定)。
五、遍历Map的方式
5.1 使用 keySet()
遍历
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
5.2 使用 entrySet()
遍历
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
5.3 使用 Iterator
遍历 吗,
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
5.4 使用 Java 8 的 Stream API
map.entrySet().stream().forEach(entry ->
System.out.println(entry.getKey() + ": " + entry.getValue())
);
六、常见问题与解决方案
6.1 如何保证键的唯一性?
Map
自动检查键的唯一性,重复键会覆盖旧值。确保键的equals()
和hashCode()
方法正确实现。
6.2 如何处理 null
键和 null
值?
HashMap
允许null
键和null
值,但TreeMap
不允许null
键(因为无法比较大小)。
6.3 如何在多线程环境下安全使用 Map
?
- 使用
ConcurrentHashMap
或通过Collections.synchronizedMap
包装普通Map
。
6.4 如何遍历时修改 Map
?
- 应当使用迭代器的
remove()
方法。如果直接修改Map
会抛出ConcurrentModificationException
。
七、总结
7.1 选择实现类
HashMap
:高性能、无序、允许null
。TreeMap
:按键排序。LinkedHashMap
:保持插入顺序或实现 LRU 缓存。ConcurrentHashMap
:高并发环境下的线程安全。
7.2 方法使用
- 添加/更新:
put
、putAll
。 - 获取/删除:
get
、remove
。 - 遍历:
keySet
、entrySet
、forEach
。
7.3 性能优化
- 合理设置初始容量和负载因子。
- 避免哈希冲突。
- 使用不可变对象作为键。
7.4 实际应用场景
- 缓存:使用
HashMap
或ConcurrentHashMap
。 - 数据统计:使用
HashMap
统计频率。 - 配置管理:使用
TreeMap
按键排序。