Case-Insensitive Map in Java (TreeMap, Commons, Spring)

A case-insensitive map in Java treats String keys such as “Lokesh” and “LOKESH” as the same key. Create one with a TreeMap and String.CASE_INSENSITIVE_ORDER, Apache Commons CaseInsensitiveMap or Spring LinkedCaseInsensitiveMap.

Java Collections

A case-insensitive map in Java is a Map that treats String keys differing only in letter case, such as “Lokesh” and “LOKESH”, as the same key. The JDK has no CaseInsensitiveHashMap class, so we create a TreeMap with the comparator String.CASE_INSENSITIVE_ORDER, or use a library class such as CaseInsensitiveMap from Apache Commons Collections or LinkedCaseInsensitiveMap from Spring Framework.

We need case-insensitive keys whenever the keys come from people or from systems that do not agree on case, such as HTTP header names or the column names in a CSV file.

The following example creates a case-insensitive TreeMap, with the result of each line as a comment.

Map<String, Integer> ages = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
ages.put("Lokesh", 37);
ages.put("LOKESH", 38);                           // replaces 37, same key

Integer age = ages.get("lokesh");                 // 38
boolean found = ages.containsKey("LoKeSh");       // true
int size = ages.size();                           // 1
String keys = ages.toString();                    // "{Lokesh=38}" (first spelling kept)

Notice that the second put() replaces the value but keeps the key spelling “Lokesh” from the first put(). Each class handles the key spelling, null keys, the iteration order and the lookup speed in its own way.

Next, we see why a normal HashMap fails for these keys and create a case-insensitive map with each class. A comparison table helps to pick the right class, and the FAQs cover converting an existing map and thread safety.

1. Case-Sensitive vs. Case-Insensitive Maps

A HashMap finds a key with the key’s hashCode() and equals() methods. For a String key, both methods are case-sensitive, so “Lokesh” and “lokesh” are two different keys. Strings are the most common map keys, because they are immutable, which is the first rule of a good HashMap key.

By default, keys are case-sensitive. When we put the same name twice in a different case, the map keeps two entries, and a lookup in a third spelling finds nothing.

Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("Lokesh", 37);
hashMap.put("lokesh", 38);

int size = hashMap.size();                        // 2
Integer missing = hashMap.get("LOKESH");          // null

A case-insensitive map keeps one entry for all spellings of a key, so the second put() replaces the value of the first, and get() finds the value in any case.

For example, an API gateway reads the request headers into a map and looks up “Content-Type”. One client sends “content-type” and another sends “CONTENT-TYPE”. HTTP header names are case-insensitive, so both clients are right, and with a HashMap the lookup fails for both of them.

2. Creating Case-Insensitive Maps

Every approach in this section makes get(), put(), containsKey() and remove() ignore the case of the key. The approaches differ in the key spelling they keep, the iteration order, null key support and the lookup speed, as we compare in section 3.

2.1. Using TreeMap with CASE_INSENSITIVE_ORDER Comparator

A TreeMap stores its entries sorted by key, and it finds keys with a Comparator instead of equals(). Two keys are the same key when the comparator returns 0 for them. The comparator String.CASE_INSENSITIVE_ORDER compares two strings and ignores the case, in the same way as compareToIgnoreCase(), so “Lokesh” and “LOKESH” become one key.

TreeMap<String, Integer> treeMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

treeMap.put("Lokesh", 37);
treeMap.put("alex", 30);
treeMap.put("Bob", 25);
treeMap.put("LOKESH", 38);                        // {alex=30, Bob=25, Lokesh=38}

Integer removed = treeMap.remove("ALEX");         // 30, map is {Bob=25, Lokesh=38}
treeMap.put(null, 0);                             // NullPointerException

The TreeMap sorts the keys alphabetically and ignores the case, so “alex” comes before “Bob”. When a key already exists, put() replaces only the value and keeps the first spelling of the key.

A TreeMap does not allow null keys, because the comparator cannot compare null. A TreeMap also needs O(log n) time for get() and put(), whereas a HashMap needs constant time on average. For a map with a few hundred keys, such as request headers or config properties, the difference does not matter in practice, and the TreeMap needs no extra library.

