Class CaseInsensitiveMap<K,V>
- Type Parameters:
K
- the type of keys maintained by this map (String keys are case-insensitive)V
- the type of mapped values
- All Implemented Interfaces:
ConcurrentMap<K,
,V> Map<K,
V>
String
keys, while preserving
the original case of the keys. Non-String keys are treated as they would be in a regular Map
.
When the backing map is a MultiKeyMap
, this map also supports multi-key operations
with case-insensitive String key handling.
ConcurrentMap Implementation: This class implements ConcurrentMap
and provides
all concurrent operations (putIfAbsent
, replace
, bulk operations, etc.) with case-insensitive
semantics. Thread safety depends entirely on the backing map implementation:
- Thread-Safe: When backed by concurrent maps (
ConcurrentHashMap
,ConcurrentHashMapNullSafe
,ConcurrentSkipListMap
,ConcurrentNavigableMapNullSafe
, etc.), all operations are thread-safe. - Not Thread-Safe: When backed by non-concurrent maps (
LinkedHashMap
,HashMap
, etc.), concurrent operations work correctly but without thread-safety guarantees.
Choose your backing map implementation based on your concurrency requirements.
Key Features
- Case-Insensitive String Keys:
String
keys are internally stored asCaseInsensitiveString
objects, enabling case-insensitive equality and hash code behavior. - Preserves Original Case: The original casing of String keys is maintained for retrieval and iteration.
- Compatible with All Map Operations: Supports Java 8+ map methods such as
computeIfAbsent()
,computeIfPresent()
,merge()
, andforEach()
, with case-insensitive handling of String keys. - Concurrent Operations: Implements
ConcurrentMap
interface with full support for concurrent operations includingputIfAbsent()
,replace()
, and bulk operations with parallelism control. - Customizable Backing Map: Allows developers to specify the backing map implementation or automatically chooses one based on the provided source map.
- Thread-Safe Case-Insensitive String Cache: Efficiently reuses
CaseInsensitiveString
instances to minimize memory usage and improve performance.
Usage Examples
// Create a case-insensitive map with default LinkedHashMap backing (not thread-safe)
CaseInsensitiveMap<String, String> map = new CaseInsensitiveMap<>();
map.put("Key", "Value");
LOG.info(map.get("key")); // Outputs: Value
LOG.info(map.get("KEY")); // Outputs: Value
// Create a thread-safe case-insensitive map with ConcurrentHashMap backing
ConcurrentMap<String, String> concurrentMap = CaseInsensitiveMap.concurrent();
concurrentMap.putIfAbsent("Key", "Value");
LOG.info(concurrentMap.get("key")); // Outputs: Value (thread-safe)
// Alternative: explicit constructor approach
ConcurrentMap<String, String> explicitMap = new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMap<>());
// Create a case-insensitive map from an existing map
Map<String, String> source = Map.of("Key1", "Value1", "Key2", "Value2");
CaseInsensitiveMap<String, String> copiedMap = new CaseInsensitiveMap<>(source);
// Use with non-String keys
CaseInsensitiveMap<Integer, String> intKeyMap = new CaseInsensitiveMap<>();
intKeyMap.put(1, "One");
LOG.info(intKeyMap.get(1)); // Outputs: One
Backing Map Selection
The backing map implementation is automatically chosen based on the type of the source map or can be explicitly specified. For example:
- If the source map is a
TreeMap
, the backing map will also be aTreeMap
. - If no match is found, the default backing map is a
LinkedHashMap
. - Unsupported map types, such as
IdentityHashMap
, will throw anIllegalArgumentException
.
Performance Considerations
- The
CaseInsensitiveString
cache reduces object creation overhead for frequently used keys. - For extremely long keys, caching is bypassed to avoid memory exhaustion.
- Performance is comparable to the backing map implementation used.
Thread Safety and ConcurrentMap Implementation
CaseInsensitiveMap implements ConcurrentMap
and provides all concurrent operations
(putIfAbsent
, replace
, remove(key, value)
, bulk operations, etc.) with
case-insensitive semantics. Thread safety is determined by the backing map implementation:
- Thread-Safe Backing Maps: When backed by concurrent implementations
(
ConcurrentHashMap
,ConcurrentSkipListMap
,ConcurrentNavigableMapNullSafe
, etc.), all operations are fully thread-safe. - Non-Thread-Safe Backing Maps: When backed by non-concurrent implementations
(
LinkedHashMap
,HashMap
,TreeMap
, etc.), concurrent operations work correctly but require external synchronization for thread safety. - String Cache: The case-insensitive string cache is thread-safe and can be safely accessed from multiple threads regardless of the backing map.
Recommendation: For multi-threaded applications, explicitly choose a concurrent backing map implementation to ensure thread safety.
Additional Notes
- String keys longer than 100 characters are not cached by default. This limit can be adjusted using
setMaxCacheLengthString(int)
.
- Author:
- John DeRegnaucourt ([email protected])
Copyright (c) Cedar Software LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
License
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - See Also:
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionclass
Entry implementation that returns a String key rather than a CaseInsensitiveString whenCaseInsensitiveMap.CaseInsensitiveEntry.getKey()
is called.static final class
Wrapper class for String keys to enforce case-insensitive comparison.Nested classes/interfaces inherited from class java.util.AbstractMap
AbstractMap.SimpleEntry<K extends Object,
V extends Object>, AbstractMap.SimpleImmutableEntry<K extends Object, V extends Object> -
Constructor Summary
ConstructorsConstructorDescriptionConstructs an empty CaseInsensitiveMap with a LinkedHashMap as the underlying implementation, providing predictable iteration order.CaseInsensitiveMap
(int initialCapacity) Constructs an empty CaseInsensitiveMap with the specified initial capacity and a LinkedHashMap as the underlying implementation.CaseInsensitiveMap
(int initialCapacity, float loadFactor) Constructs an empty CaseInsensitiveMap with the specified initial capacity and load factor, using a LinkedHashMap as the underlying implementation.CaseInsensitiveMap
(Map<K, V> source) Creates a case-insensitive map initialized with the entries from the specified source map.Creates a CaseInsensitiveMap by copying entries from the specified source map into the specified destination map implementation. -
Method Summary
Modifier and TypeMethodDescriptioncomputeIfAbsent
(K key, Function<? super K, ? extends V> mappingFunction) computeIfPresent
(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) static <K,
V> CaseInsensitiveMap<K, V> Creates a new thread-safe CaseInsensitiveMap backed by a ConcurrentHashMap that can handle null as a key or value.static <K,
V> CaseInsensitiveMap<K, V> concurrent
(int initialCapacity) Creates a new thread-safe CaseInsensitiveMap backed by a ConcurrentHashMap that can handle null as a key or value with the specified initial capacity.static <K,
V> CaseInsensitiveMap<K, V> Creates a new thread-safe sorted CaseInsensitiveMap backed by a ConcurrentSkipListMap.boolean
containsKey
(Object key) boolean
containsMultiKey
(Object... keys) Returns true if this map contains a mapping for the specified multi-dimensional key.Copies all entries from the source map to the destination map, wrapping String keys as needed.determineBackingMap
(Map<K, V> source) Determines the appropriate backing map based on the source map's type.entrySet()
boolean
void
forEach
(long parallelismThreshold, BiConsumer<? super K, ? super V> action) Performs the given action for each entry in this map until all entries have been processed or the action throws an exception.void
forEach
(BiConsumer<? super K, ? super V> action) void
forEachKey
(long parallelismThreshold, Consumer<? super K> action) Performs the given action for each key in this map until all entries have been processed or the action throws an exception.void
forEachValue
(long parallelismThreshold, Consumer<? super V> action) Performs the given action for each value in this map until all entries have been processed or the action throws an exception.getMultiKey
(Object... keys) Retrieves the value associated with the specified multi-dimensional key.Returns the underlying wrapped map instance.keySet()
Returns aSet
view of the keys contained in this map.long
Returns the number of mappings.putIfAbsent
(K key, V value) putMultiKey
(V value, Object... keys) Stores a value with multiple keys, applying case-insensitive handling to String keys.<U> U
reduceKeys
(long parallelismThreshold, Function<? super K, ? extends U> transformer, BiFunction<? super U, ? super U, ? extends U> reducer) Returns the result of accumulating all keys using the given reducer to combine values, or null if none.<U> U
reduceValues
(long parallelismThreshold, Function<? super V, ? extends U> transformer, BiFunction<? super U, ? super U, ? extends U> reducer) Returns the result of accumulating all values using the given reducer to combine values, or null if none.boolean
removeMultiKey
(Object... keys) Removes the mapping for the specified multi-dimensional key.boolean
void
replaceAll
(BiFunction<? super K, ? super V, ? extends V> function) static void
Replaces the current cache used for CaseInsensitiveString instances with a new cache.static void
Allows users to replace the entire registry with a new list of map type entries.<U> U
searchKeys
(long parallelismThreshold, Function<? super K, ? extends U> searchFunction) Returns a non-null result from applying the given search function on each key, or null if none.<U> U
searchValues
(long parallelismThreshold, Function<? super V, ? extends U> searchFunction) Returns a non-null result from applying the given search function on each value, or null if none.static void
setMaxCacheLengthString
(int length) Sets the maximum string length for which CaseInsensitiveString instances will be cached.Methods inherited from class java.util.AbstractMap
clear, clone, containsValue, hashCode, isEmpty, putAll, size, toString, values
Methods inherited from class java.lang.Object
finalize, getClass, notify, notifyAll, wait, wait, wait
Methods inherited from interface java.util.concurrent.ConcurrentMap
getOrDefault
-
Constructor Details
-
CaseInsensitiveMap
public CaseInsensitiveMap()Constructs an empty CaseInsensitiveMap with a LinkedHashMap as the underlying implementation, providing predictable iteration order. -
CaseInsensitiveMap
public CaseInsensitiveMap(int initialCapacity) Constructs an empty CaseInsensitiveMap with the specified initial capacity and a LinkedHashMap as the underlying implementation.- Parameters:
initialCapacity
- the initial capacity- Throws:
IllegalArgumentException
- if the initial capacity is negative
-
CaseInsensitiveMap
public CaseInsensitiveMap(int initialCapacity, float loadFactor) Constructs an empty CaseInsensitiveMap with the specified initial capacity and load factor, using a LinkedHashMap as the underlying implementation.- Parameters:
initialCapacity
- the initial capacityloadFactor
- the load factor- Throws:
IllegalArgumentException
- if the initial capacity is negative or the load factor is negative
-
CaseInsensitiveMap
Creates a CaseInsensitiveMap by copying entries from the specified source map into the specified destination map implementation.- Parameters:
source
- the map containing entries to be copiedmapInstance
- the empty map instance to use as the underlying implementation- Throws:
NullPointerException
- if either map is nullIllegalArgumentException
- if mapInstance is not empty
-
CaseInsensitiveMap
Creates a case-insensitive map initialized with the entries from the specified source map. The created map preserves the characteristics of the source map by using a similar implementation type.Concrete or known map types are matched to their corresponding internal maps (e.g. TreeMap to TreeMap). If no specific match is found, a LinkedHashMap is used by default.
- Parameters:
source
- the map whose mappings are to be placed in this map. Must not be null.- Throws:
NullPointerException
- if the source map is null
-
-
Method Details
-
replaceRegistry
public static void replaceRegistry(List<Map.Entry<Class<?>, Function<Integer, ? extends Map<?, ?>>>> newRegistry) Allows users to replace the entire registry with a new list of map type entries. This should typically be done at startup before any CaseInsensitiveMap instances are created.- Parameters:
newRegistry
- the new list of map type entries- Throws:
NullPointerException
- if newRegistry is null or contains null elementsIllegalArgumentException
- if newRegistry contains duplicate Class types or is incorrectly ordered
-
replaceCache
Replaces the current cache used for CaseInsensitiveString instances with a new cache. This operation is thread-safe due to the volatile nature of the cache field. When replacing the cache: - Existing CaseInsensitiveString instances in maps remain valid - The new cache will begin populating with strings as they are accessed - There may be temporary duplicate CaseInsensitiveString instances during transition- Parameters:
lruCache
- the new LRUCache instance to use for caching CaseInsensitiveString objects- Throws:
NullPointerException
- if the provided cache is null
-
setMaxCacheLengthString
public static void setMaxCacheLengthString(int length) Sets the maximum string length for which CaseInsensitiveString instances will be cached. Strings longer than this length will not be cached but instead create new instances each time they are needed. This helps prevent memory exhaustion from very long strings.- Parameters:
length
- the maximum length of strings to cache. Must be non-negative.- Throws:
IllegalArgumentException
- if length is < 10.
-
concurrent
Creates a new thread-safe CaseInsensitiveMap backed by a ConcurrentHashMap that can handle null as a key or value. This is equivalent tonew CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMapNullSafe<>())
.- Type Parameters:
K
- the type of keys maintained by this mapV
- the type of mapped values- Returns:
- a new thread-safe CaseInsensitiveMap
-
concurrent
Creates a new thread-safe CaseInsensitiveMap backed by a ConcurrentHashMap that can handle null as a key or value with the specified initial capacity. This is equivalent tonew CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMapNullSafe<>(initialCapacity))
.- Type Parameters:
K
- the type of keys maintained by this mapV
- the type of mapped values- Parameters:
initialCapacity
- the initial capacity of the backing ConcurrentHashMap- Returns:
- a new thread-safe CaseInsensitiveMap
- Throws:
IllegalArgumentException
- if the initial capacity is negative
-
concurrentSorted
Creates a new thread-safe sorted CaseInsensitiveMap backed by a ConcurrentSkipListMap. This is equivalent tonew CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentNavigableMapNullSafe<>())
.- Type Parameters:
K
- the type of keys maintained by this mapV
- the type of mapped values- Returns:
- a new thread-safe sorted CaseInsensitiveMap
-
determineBackingMap
Determines the appropriate backing map based on the source map's type.- Parameters:
source
- the source map to copy from- Returns:
- a new Map instance with entries copied from the source
- Throws:
IllegalArgumentException
- if the source map is an IdentityHashMap
-
copy
Copies all entries from the source map to the destination map, wrapping String keys as needed.- Parameters:
source
- the map whose entries are being copieddest
- the destination map- Returns:
- the populated destination map
-
get
String keys are handled case-insensitively.
When backing map is MultiKeyMap, Collections and Arrays are automatically expanded to multi-key operations.
-
containsKey
String keys are handled case-insensitively.
When backing map is MultiKeyMap, Collections and Arrays are automatically expanded to multi-key operations.
- Specified by:
containsKey
in interfaceMap<K,
V> - Overrides:
containsKey
in classAbstractMap<K,
V>
-
put
String keys are stored case-insensitively.
When backing map is MultiKeyMap, Collections and Arrays are automatically expanded to multi-key operations.
-
remove
String keys are handled case-insensitively.
When backing map is MultiKeyMap, Collections and Arrays are automatically expanded to multi-key operations.
-
putMultiKey
Stores a value with multiple keys, applying case-insensitive handling to String keys. This method is only supported when the backing map is a MultiKeyMap.Examples:
CaseInsensitiveMap<String, String> map = new CaseInsensitiveMap<>(Collections.emptyMap(), new MultiKeyMap<>()); // Multi-key operations with case-insensitive String handling map.putMultiKey("Value1", "DEPT", "Engineering"); // String keys converted to case-insensitive map.putMultiKey("Value2", "dept", "Marketing", "West"); // Mixed case handled automatically map.putMultiKey("Value3", 123, "project", "Alpha"); // Mixed String and non-String keys // Retrieval with case-insensitive matching String val1 = map.getMultiKey("dept", "ENGINEERING"); // Returns "Value1" String val2 = map.getMultiKey("DEPT", "marketing", "west"); // Returns "Value2"
- Parameters:
value
- the value to storekeys
- the key components (unlimited number, String keys are handled case-insensitively)- Returns:
- the previous value associated with the key, or null if there was no mapping
- Throws:
IllegalStateException
- if the backing map is not a MultiKeyMap instance
-
getMultiKey
Retrieves the value associated with the specified multi-dimensional key. String keys are matched case-insensitively. This method is only supported when the backing map is a MultiKeyMap.Examples:
// Assumes previous puts: map.putMultiKey("Value1", "Dept", "Engineering"); String val = map.getMultiKey("DEPT", "engineering"); // Returns "Value1" (case-insensitive) String val2 = map.getMultiKey("dept", "ENGINEERING"); // Returns "Value1" (case-insensitive)
- Parameters:
keys
- the key components (String keys are matched case-insensitively)- Returns:
- the value associated with the key, or null if not found
- Throws:
IllegalStateException
- if the backing map is not a MultiKeyMap instance
-
removeMultiKey
Removes the mapping for the specified multi-dimensional key. String keys are matched case-insensitively. This method is only supported when the backing map is a MultiKeyMap.- Parameters:
keys
- the key components (String keys are matched case-insensitively)- Returns:
- the previous value associated with the key, or null if there was no mapping
- Throws:
IllegalStateException
- if the backing map is not a MultiKeyMap instance
-
containsMultiKey
Returns true if this map contains a mapping for the specified multi-dimensional key. String keys are matched case-insensitively. This method is only supported when the backing map is a MultiKeyMap.- Parameters:
keys
- the key components (String keys are matched case-insensitively)- Returns:
- true if a mapping exists for the key
- Throws:
IllegalStateException
- if the backing map is not a MultiKeyMap instance
-
equals
Equality is based on case-insensitive comparison for String keys.
-
getWrappedMap
Returns the underlying wrapped map instance. This map contains the keys in their case-insensitive form (i.e.,CaseInsensitiveMap.CaseInsensitiveString
for String keys).- Returns:
- the wrapped map
-
keySet
Returns aSet
view of the keys contained in this map. The set is backed by the map, so changes to the map are reflected in the set, and vice versa. For String keys, the set contains the original Strings rather than their case-insensitive representations. -
entrySet
Returns a Set view of the entries contained in this map. Each entry returns its key in the original String form (if it was a String). Operations on this set affect the underlying map.
-
computeIfAbsent
For String keys, the mapping is performed in a case-insensitive manner. If the mapping function receives a String key, it will be passed the original String rather than the internal case-insensitive representation.
- Specified by:
computeIfAbsent
in interfaceConcurrentMap<K,
V> - Specified by:
computeIfAbsent
in interfaceMap<K,
V> - See Also:
-
computeIfPresent
For String keys, the mapping is performed in a case-insensitive manner. If the remapping function receives a String key, it will be passed the original String rather than the internal case-insensitive representation.
- Specified by:
computeIfPresent
in interfaceConcurrentMap<K,
V> - Specified by:
computeIfPresent
in interfaceMap<K,
V> - See Also:
-
compute
For String keys, the computation is performed in a case-insensitive manner. If the remapping function receives a String key, it will be passed the original String rather than the internal case-insensitive representation.
-
merge
For String keys, the merge is performed in a case-insensitive manner. The remapping function operates only on values and is not affected by case sensitivity.
-
putIfAbsent
For String keys, the operation is performed in a case-insensitive manner.
- Specified by:
putIfAbsent
in interfaceConcurrentMap<K,
V> - Specified by:
putIfAbsent
in interfaceMap<K,
V> - See Also:
-
remove
For String keys, the removal is performed in a case-insensitive manner.
-
replace
For String keys, the replacement is performed in a case-insensitive manner.
-
replace
For String keys, the replacement is performed in a case-insensitive manner.
-
forEach
For String keys, the action receives the original String key rather than the internal case-insensitive representation.
-
replaceAll
For String keys, the function receives the original String key rather than the internal case-insensitive representation. The replacement is performed in a case-insensitive manner.
- Specified by:
replaceAll
in interfaceConcurrentMap<K,
V> - Specified by:
replaceAll
in interfaceMap<K,
V> - See Also:
-
mappingCount
public long mappingCount()Returns the number of mappings. This method should be used instead ofAbstractMap.size()
because a ConcurrentHashMap may contain more mappings than can be represented as an int. The value returned is an estimate; the actual count may differ if there are concurrent insertions or removals.This method delegates to
ConcurrentHashMap.mappingCount()
when the backing map is a ConcurrentHashMap, otherwise returnsAbstractMap.size()
.- Returns:
- the number of mappings
- Since:
- 3.7.0
-
forEach
Performs the given action for each entry in this map until all entries have been processed or the action throws an exception. Exceptions thrown by the action are relayed to the caller. The iteration may be performed in parallel if the backing map supports it and the parallelismThreshold is met.For String keys, the action receives the original String key rather than the internal case-insensitive representation.
- Parameters:
parallelismThreshold
- the (estimated) number of elements needed for this operation to be executed in parallelaction
- the action to be performed for each entry- Throws:
NullPointerException
- if the specified action is null- Since:
- 3.7.0
-
forEachKey
Performs the given action for each key in this map until all entries have been processed or the action throws an exception.For String keys, the action receives the original String key rather than the internal case-insensitive representation.
- Parameters:
parallelismThreshold
- the (estimated) number of elements needed for this operation to be executed in parallelaction
- the action to be performed for each key- Throws:
NullPointerException
- if the specified action is null- Since:
- 3.7.0
-
forEachValue
Performs the given action for each value in this map until all entries have been processed or the action throws an exception.- Parameters:
parallelismThreshold
- the (estimated) number of elements needed for this operation to be executed in parallelaction
- the action to be performed for each value- Throws:
NullPointerException
- if the specified action is null- Since:
- 3.7.0
-
searchKeys
Returns a non-null result from applying the given search function on each key, or null if none. Upon success, further element processing is suppressed and the results of any other parallel invocations of the search function are ignored.For String keys, the search function receives the original String key rather than the internal case-insensitive representation.
- Type Parameters:
U
- the return type of the search function- Parameters:
parallelismThreshold
- the (estimated) number of elements needed for this operation to be executed in parallelsearchFunction
- a function returning a non-null result on success, else null- Returns:
- a non-null result from applying the given search function on each key, or null if none
- Throws:
NullPointerException
- if the search function is null- Since:
- 3.7.0
-
searchValues
public <U> U searchValues(long parallelismThreshold, Function<? super V, ? extends U> searchFunction) Returns a non-null result from applying the given search function on each value, or null if none.- Type Parameters:
U
- the return type of the search function- Parameters:
parallelismThreshold
- the (estimated) number of elements needed for this operation to be executed in parallelsearchFunction
- a function returning a non-null result on success, else null- Returns:
- a non-null result from applying the given search function on each value, or null if none
- Throws:
NullPointerException
- if the search function is null- Since:
- 3.7.0
-
reduceKeys
public <U> U reduceKeys(long parallelismThreshold, Function<? super K, ? extends U> transformer, BiFunction<? super U, ? super U, ? extends U> reducer) Returns the result of accumulating all keys using the given reducer to combine values, or null if none.For String keys, the transformer and reducer receive the original String key rather than the internal case-insensitive representation.
- Type Parameters:
U
- the return type of the transformer- Parameters:
parallelismThreshold
- the (estimated) number of elements needed for this operation to be executed in paralleltransformer
- a function returning the transformation for an element, or null if there is no transformationreducer
- a commutative associative combining function- Returns:
- the result of accumulating all keys, or null if none
- Throws:
NullPointerException
- if the transformer or reducer is null- Since:
- 3.7.0
-
reduceValues
public <U> U reduceValues(long parallelismThreshold, Function<? super V, ? extends U> transformer, BiFunction<? super U, ? super U, ? extends U> reducer) Returns the result of accumulating all values using the given reducer to combine values, or null if none.- Type Parameters:
U
- the return type of the transformer- Parameters:
parallelismThreshold
- the (estimated) number of elements needed for this operation to be executed in paralleltransformer
- a function returning the transformation for an element, or null if there is no transformationreducer
- a commutative associative combining function- Returns:
- the result of accumulating all values, or null if none
- Throws:
NullPointerException
- if the transformer or reducer is null- Since:
- 3.7.0
-