Map with case insensitive get, remove and containsKey I am looking for a Map implementation (with String keys and any generic type for values) that would support getting, removing and checking the existence of keys in a case insensitive manner while preserving the original keys for the extraction with keySet() or entrySet(). So running put("Some Text", object) I want to be able to retrieve/remove/test object via get("sOmE tExt"), remove("some text"), containsKey("Some TeXT"), yet running keySet() I want to get {"Some Text"}.

The map will be used in a highly concurrent context, but if you have really great ideas based around a standard HashMap please suggest. The implementation should have no external dependencies, that is use only standard Java.

So far I came up with this fairly straightforward implementation. However, I am not sure what could be potential deficiencies of this implementation in a concurrent setting. So far I can see one: keySet, entrySet and values are no longer views, but one should not iterate over keys extracting values by key in a concurrent setting anyway.

Any other issues with this implementation or can you suggest a better one?

public class Context implements Map&lt;String, String&gt; {

    private final ConcurrentHashMap&lt;String, Entry&lt;String, String&gt;&gt; data = new ConcurrentHashMap&lt;&gt;();


    public Context() {
        this(null);
    }

    public Context(@Nullable Context context) {
        this((Map&lt;String, String&gt;) context);
    }

    public Context(@Nullable Map&lt;String, String&gt; context) {
        if (context != null) {
            putAll(context);
        }
    }

    @Override
    public int size() {
        return data.size();
    }

    @Override
    public boolean isEmpty() {
        return data.isEmpty();
    }

    @Override
    public boolean containsKey(@Nonnull Object key) {
        return data.containsKey(String.valueOf(key).toLowerCase());
    }

    @Override
    public boolean containsValue(@Nonnull Object value) {
        Collection&lt;String&gt; vals = values();
        return vals.contains(String.valueOf(value));
    }

    @Override
    public String get(@Nonnull Object key) {
        Entry&lt;String, String&gt; val = data.get(String.valueOf(key).toLowerCase());
        return val != null ? val.getValue() : null;
    }

    @Override
    public String put(@Nonnull String key, @Nonnull String value) {
        Entry&lt;String, String&gt; val = data.put(key.toLowerCase(), new SimpleEntry&lt;&gt;(key, value));
        return val != null ? val.getValue() : null;
    }

    @Override
    public String remove(@Nonnull Object key) {
        Entry&lt;String, String&gt; val = data.remove(String.valueOf(key).toLowerCase());
        return val != null ? val.getValue() : null;
    }

    @Override
    public void putAll(@Nonnull Map&lt;? extends String, ? extends String&gt; map) {
        Map&lt;String, Entry&lt;String, String&gt;&gt; transformed = new HashMap&lt;&gt;();
        for (Entry&lt;? extends String, ? extends String&gt; entry: map.entrySet()) {
            transformed.put(entry.getKey().toLowerCase(), new SimpleEntry&lt;&gt;(entry.getKey(), entry.getValue()));
        }
        data.putAll(transformed);
    }

    @Override
    public void clear() {
        data.clear();
    }

    @Nonnull
    @Override
    public Set&lt;String&gt; keySet() {
        return data.values().stream().map(Entry::getKey).collect(Collectors.toSet());
    }

    @Nonnull
    @Override
    public Collection&lt;String&gt; values() {
        return data.values().stream().map(Entry::getValue).collect(Collectors.toList());
    }

    @Nonnull
    @Override
    public Set&lt;Entry&lt;String, String&gt;&gt; entrySet() {
        return new HashSet&lt;&gt;(data.values());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Context context = (Context) o;
        return Objects.equals(data, context.data);
    }

    @Override
    public int hashCode() {
        return Objects.hash(data);
    }
}
You can't do that with HashMap. Use a TreeMap instead:

new TreeMap&lt;String, String&gt;(String.CASE_INSENSITIVE_ORDER)


For highly concurrent version, use ConcurrentSkipListMap:

new ConcurrentSkipListMap&lt;String, String&gt;(String.CASE_INSENSITIVE_ORDER)


Since you don't care about actual ordering, the String.CASE_INSENSITIVE_ORDER is fine for this. If locale-sensitive ordering is required, you need to supply a Collator instead, e.g.

Collator.getInstance(Locale.forLanguageTag("es-ES"))