6
\$\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);
 }
}
asked Nov 25, 2017 at 0:55
\$\endgroup\$
0

1 Answer 1

12
\$\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"))
answered Nov 25, 2017 at 1:20
\$\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\$ Commented Nov 25, 2017 at 8:20

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.