1. 概述
Map
是 Java 中最常用的数据结构之一,而 String
又是 Map
中最常见的键类型。默认情况下,这类 Map
的键是区分大小写的。
本文将介绍几种不同的 Map
实现方式,让所有大小写变体的字符串被视为同一个键,即实现不区分大小写的键匹配。
这在处理 HTTP headers、配置项、用户输入等场景中非常实用,避免因大小写不一致导致的“踩坑”。
2. 问题场景分析
我们先来看一个典型问题。
假设有一个 Map<String, Integer>
,初始插入一条数据:
接着再插入:
map.put("ABC", 2);
在默认的 HashMap
中(区分大小写),结果会是两条独立记录:
而如果使用不区分大小写的 Map
,则第二次 put
应视为对 "abc"
的更新,最终只保留一条记录:
接下来,我们看看如何实现这种行为。
3. 使用 TreeMap + CASE_INSENSITIVE_ORDER
TreeMap
是基于红黑树的 NavigableMap
实现,它通过 Comparator
来排序和判断键的唯一性。
✅ 核心思路:只要传入一个不区分大小写的 Comparator
,就能让 TreeMap
变成不区分大小写的 Map。
幸运的是,String
类自带了一个静态常量:
String.CASE_INSENSITIVE_ORDER
我们可以直接在构造时传入:
Map<String, Integer> treeMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
treeMap.put("abc", 1);
treeMap.put("ABC", 2);
验证结果:
assertEquals(1, treeMap.size()); // 只有一条
assertEquals(2, treeMap.get("aBc").intValue()); // 值被更新为 2
assertEquals(2, treeMap.get("ABc").intValue());
删除时也一样:
treeMap.remove("aBC");
assertEquals(0, treeMap.size());
⚠️ 注意事项
- 性能:
TreeMap
的put
和get
平均时间复杂度为 O(log n),比HashMap
的 O(1) 慢,数据量大时需权衡。 - null 键:
TreeMap
❌ 不允许null
键,否则抛NullPointerException
。 - 排序:天然有序,按键的字典序(忽略大小写)排序,适合需要排序的场景。
4. Apache Commons 的 CaseInsensitiveMap
Apache Commons Collections 提供了 CaseInsensitiveMap
,是基于哈希表的实现。
✅ 特点:
- 内部将所有 key 转为小写进行存储和查找
- ✅ 允许
null
键(但不推荐使用) - 简单粗暴,适合快速集成
引入依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
使用示例
Map<String, Integer> commonsHashMap = new CaseInsensitiveMap<>();
commonsHashMap.put("abc", 1);
commonsHashMap.put("ABC", 2);
测试结果:
assertEquals(1, commonsHashMap.size());
assertEquals(2, commonsHashMap.get("aBc").intValue());
assertEquals(2, commonsHashMap.get("ABc").intValue());
commonsHashMap.remove("aBC");
assertEquals(0, commonsHashMap.size());
⚠️ 注意事项
- 包含在
commons-collections4
,注意不要引入老版本(3.x) - 虽然允许
null
键,但实际使用中建议避免,容易引发歧义
5. Spring 的 LinkedCaseInsensitiveMap
Spring Core 模块提供了 LinkedCaseInsensitiveMap
,封装了 LinkedHashMap
。
✅ 核心优势:
- ✅ 保持插入顺序
- ✅ 保留原始 key 的大小写格式(比如第一次 put 的
"Content-Type"
,遍历时仍为此形式) - ❌ 不允许
null
键 - 适用于 Web 场景,如 HTTP headers 处理
引入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
使用示例
Map<String, Integer> linkedHashMap = new LinkedCaseInsensitiveMap<>();
linkedHashMap.put("abc", 1);
linkedHashMap.put("ABC", 2);
测试:
assertEquals(1, linkedHashMap.size());
assertEquals(2, linkedHashMap.get("aBc").intValue());
assertEquals(2, linkedHashMap.get("ABc").intValue());
linkedHashMap.remove("aBC");
assertEquals(0, linkedHashMap.size());
✅ 保留原始键格式
linkedHashMap.put("Content-Type", "application/json");
// 遍历时,key 仍然是 "Content-Type",而不是小写
linkedHashMap.keySet().forEach(System.out::println); // 输出: Content-Type
⚠️ 注意事项
- 来自 Spring 内部工具类,虽然稳定但不属于 Spring 主要功能模块
- 如果项目已引入 Spring,推荐使用;否则不建议为了这个功能单独引入
6. 总结对比
方案 | 实现方式 | 保持顺序 | 保留原始 key 格式 | 允许 null 键 | 性能 | 适用场景 |
---|---|---|---|---|---|---|
TreeMap + CASE_INSENSITIVE_ORDER |
红黑树 + Comparator | ✅ 是(排序) | ❌ 否(按 Comparator 比较) | ❌ 否 | O(log n) | 需要排序,且不依赖第三方库 |
CaseInsensitiveMap (Apache) |
哈希表 | ❌ 否 | ❌ 否 | ✅ 是 | O(1) | 快速集成,允许 null 键 |
LinkedCaseInsensitiveMap (Spring) |
哈希表 + 链表 | ✅ 是(插入序) | ✅ 是 | ❌ 否 | O(1) | Web 场景,如 headers、配置解析 |
✅ 推荐选择
- 项目用了 Spring?→ 无脑选
LinkedCaseInsensitiveMap
- 用了 Apache Commons?→ 用
CaseInsensitiveMap
- 都没用,且需要排序?→
TreeMap
+String.CASE_INSENSITIVE_ORDER
- 追求极致性能且不介意手写?可以考虑封装一个
HashMap
,手动toLowerCase()
处理 key
所有示例代码均可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-collections-maps-5