Thứ Bảy, 23 tháng 10, 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.

Không có nhận xét nào:

Đăng nhận xét