2.2. Using CaseInsensitiveMap

The class CaseInsensitiveMap is part of Apache Commons Collections. It is a hash-based map, so get() and put() run in constant time on average. We add the latest version of the library to the project.

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.6.0</version>
</dependency>

The CaseInsensitiveMap converts every key to lowercase before it stores or compares the key, so the map loses the original spelling. The conversion does not depend on the default locale. The map also supports a null key.

Map<String, Integer> commonsMap = new CaseInsensitiveMap<>();

commonsMap.put("Lokesh", 37);
commonsMap.put("Bob", 25);
commonsMap.put("LOKESH", 38);                     // replaces 37
commonsMap.put(null, 0);                          // null key is allowed

Integer age = commonsMap.get("lokesh");           // 38
Integer nullValue = commonsMap.get(null);         // 0
String keys = commonsMap.keySet().toString();     // "[lokesh, null, bob]" (lowercase, no fixed order)

commonsMap.remove("BOB");                         // removes the entry with the key "bob"

Notice that keySet() returns “lokesh” and “bob” in lowercase, so the map is a poor choice when we print or return the keys to a user. The Javadoc also warns that the map breaks parts of the Map contract, so we should not compare it with other maps through equals().

2.3. Using LinkedCaseInsensitiveMap

The class LinkedCaseInsensitiveMap is part of the spring-core module of Spring Framework. In a Spring or Spring Boot application, spring-core is already on the classpath. In other projects, we add the dependency.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>7.0.9</version>
</dependency>

The class wraps a LinkedHashMap, so it keeps the insertion order and the original spelling of each key, unlike the Apache Commons CaseInsensitiveMap. It has only one type parameter, the value type, because the keys are always String. When we put an existing key in a new spelling, the map stores the new spelling and moves the entry to the end.

Map<String, Integer> springMap = new LinkedCaseInsensitiveMap<>();

springMap.put("Lokesh", 37);
springMap.put("Bob", 25);                         // {Lokesh=37, Bob=25}
springMap.put("LOKESH", 38);                      // {Bob=25, LOKESH=38}

springMap.remove("bob");                          // {LOKESH=38}
Integer age = springMap.get("lokesh");            // 38
Integer noNull = springMap.get(null);             // null
springMap.put(null, 0);                           // NullPointerException

The map does not support null keys, so put(null, …) throws a NullPointerException, whereas get(null) returns null.

The no-argument constructor converts the keys with the default Locale of the JVM. On a server with a Turkish default locale, “TITLE”.toLowerCase() returns a string with a dotless i (U+0131) instead of “title”, so a lookup with “title” returns null. We pass Locale.ROOT to the constructor when the keys are technical names, such as header names or column names.

Locale.setDefault(Locale.forLanguageTag("tr-TR"));

Map<String, Integer> turkish = new LinkedCaseInsensitiveMap<>();
turkish.put("TITLE", 1);
Integer notFound = turkish.get("title");          // null

Map<String, Integer> root = new LinkedCaseInsensitiveMap<>(Locale.ROOT);
root.put("TITLE", 1);
Integer found = root.get("title");                // 1

2.4. Normalizing the Keys of a HashMap

Without a library and without a TreeMap, we can convert every key to lowercase before each put() and get() on a normal HashMap. Normalized keys keep the constant-time HashMap lookups, but the map loses the original spelling, and one missed conversion in the code brings the bug back. So we put the conversion in one small method and call it everywhere.

private static String key(String name) {
  return name.toLowerCase(Locale.ROOT);
}

Map<String, Integer> normalized = new HashMap<>();
normalized.put(key("Lokesh"), 37);
normalized.put(key("LOKESH"), 38);                // replaces 37
Integer age = normalized.get(key("lokesh"));      // 38, map is {lokesh=38}

We always pass Locale.ROOT to toLowerCase(). The method toLowerCase() without an argument has the same Turkish i problem as the default constructor in section 2.3.

3. Choosing a Case-Insensitive Map

