Saturday, October 23, 2010

Cannot Assume Serializability for Java Map's Nested Classes

A colleague of mine recently ran into an issue that I have not run into myself, but found to be interesting and, in my opinion, worth blogging about here. During some distributed Java development, my colleague observed that a NotSerializableException was encountered when he tried to pass the Set returned from Map's (HashMap in particular in this case) keySet() across the wire. Although I had never run into this myself, it is a commonly described issue online. Online references to this issue include Java HashMap.keySet return value, Bug Report 4501848, Bug Report 4756277, HashMap.Entry Not Serializable?, and Java Core APIs: private nonserializable classes in HashMap and Hashtable.

The following class demonstrates which Map implementations and which nested Map classes are Serializable and which are not for several popular standard Map implementations.

package dustin.examples;

import java.io.Serializable;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

import static java.lang.System.out;

/**
 * This class demonstrates that the nested classes for various types of Java
 * maps are not Serializable.
 */
public class NonSerializableCollectionsInnerClassesDemonstrator
{
   final String NEW_LINE = System.getProperty("line.separator");

   /** Enum describing types of Maps used in this demonstration. */
   private enum MapTypeEnum
   {
      CONCURRENT_HASH
      {
         public Map<Long, String> newSingleEntryMapInstance()
         {
            final Map<Long, String> map = new ConcurrentHashMap<Long, String>();
            map.put(1L, "One");
            return map;
         }
      },
      HASH
      {
         public Map<Long, String> newSingleEntryMapInstance()
         {
            final Map<Long, String> map = new HashMap<Long, String>();
            map.put(2L, "Two");
            return map;
         }
      },
      HASH_TABLE
      {
         public Map<Long, String> newSingleEntryMapInstance()
         {
            final Map<Long, String> map = new Hashtable<Long, String>();
            map.put(3L, "Three");
            return map;
         }
      },
      LINKED_HASH
      {
         public Map<Long, String> newSingleEntryMapInstance()
         {
            final Map<Long, String> map = new LinkedHashMap<Long, String>();
            map.put(4L, "Four");
            return map;
         }
      },
      TREE
      {
         public Map<Long, String> newSingleEntryMapInstance()
         {
            final Map<Long, String> map = new TreeMap<Long, String>();
            map.put(5L, "Five");
            return map;
         }
      },
      WEAK_HASH
      {
         public Map<Long, String> newSingleEntryMapInstance()
         {
            final Map<Long, String> map = new WeakHashMap<Long, String>();
            map.put(6L, "Six");
            return map;
         }
      };

      public abstract Map<Long, String> newSingleEntryMapInstance();
   }

   /**
    * Indicate where the provided class defines a class that implements the
    * java.io.Serializable interface.
    *
    * @param candidateClass Class whose serializable status is desired.
    * @return {@code true} if the provided class is Serializable.
    */
   public static boolean isSerializable(final Object candidateClass)
   {
      return candidateClass instanceof Serializable;
   }

   /**
    * Print (to stdout) a simple message describing if the provided object
    * is serializable or not.
    *
    * @param object Object whose class's status as Serializable or not is to be
    *    printed to stdout.
    */
   public static void printSerializableStatus(final Object object)
   {
      final Class clazz = object.getClass();
      out.println(
         clazz.getName() + " is " + (isSerializable(object) ? "" : "NOT ") + "Serializable.");
   }

   /**
    * Process a provided Map instance to indicate if the Map itself and its
    * nested classes are Serializable.
    *
    * @param header Header to be printed before Serializable results are printed.
    * @param map Map to be evaluated for Serializability or it and its nested classes.
    */
   private void processSpecificMapInstance(
      final String header, final Map<Long, String> map)
   {
      out.println(NEW_LINE + "===== " + header + " =====");
      printSerializableStatus(map);
      final Set<Long> mapKeySet = map.keySet();
      printSerializableStatus(mapKeySet);
      final Iterator<Long> mapKeySetIterator = mapKeySet.iterator();
      printSerializableStatus(mapKeySetIterator);
      final Collection<String> mapValues = map.values();
      printSerializableStatus(mapValues);
      for (final Map.Entry<Long, String> mapEntrySet : map.entrySet())
      {
         printSerializableStatus(mapEntrySet);
      }
   }

   /**
    * Method for explicitly creating EnumMap to check it and its nested classes
    * for Serializability.
    */
   private void processAndDemonstrateEnumMap()
   {
      out.println(NEW_LINE + "===== ENUMMAP =====");
      final Map<MapTypeEnum, String> map = new EnumMap(MapTypeEnum.class);
      map.put(MapTypeEnum.HASH, "HashMap");
      printSerializableStatus(map);
      final Set<MapTypeEnum> mapKeySet = map.keySet();
      printSerializableStatus(mapKeySet);
      final Iterator<MapTypeEnum> mapKeySetIterator = mapKeySet.iterator();
      printSerializableStatus(mapKeySetIterator);
      final Collection<String> mapValues = map.values();
      printSerializableStatus(mapValues);
      for (final Map.Entry<MapTypeEnum, String> mapEntrySet : map.entrySet())
      {
         printSerializableStatus(mapEntrySet);
      }
   }

