10

I want to implement a case insensitive hash map. This question by itself isn't new, but I wanted to add extra functionality and don't know what general direction to take. I want the client to be able to do something like this:

boolean preserve_case = true;
Map<String, MyClass> maplet = new CaseInsensitiveHashMap<MyClass>(preserve_case); // If the client enters true at construction, then the put, get, and remove methods should still be case insensitive, but the entry and key sets should preserve the case that the client used when calling put.

maplet.put("FoO", my_class);

MyClass bar = maplet.get("foo"); // Should return a reference to my_class

Set<Entry<String, MyClass>> case_sensitive_set = maplet.entrySet(); // Since the client input true to preserve order, this entry set should be ["FoO"=my_class.toString()]

I can handle most of this pretty well; I simply keep a HashMap on the backend. When a client puts anything in, I uppercase the key before adding it to the map.

I'm just having a hard time writing the keySet() and entrySet() methods. I want the returned entry set and key set to be backed by the map, as is the standard with Java maps.

However, the only way I can think of handling this is to create a second backing data structure, something like a preserved_case_map, which contains the input.toUpperCase() => input as key value pairs. When the client calls for the entrySet() (or keySet()), I can construct the returned entry set by looping through the preserved_case_map. The problem here is that the returned entry set will not be modified if I make changes to the HashMap, unless I'm misunderstanding something...

Let me know if this makes sense, or if I'm convoluting a simple situation.

AusCBloke
  • 18,014
  • 6
  • 40
  • 44
