1. 概述

Map 是 Java 中最常用的数据结构之一,而 String 又是 Map 中最常见的键类型。默认情况下,这类 Map 的键是区分大小写的

本文将介绍几种不同的 Map 实现方式,让所有大小写变体的字符串被视为同一个键,即实现不区分大小写的键匹配

这在处理 HTTP headers、配置项、用户输入等场景中非常实用,避免因大小写不一致导致的“踩坑”。

2. 问题场景分析

我们先来看一个典型问题。

假设有一个 Map<String, Integer>,初始插入一条数据:

firstEntry

接着再插入:

map.put("ABC", 2);

在默认的 HashMap 中(区分大小写),结果会是两条独立记录:

sensetiveKey-1

而如果使用不区分大小写的 Map,则第二次 put 应视为对 "abc" 的更新,最终只保留一条记录:

insensetiveKey-1

接下来,我们看看如何实现这种行为。

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());

⚠️ 注意事项

  • 性能TreeMapputget 平均时间复杂度为 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


原始标题:Java Map With Case-Insensitive Keys