Map 与集合选择总览
学习目标
- 理解 Map 的键唯一性、顺序、排序和视图语义。
- 根据通用查找、保序、范围查询、不可变和并发需求选择实现。
- 通过 Map 常用 API 页面按标题检索每个方法。
核心知识点
专业术语
- HashMap:通用哈希 Map,平均查找接近 O(1),不承诺顺序。
- LinkedHashMap:维护插入顺序或访问顺序。
- TreeMap:按键排序并支持范围查询。
- ConcurrentHashMap:并发读写和单键复合更新,不接受 null。
- 视图:keySet、values、entrySet 与源 Map 共享结构。
白话解释与边界
Map 的键必须保持 equals/hashCode 或比较器关系稳定;修改可变 key 的字段会让它“还在 Map 中却找不到”。HashMap 是默认通用选择,LinkedHashMap 表达稳定展示顺序,TreeMap 表达排序与范围,ConcurrentHashMap 只解决容器级并发访问,不会自动保护跨多个键的业务事务。
细粒度案例见 Map 常用 API;排序、不可变和并发集合边界见 集合排序与不可变集合。
常用用法
Map.put:写入键值
需要新增或覆盖键对应的值时使用 put,返回值是旧值。
// 语义:需要新增或覆盖键对应的值时使用 put,返回值是旧值。
import java.util.LinkedHashMap;
import java.util.Map;
public class MapBasicOverviewDemo {
public static void main(String[] args) {
Map<String, Integer> scores = new LinkedHashMap<>();
scores.put("java", 95);
System.out.println(scores);
// 输出:{java=95}
}
}Map.get:按键读取值
需要读取键对应的值时使用 get,键缺失时返回 null。
// 语义:需要读取键对应的值时使用 get,键缺失时返回 null。
import java.util.Map;
Map<String, Integer> scores = Map.of("java", 95);
System.out.println(scores.get("java"));
// 输出:95Map.containsKey:判断键是否存在
需要区分“键不存在”和“键映射到 null”时使用 containsKey。
// 语义:需要区分“键不存在”和“键映射到 null”时使用 containsKey。
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> scores = new HashMap<>();
scores.put("java", null);
System.out.println(scores.containsKey("java"));
// 输出:trueMap.merge:合并键对应的值
需要累加计数或按规则合并新旧值时使用 merge。
// 语义:需要累加计数或按规则合并新旧值时使用 merge。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MapCompoundOverviewDemo {
public static void main(String[] args) {
Map<String, Integer> count = new HashMap<>();
count.merge("java", 1, Integer::sum);
// 关键变化:count.merge("java", 1, Integer::sum);;向 count 合并 "java", 1, Integer::sum,返回合并后的值;映射内容更新。
System.out.println(count);
// 输出:{java=1}
}
}Map.computeIfAbsent:缺失时初始化值
需要按键延迟创建集合或昂贵对象时使用 computeIfAbsent。
// 语义:需要按键延迟创建集合或昂贵对象时使用 computeIfAbsent。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("java", key -> new ArrayList<>()).add("String");
// 关键变化:groups.computeIfAbsent("java", key -> new ArrayList<>()).add("String");;按 "java", key -> new ArrayList<>() 计算 groups 的映射值,返回新值或 null;映射内容更新。
System.out.println(groups);
// 输出:{java=[String]}HashMap:通用键值映射
不需要稳定迭代顺序或按键排序时,通常使用 HashMap。
// 语义:不需要稳定迭代顺序或按键排序时,通常使用 HashMap。
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class MapChoiceOverviewDemo {
public static void main(String[] args) {
Map<String, Integer> hash = new HashMap<>();
hash.put("b", 2);
System.out.println(hash.getClass().getSimpleName());
// 输出:HashMap
}
}LinkedHashMap:保留插入顺序
需要稳定地按插入顺序遍历键值时使用 LinkedHashMap。
// 语义:需要稳定地按插入顺序遍历键值时使用 LinkedHashMap。
import java.util.LinkedHashMap;
import java.util.Map;
Map<String, Integer> map = new LinkedHashMap<>();
map.put("b", 2);
map.put("a", 1);
System.out.println(map.keySet());
// 输出:[b, a]TreeMap:按键排序
需要键的有序遍历或范围查询时使用 TreeMap。
// 语义:需要键的有序遍历或范围查询时使用 TreeMap。
import java.util.Map;
import java.util.TreeMap;
Map<String, Integer> map = new TreeMap<>();
map.put("b", 2);
map.put("a", 1);
System.out.println(map.keySet());
// 输出:[a, b]按 key 的稳定性选择不可变键
优先使用 String、数字或 record 作为 key;可变 key 放入后不要修改参与 equals/hashCode 的字段。
// 语义:优先使用 String、数字或 record 作为 key。
import java.util.HashMap;
import java.util.Map;
record UserKey(String id) {}
public class MapStableKeyDemo {
public static void main(String[] args) {
Map<UserKey, Integer> map = new HashMap<>();
map.put(new UserKey("u-1"), 95);
System.out.println(map.get(new UserKey("u-1")));
// 输出:95
}
}用 entrySet 遍历键值
entrySet 同时提供 key 和 value;只需要键或值时才使用 keySet 或 values。
// 语义:entrySet 同时提供 key 和 value。
import java.util.LinkedHashMap;
import java.util.Map;
public class MapEntryOverviewDemo {
public static void main(String[] args) {
Map<String, Integer> map = new LinkedHashMap<>(Map.of("java", 95));
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
// 输出:java=95
}
}
}不常用但需要知道
Map.copyOf:复制为不可变 Map
不可变工厂拒绝 null;Map.copyOf 复制容器结构但不会深复制键和值对象。
import java.util.Map;
public class MapImmutableOverviewDemo {
public static void main(String[] args) {
Map<String, Integer> constants = Map.of("ok", 200);
Map<String, Integer> copy = Map.copyOf(constants);
System.out.println(copy);
// 输出:{ok=200}
}
}LinkedHashMap 访问顺序:LRU 基础
访问顺序只维护最近访问排列,不自动实现容量淘汰;完整边界见 Map 常用 API。
import java.util.LinkedHashMap;
public class MapAccessOrderOverviewDemo {
public static void main(String[] args) {
LinkedHashMap<String, Integer> map =
new LinkedHashMap<>(16, 0.75f, true);
// 关键变化:new LinkedHashMap<>(16, 0.75f, true);;使用初始容量 16、负载因子 0.75 和 accessOrder=true 创建按访问顺序迭代的 LinkedHashMap。
map.put("A", 1);
map.put("B", 2);
map.get("A");
// 关键变化:map.get("A");;从 map 按 "A" 读取值,返回值不修改映射内容。
System.out.println(map.keySet());
// 输出:[B, A]
}
}ConcurrentHashMap:并发单键操作
ConcurrentHashMap 不接受 null;merge、computeIfAbsent 的原子性以单键操作为边界,跨键流程仍需要锁或事务协调。
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentMapOverviewDemo {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> count = new ConcurrentHashMap<>();
count.merge("java", 1, Integer::sum);
// 关键变化:count.merge("java", 1, Integer::sum);;向 count 合并 "java", 1, Integer::sum,返回合并后的值;映射内容更新。
System.out.println(count.get("java"));
// 输出:1
}
}简单案例
import java.util.LinkedHashMap;
import java.util.Map;
public class MapSelectionDemo {
public static void main(String[] args) {
Map<String, Integer> scores = new LinkedHashMap<>();
scores.merge("java", 1, Integer::sum);
// 关键变化:scores.merge("java", 1, Integer::sum);;向 scores 合并 "java", 1, Integer::sum,返回合并后的值;映射内容更新。
System.out.println(scores);
// 输出:{java=1}
}
}频次统计使用 merge,比先 get 再 put 更直接;需要顺序时选择 LinkedHashMap。
易混点
- HashMap 不承诺顺序,LinkedHashMap 保序,TreeMap 按 key 排序;三者的顺序区别属于实现契约。
- getOrDefault 不写回,putIfAbsent 和 computeIfAbsent 才可能修改 Map。
- keySet、values、entrySet 是视图;Map.copyOf 才是不可修改结构副本。
- ConcurrentHashMap 的单键原子更新不等于整个业务流程原子。
课后小问
为什么按值排序后通常得到 List<Entry> 而不是“排序后的 HashMap”? 答案:Map 的基本顺序语义按键映射,按值排序更自然地表示为条目序列。 解析:如果要保留展示顺序,可把条目收集到 LinkedHashMap;不能把 HashMap 本身当成按值有序容器。
为什么 ConcurrentHashMap 不接受 null? 答案:并发 Map 需要区分“没有映射”和“映射到 null”,null 会让 get 的语义不明确。 解析:使用 get、putIfAbsent、computeIfAbsent 时,明确的无映射结果有助于保证并发操作语义。
本节小结
- Map 选择先看 key 语义,再看顺序、排序、范围和并发。
- HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap 分别承担不同边界。
- 复合更新优先 merge、computeIfAbsent 等按键操作。
- Map 的视图、不可变副本和并发原子性不能混为一谈。
快速回顾
- 能解释四种常见 Map 实现的选择依据。
- 能写出基础读写、复合更新和 entrySet 遍历。
- 能判断 Map key 是否稳定。
- 能说明 ConcurrentHashMap 的原子边界和 null 限制。
阿源的知识库