Each class keeps a different key spelling and a different order, and the right choice depends on what we do with the keys besides the lookup. The table compares the four approaches on the same calls, put(“Lokesh”, 37) followed by put(“LOKESH”, 38).

TreeMap + CASE_INSENSITIVE_ORDERCommons CaseInsensitiveMapSpring LinkedCaseInsensitiveMapHashMap + toLowerCase(Locale.ROOT)
LibraryJDKcommons-collections4spring-coreJDK
Stored key“Lokesh” (first spelling)“lokesh” (lowercase)“LOKESH” (last spelling)“lokesh” (lowercase)
Iteration ordersorted, case ignoredno fixed orderinsertion orderno fixed order
null keyNullPointerExceptionallowedput() throws NullPointerExceptionNullPointerException in toLowerCase()
get() and put() timeO(log n)O(1) averageO(1) averageO(1) average
Default locale usednonoyes, unless we pass a Localeno, with Locale.ROOT

When the project already uses Spring, LinkedCaseInsensitiveMap is the best default, because it keeps the order and the original spelling of each key. Without Spring, the TreeMap needs no dependency and is fast enough for small maps. The Apache Commons CaseInsensitiveMap fits when we need a null key or constant-time lookups in a large map, and we do not show the keys to a user.

4. Case-Insensitive Map FAQs

4.1. Does Java Have a Built-in Case-Insensitive HashMap?

No. The JDK has no HashMap variant with case-insensitive keys. The closest built-in option is a TreeMap with String.CASE_INSENSITIVE_ORDER from section 2.1, because TreeMap accepts a comparator for its keys and HashMap does not.

4.2. How Do I Make an Existing Map Case-Insensitive?

We create an empty case-insensitive map and copy the entries into it with putAll(). When the old map has two keys that differ only in case, the copy keeps only one of them, so we must check the sizes and decide which value wins.

Map<String, Integer> source = new HashMap<>();
source.put("Lokesh", 37);
source.put("LOKESH", 38);

Map<String, Integer> converted = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
converted.putAll(source);

int before = source.size();                       // 2
int after = converted.size();                     // 1 (one value is lost)

The value that survives depends on the iteration order of the HashMap, which is not defined, so we merge the duplicates on purpose before the copy.

4.3. Why Does equals() Give Different Results for the Same Two Maps?

The method equals() of a Map looks up each of its own keys in the other map. A case-insensitive map finds “LOKESH” under “Lokesh”, but a normal map does not, so the result depends on which map we call it on.

Map<String, Integer> ciMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
ciMap.put("Lokesh", 37);
Map<String, Integer> plain = Map.of("LOKESH", 37);

boolean ciEqualsPlain = ciMap.equals(plain);      // false
boolean plainEqualsCi = plain.equals(ciMap);      // true

So we do not compare a case-insensitive map with a normal map. We compare two maps of the same kind, or we compare the normalized keys.

4.4. Is a Case-Insensitive Map Thread-Safe?

No, none of the classes in section 2 is thread-safe. For a sorted map that many threads read and write, we pass the same comparator to a ConcurrentSkipListMap. For the other classes, we wrap the map with Collections.synchronizedMap(), which locks the map on every call.

Map<String, Integer> concurrent = new ConcurrentSkipListMap<>(String.CASE_INSENSITIVE_ORDER);
concurrent.put("Lokesh", 37);
Integer age = concurrent.get("LOKESH");           // 37

Map<String, Integer> synced = Collections.synchronizedMap(new CaseInsensitiveMap<>());
synced.put("Lokesh", 37);
Integer syncedAge = synced.get("LOKESH");         // 37

5. Conclusion

A HashMap compares String keys with equals(), so “Lokesh” and “LOKESH” are two different keys. To ignore the case, we create a TreeMap with String.CASE_INSENSITIVE_ORDER, which needs no library but does not allow null keys.

In a Spring application, LinkedCaseInsensitiveMap keeps the insertion order and the spelling of the keys, and we pass Locale.ROOT for technical keys. The Apache Commons CaseInsensitiveMap stores lowercase keys and supports a null key. Whichever class we use, we copy existing data with care, because keys that differ only in case collapse into one entry.

6. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.