Search This Blog

Tuesday 12 November 2013

Enums as the key in Map

In the previous post we saw how EnumSet is an optimized collection built for use with Enums. While I genuinely struggled to come up with scenarios where I could need a set of enums, I have often come across and used Enums as keys in my maps. Java has come up with an optimized collection for the above use case too - EnumMap.
The maps that we use are hash based implementations. Adding a key value pair involves
  1. computing a hash for the key,
  2. locating a bucket for the hash and 
  3. then adding the key-value pair to the bucket.
When the key for the map is an Enum - we can only have a finite number of keys. Plus as we know all enums have an ordinal property - or a zero based index associated with it.
These two features of an enum makes it possible to implement an array based fixed size map for it.
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{
    /**
     * The <tt>Class</tt> object for the enum type of all the keys of this map.
     */
    private final Class<K> keyType;

    /**
     * All of the values comprising K.  (Cached for performance.)
     */
    private transient K[] keyUniverse;

    /**
     * Array representation of this map.  The ith element is the value
     * to which universe[i] is currently mapped, or null if it isn't
     * mapped to anything, or NULL if it's mapped to null.
     */
    private transient Object[] vals;

        //remaining code...
}
As seen here, the EnumMap includes two fixed size arrays - an array of all possible Enum keys and an array of enum values.
Consider the contains method for EnumMap:
public boolean containsKey(Object key) {
  return isValidKey(key) && vals[((Enum)key).ordinal()] != null;
}
For any key, its ordinal acts as an index into the value array. If there is a value at this location it is returned, else null is returned. Thats it. No hashing, no buckets. This kind of implementation is what makes it both fast and compact.
public boolean containsValue(Object value) {
      value = maskNull(value);

      for (Object val : vals)
         if (value.equals(val))
            return true;

      return false;
   }
The containsValue method simply runs a single for loop over the array looking for the passed object. In the hash based implementations, we have a double for loop - one for the buckets and the other for the entries inside the bucket to locate the value.
public V get(Object key) {
   return (isValidKey(key) ?
           unmaskNull(vals[((Enum)key).ordinal()]) : null);
}
public V put(K key, V value) {
   typeCheck(key);

   int index = ((Enum)key).ordinal();
   Object oldValue = vals[index];
   vals[index] = maskNull(value);
   if (oldValue == null)
       size++;
   return unmaskNull(oldValue);
}
The get and put methods as seen simply access the location in our value array corresponding to ordinal associated with the passed key. Fast. And efficient. EnumMaps are something that we should use when working with enums as keys for our maps.
public static void main(String[] args) {

   EnumMap<Rating, String> enumMap = new EnumMap<Rating, String>(Rating.class);
   System.out.println("is BAD key present ? " + enumMap.containsKey(Rating.BAD));
   enumMap.put(Rating.BAD, Rating.BAD.name());
   System.out.println("Is BAD key present ? " + enumMap.containsKey(Rating.BAD));
   System.out.println("Is object for  BAD key present ? " + enumMap.containsValue(Rating.BAD.name()));
   System.out.println("Is object for  BAD key present ? " + enumMap.get(Rating.BAD));

}
The output is :
is BAD key present ? false
Is BAD key present ? true
Is object for  BAD key present ? true
Is object for  BAD key present ? BAD

No comments:

Post a Comment