   /**
    * Demonstrate Serializability status of different Map implementations and
    * the Serializability of those Map instance's nested classes.
    */
   private void demonstrateMapNestedClassesSerializability()
   {
      final Map<Long, String> hashMap = MapTypeEnum.HASH.newSingleEntryMapInstance();
      processSpecificMapInstance("HASHMAP", hashMap);
      final Map<Long, String> linkedHashMap = MapTypeEnum.LINKED_HASH.newSingleEntryMapInstance();
      processSpecificMapInstance("LINKEDHASHMAP", linkedHashMap);
      final Map<Long, String> concurrentHashMap = MapTypeEnum.CONCURRENT_HASH.newSingleEntryMapInstance();
      processSpecificMapInstance("CONCURRENTHASHMAP", concurrentHashMap);
      final Map<Long, String> weakHashMap = MapTypeEnum.WEAK_HASH.newSingleEntryMapInstance();
      processSpecificMapInstance("WEAKHASHMAP", weakHashMap);
      final Map<Long, String> treeMap = MapTypeEnum.TREE.newSingleEntryMapInstance();
      processSpecificMapInstance("TREEMAP", treeMap);
      final Map<Long, String> hashTable = MapTypeEnum.HASH_TABLE.newSingleEntryMapInstance();
      processSpecificMapInstance("HASHTABLE", hashTable);
      processAndDemonstrateEnumMap();
   }

   /**
    * Main executable function to run the demonstrations.
    *
    * @param arguments Command-line arguments; none expected.
    */
   public static void main(final String[] arguments)
   {
      final NonSerializableCollectionsInnerClassesDemonstrator instance =
         new NonSerializableCollectionsInnerClassesDemonstrator();
      instance.demonstrateMapNestedClassesSerializability();
   }
}

The above class runs through several popular Map implementations (EnumMap, HashMap, LinkedHashMap, TreeMap, Hashtable, WeakHashMap, ConcurrentHashMap) and prints out whether each Map implementation and its nested classes are Serializable.  The output it generates is shown next.

===== HASHMAP =====
java.util.HashMap is Serializable.
java.util.HashMap$KeySet is NOT Serializable.
java.util.HashMap$KeyIterator is NOT Serializable.
java.util.HashMap$Values is NOT Serializable.
java.util.HashMap$Entry is NOT Serializable.

===== LINKEDHASHMAP =====
java.util.LinkedHashMap is Serializable.
java.util.HashMap$KeySet is NOT Serializable.
java.util.LinkedHashMap$KeyIterator is NOT Serializable.
java.util.HashMap$Values is NOT Serializable.
java.util.LinkedHashMap$Entry is NOT Serializable.

===== CONCURRENTHASHMAP =====
java.util.concurrent.ConcurrentHashMap is Serializable.
java.util.concurrent.ConcurrentHashMap$KeySet is NOT Serializable.
java.util.concurrent.ConcurrentHashMap$KeyIterator is NOT Serializable.
java.util.concurrent.ConcurrentHashMap$Values is NOT Serializable.
java.util.concurrent.ConcurrentHashMap$WriteThroughEntry is Serializable.

===== WEAKHASHMAP =====
java.util.WeakHashMap is NOT Serializable.
java.util.WeakHashMap$KeySet is NOT Serializable.
java.util.WeakHashMap$KeyIterator is NOT Serializable.
java.util.WeakHashMap$Values is NOT Serializable.
java.util.WeakHashMap$Entry is NOT Serializable.

===== TREEMAP =====
java.util.TreeMap is Serializable.
java.util.TreeMap$KeySet is NOT Serializable.
java.util.TreeMap$KeyIterator is NOT Serializable.
java.util.TreeMap$Values is NOT Serializable.
java.util.TreeMap$Entry is NOT Serializable.

===== HASHTABLE =====
java.util.Hashtable is Serializable.
java.util.Collections$SynchronizedSet is Serializable.
java.util.Hashtable$Enumerator is NOT Serializable.
java.util.Collections$SynchronizedCollection is Serializable.
java.util.Hashtable$Entry is NOT Serializable.

===== ENUMMAP =====
java.util.EnumMap is Serializable.
java.util.EnumMap$KeySet is NOT Serializable.
java.util.EnumMap$KeyIterator is NOT Serializable.
java.util.EnumMap$Values is NOT Serializable.
java.util.EnumMap$EntryIterator is NOT Serializable.

The output shown above leads to several interesting observations. First, and most importantly from this post's perspective, is the fact that most (all but WeakHashMap) of the Map implementations are themselves Serializable, but most of them (all but Hashtable) have nested classes (for keyset, values, and entryset) that are NOT Serializable.

There are several approaches that can be used if the data from one of these classes nested within Map need to be distributed. The previously referenced bug reports provide an obvious "work around." One can copy the returned key set into its own new (and Serializable) Set:
Copy the values of the Set returned by keySet(), entrySet() or values() into a new HashSet or TreeSet object: Set obj = new HashSet(myMap.keySet());
This blog post has attempted to demonstrate that Serializable cannot be taken for granted. This is particularly true when dealing with nested classes in Map implementations.

4 comments:

Cluggas said...

A very informative post for a Java novice. Thanks.

In the post you say that Hashtable does NOT have nested implementations of ketset and values. Does this mean that Hashtable does not suffer from this serialisation problem?

Dustin said...

Cluggas,

Although Hashtable has neither keyset nor values, its nested Enumerator and Entry are similarly non-Serializable, meaning one needs to be careful with passing either the Enumerator or the Entry to remote objects. That being said, its SynchronizedSet and SynchronizedCollection appear to be Serializable.

Dustin

Brandon said...

Is it just me or does it seem crazy that an implementation (especially one provided by Java itself) would implement Serializable if it can't actually be serialized? Am I missing something? I thought that was the whole point of needing to explicitly implement the interface - it puts some responsibility on the developer to ensure their class truly is serializable.

gufalcon1 said...

Yes, you are missing something. The fact that a data-structure returned by a map, when queried in a very specific way, is not serializable does not mean that the map as a whole is not...
The interface just ensures that the map itself is serializable (save as a whole, read as a whole) and not all the data-structures involved in all the methods the map exposes.