Sal
  • 3,179
  • 7
  • 31
  • 41
  • 1
    Its not clear if you want keySet() to return all keys that were inserted (FoO, foo, fOo) or just the last one that remains in the map. – Andrejs Mar 21 '12 at 23:00
  • My mistake. I want the keySet() to return [FoO], since the client entered true in the constructor and entered the key value pair as "FoO", my_class. – Sal Mar 21 '12 at 23:16
  • Using the TreeMap as the backing store for my class? Of course. However, I don't see how using a TreeMap would help... Could you elaborate? – Sal Mar 24 '12 at 02:27
  • See my other [answer here](http://stackoverflow.com/a/9851260/1180621) on how to solve this using TreeMap – Andrejs Mar 24 '12 at 11:13
  • The CaseInsenstiveMap here: https://github.com/jdereg/java-util does exactly what you want - it implements the keySet() and entrySet() methods which retain the case. It is very tricky business to get the implementation of .keySet() and .entrySet() exactly correct. There is an extensive test case in the same java-util library that you can use to test your implementation. – John DeRegnaucourt Mar 04 '16 at 08:16

6 Answers6

27

You could use a TreeMap with case insensitive comparator. The TreeMap will use the comparator to compare keys in a case-insensitive way:

    Map<String, Integer> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

    map.put("Foo", 1);
    map.put("fOo", 2);

    System.out.println(map.get("foo")); // prints 2
    System.out.println(map.keySet()); // prints [Foo]
Andrejs
  • 26,885
  • 12
  • 107
  • 96
  • That's exactly what I'm looking for, Andrejs. Why can't the HashMap take in a comparator during construction? It may not make sense in the sense that a comparator can't preserve order in a HashMap, but it can use the comparator for the get and put methods and handle it just like the TreeMap as you've mentioned. – Sal Mar 27 '12 at 15:32
  • 2
    Great! Its because HashMap relies on key.hashCode() and its items don't have to be Comparable. TreeMap keys must be comparable because it does item comparisons internally. The Comparator is configurable though. – Andrejs Mar 27 '12 at 15:46
  • Very good, the comparator is configurable; is there a way to tie that comparator to my CaseInsensitiveHashMap? I really want to preserve the speed of hash lookups, so ideally, I'd like to create a simple class, as mentioned in the question. Let me know if you think of another solution, but I think the best possible answer is creating a class as mentioned in the initial question, then creating custom Entries, EntrySets, KeySet, Iterators, etc. This way, one can ensure that modifications to the entry set are backed by the original map, and vice versa. – Sal Mar 28 '12 at 00:29
5

To simplify wrapping a map you can use the ForwardingMap from Googles Guava libraries but thats optional.

Before putting/getting something from the backing map, wrap your String key in a class that overrides hashCode()/equals() and use the wrapper as the key in the map. Something like:

class KeyWrapper { 
    int hashCode() { 
        return actualStringKey.toUpperCase().hashCode()
    }
    boolean equals(Object o) {...} // compare case-insensitive
}

If you override keySet() you can create a new set and fill it with actualStringKeys.

Andrejs
  • 26,885
  • 12
  • 107
  • 96
  • This looks like it could definitely be beneficial, but it would involve importing a big library into my code base... – Sal Mar 21 '12 at 22:49
4

You could use CaseInsensitiveMap from Apache's Commons Collections.

http://commons.apache.org/proper/commons-collections/

SANN3
  • 9,459
  • 6
  • 61
  • 97
3

Instead of just storing the value, store a KeyValuePair of both the input key (which still has the user-supplied case) and the value, and use the upper-cased key as the key.

When you pull out the value, extract it from the KeyValuePair before returning it. You can also retrieve the case of the original key by doing the same lookup with the uppercased key, and then pulling out the original key.

xtopher
  • 406
  • 3
  • 9
2

I do something along these lines for a mapped Cache that I have written. Naming changed here to reflect your case-insensitivity instead of my caching, but the concepts are the same.

This would give you an UncasedMap class that could be re-used to hold anything, but would be limited to using Strings as keys.

// Map that uses case insensitive strings as keys to any kind of element
public class UncasedMap<V> extends HashMap<String, V>
{
    private static final long serialVersionUID = 1L;
    private Map<String, MappedObject> backingMap = new HashMap<String, MappedObject>();

    @Override
    public V put(String key, V value)
    {
        MappedObject currentVal = backingMap.get( key.toUpperCase() );
        backingMap.put( key.toUpperCase(), new MappedObject(key.toUpperCase(), value) );
        return currentVal.getElement();

    }
    @Override
    public V get(Object key)
    {
        return backingMap.get(key.toString().toUpperCase()).getElement();
    }

    // a private inner class of my case insensitive map, to wrap the callers element
    private class MappedObject
    {
        private String key;
        private String orig_key;
        private V mapElement;

        public MappedObject(String keyx, V elem)
        {
            this.orig_key = keyx;
            this.key = keyx.toUpperCase();
            this.mapElement = elem;
        }

        public V getElement()
        {
            return this.mapElement;
        }
    }
}

Implementing keySet() and emptySet() would be more complicated, but done along the same lines.

Stephen P
  • 14,422
  • 2
  • 43
  • 67
  • 1
    I think I see where this is going, but you're right, implementing keySet() and emptySet() so that they back the backingMap would be a little tricky... In fact, I'm not seeing how it could be done. – Sal Mar 21 '12 at 22:51
1

One way is to inherit from HashMap<String, V>, then call methods in the base class after calling toUpperCase() on the key(s).

However, it is much easier to just override the hashCode() method in MyClass (or whatever classes you'll use), and then use the standard HashMap<K, V> class, without implementing your own. For example:

public int hashCode() {
    return this.toString().toUperCase().hashCode();
}

If you put that in a base class and then have your classes inherit from it, then you will have a DRY and very simple solution to your problem.

Diego
  • 18,035
  • 5
  • 62
  • 66
  • This is a great idea, although I wanted to allow the client to input an arbitrary class. However, if I create a short containing class, can I override the put method to do a similar treatment and store the key with a hashCode that may look like key.toUpperCase().hashCode()? – Sal Mar 21 '12 at 22:16
  • To be more explicit, can I override the hash lookup on my backing data structure, so that I can send the incoming key toUpperCase()? – Sal Mar 21 '12 at 22:20
  • But since the client (programmer using the case insensitive map) can provide any class for `MyClass` you'd have to require *them* to override hashCode and equals. – Stephen P Mar 21 '12 at 22:20
  • Overriding hashCode in MyClass won't help since String is the key of the map. And String is a final class. – Andrejs Mar 21 '12 at 22:54