5
\$\begingroup\$

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<String, String> {

    private final ConcurrentHashMap<String, Entry<String, String>> data = new ConcurrentHashMap<>();


    public Context() {
        this(null);
    }

    public Context(@Nullable Context context) {
        this((Map<String, String>) context);
    }

    public Context(@Nullable Map<String, String> 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<String> vals = values();
        return vals.contains(String.valueOf(value));
    }

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

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

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

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

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

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

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

    @Nonnull
    @Override
    public Set<Entry<String, String>> entrySet() {
        return new HashSet<>(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);
    }
}
\$\endgroup\$
0

1 Answer 1

11
\$\begingroup\$

You can't do that with HashMap. Use a TreeMap instead:

new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER)

For highly concurrent version, use ConcurrentSkipListMap:

new ConcurrentSkipListMap<String, String>(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"))
\$\endgroup\$
1
  • \$\begingroup\$ I saw this solution with the TreeMap mentioned elsewhere, but I did not know how to make it concurrent. I've just tested it -- works like a charm! Simple and nice, thanks a lot! \$\endgroup\$
    – Oleg Sklyar
    Nov 25, 2017 at 8:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.