Class CaseInsensitiveMap<K,V>

java.lang.Object
java.util.AbstractMap<K,V>
com.cedarsoftware.util.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>

public class CaseInsensitiveMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>
A Map implementation that provides case-insensitive key comparison for 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:

Choose your backing map implementation based on your concurrency requirements.

Key Features

  • Case-Insensitive String Keys: String keys are internally stored as CaseInsensitiveString 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(), and forEach(), with case-insensitive handling of String keys.
  • Concurrent Operations: Implements ConcurrentMap interface with full support for concurrent operations including putIfAbsent(), 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:

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

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:
  • 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 capacity
      loadFactor - the load factor
      Throws:
      IllegalArgumentException - if the initial capacity is negative or the load factor is negative
    • CaseInsensitiveMap

      public CaseInsensitiveMap(Map<K,V> source, Map<K,V> mapInstance)
      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 copied
      mapInstance - the empty map instance to use as the underlying implementation
      Throws:
      NullPointerException - if either map is null
      IllegalArgumentException - if mapInstance is not empty
    • CaseInsensitiveMap

      public CaseInsensitiveMap(Map<K,V> source)
      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 elements
      IllegalArgumentException - if newRegistry contains duplicate Class types or is incorrectly ordered
    • replaceCache

      public static void replaceCache(LRUCache<String,CaseInsensitiveMap.CaseInsensitiveString> lruCache)
      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

      public static <K, V> CaseInsensitiveMap<K,V> concurrent()
      Creates a new thread-safe CaseInsensitiveMap backed by a ConcurrentHashMap that can handle null as a key or value. This is equivalent to new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMapNullSafe<>()).
      Type Parameters:
      K - the type of keys maintained by this map
      V - the type of mapped values
      Returns:
      a new thread-safe CaseInsensitiveMap
    • concurrent

      public 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. This is equivalent to new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentHashMapNullSafe<>(initialCapacity)).
      Type Parameters:
      K - the type of keys maintained by this map
      V - 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

      public static <K, V> CaseInsensitiveMap<K,V> concurrentSorted()
      Creates a new thread-safe sorted CaseInsensitiveMap backed by a ConcurrentSkipListMap. This is equivalent to new CaseInsensitiveMap<>(Collections.emptyMap(), new ConcurrentNavigableMapNullSafe<>()).
      Type Parameters:
      K - the type of keys maintained by this map
      V - the type of mapped values
      Returns:
      a new thread-safe sorted CaseInsensitiveMap
    • determineBackingMap

      protected Map<K,V> determineBackingMap(Map<K,V> source)
      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

      protected Map<K,V> copy(Map<K,V> source, Map<K,V> dest)
      Copies all entries from the source map to the destination map, wrapping String keys as needed.
      Parameters:
      source - the map whose entries are being copied
      dest - the destination map
      Returns:
      the populated destination map
    • get

      public V get(Object key)

      String keys are handled case-insensitively.

      When backing map is MultiKeyMap, Collections and Arrays are automatically expanded to multi-key operations.

      Specified by:
      get in interface Map<K,V>
      Overrides:
      get in class AbstractMap<K,V>
    • containsKey

      public boolean containsKey(Object key)

      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 interface Map<K,V>
      Overrides:
      containsKey in class AbstractMap<K,V>
    • put

      public V put(K key, V value)

      String keys are stored case-insensitively.

      When backing map is MultiKeyMap, Collections and Arrays are automatically expanded to multi-key operations.

      Specified by:
      put in interface Map<K,V>
      Overrides:
      put in class AbstractMap<K,V>
    • remove

      public V remove(Object key)

      String keys are handled case-insensitively.

      When backing map is MultiKeyMap, Collections and Arrays are automatically expanded to multi-key operations.

      Specified by:
      remove in interface Map<K,V>
      Overrides:
      remove in class AbstractMap<K,V>
    • putMultiKey

      public V putMultiKey(V value, Object... keys)
      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 store
      keys - 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

      public V getMultiKey(Object... keys)
      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

      public V removeMultiKey(Object... keys)
      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

      public boolean containsMultiKey(Object... keys)
      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

      public boolean equals(Object other)

      Equality is based on case-insensitive comparison for String keys.

      Specified by:
      equals in interface Map<K,V>
      Overrides:
      equals in class AbstractMap<K,V>
    • getWrappedMap

      public Map<K,V> 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

      public Set<K> keySet()
      Returns a Set 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.
      Specified by:
      keySet in interface Map<K,V>
      Overrides:
      keySet in class AbstractMap<K,V>
      Returns:
      a set view of the keys contained in this map
    • entrySet

      public Set<Map.Entry<K,V>> 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.

      Specified by:
      entrySet in interface Map<K,V>
      Specified by:
      entrySet in class AbstractMap<K,V>
    • computeIfAbsent

      public V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)

      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 interface ConcurrentMap<K,V>
      Specified by:
      computeIfAbsent in interface Map<K,V>
      See Also:
    • computeIfPresent

      public V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)

      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 interface ConcurrentMap<K,V>
      Specified by:
      computeIfPresent in interface Map<K,V>
      See Also:
    • compute

      public V compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)

      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.

      Specified by:
      compute in interface ConcurrentMap<K,V>
      Specified by:
      compute in interface Map<K,V>
      See Also:
    • merge

      public V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)

      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.

      Specified by:
      merge in interface ConcurrentMap<K,V>
      Specified by:
      merge in interface Map<K,V>
      See Also:
    • putIfAbsent

      public V putIfAbsent(K key, V value)

      For String keys, the operation is performed in a case-insensitive manner.

      Specified by:
      putIfAbsent in interface ConcurrentMap<K,V>
      Specified by:
      putIfAbsent in interface Map<K,V>
      See Also:
    • remove

      public boolean remove(Object key, Object value)

      For String keys, the removal is performed in a case-insensitive manner.

      Specified by:
      remove in interface ConcurrentMap<K,V>
      Specified by:
      remove in interface Map<K,V>
      See Also:
    • replace

      public boolean replace(K key, V oldValue, V newValue)

      For String keys, the replacement is performed in a case-insensitive manner.

      Specified by:
      replace in interface ConcurrentMap<K,V>
      Specified by:
      replace in interface Map<K,V>
      See Also:
    • replace

      public V replace(K key, V value)

      For String keys, the replacement is performed in a case-insensitive manner.

      Specified by:
      replace in interface ConcurrentMap<K,V>
      Specified by:
      replace in interface Map<K,V>
      See Also:
    • forEach

      public void forEach(BiConsumer<? super K,? super V> action)

      For String keys, the action receives the original String key rather than the internal case-insensitive representation.

      Specified by:
      forEach in interface ConcurrentMap<K,V>
      Specified by:
      forEach in interface Map<K,V>
      See Also:
    • replaceAll

      public void replaceAll(BiFunction<? super K,? super V,? extends V> function)

      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 interface ConcurrentMap<K,V>
      Specified by:
      replaceAll in interface Map<K,V>
      See Also:
    • mappingCount

      public long mappingCount()
      Returns the number of mappings. This method should be used instead of AbstractMap.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 returns AbstractMap.size().

      Returns:
      the number of mappings
      Since:
      3.7.0
    • forEach

      public 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. 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 parallel
      action - the action to be performed for each entry
      Throws:
      NullPointerException - if the specified action is null
      Since:
      3.7.0
    • forEachKey

      public 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.

      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 parallel
      action - the action to be performed for each key
      Throws:
      NullPointerException - if the specified action is null
      Since:
      3.7.0
    • forEachValue

      public 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.
      Parameters:
      parallelismThreshold - the (estimated) number of elements needed for this operation to be executed in parallel
      action - the action to be performed for each value
      Throws:
      NullPointerException - if the specified action is null
      Since:
      3.7.0
    • searchKeys

      public <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. 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 parallel
      searchFunction - 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 parallel
      searchFunction - 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 parallel
      transformer - a function returning the transformation for an element, or null if there is no transformation
      reducer - 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 parallel
      transformer - a function returning the transformation for an element, or null if there is no transformation
      reducer - 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