19
votes

I am intending to serialize and deserialize a hashmap whose key is a string.

From Josh Bloch's Effective Java, I understand the following. P.222

For example, consider the case of a hash table. The physical representation is a sequence of hash buckets containing key-value entries. Which bucket an entry is placed in is a function of the hash code of the key, which is not, in general guaranteed to be the same from JVM implementation to JVM implementation. In fact, it isn't even guaranteed to be the same from run to run on the same JVM implementation. Therefore accepting the default serialized form for a hash table would constitute a serious bug. Serializing and deserializing the hash table could yield an object whose invariants were seriously corrupt.

My questions are: 1) In general, would overriding equals and hashcode of the key class of the map resolve this issue and the map can be correctly restored?

2) If my key is a String and the String class is already overriding the hashCode() method, would I still have problem described above. (I am seeing a bug which makes me think this is probably still a problem even though the key is String with overriding hashCode.)

3)Previously, I got around this issue by serializing an array of entries (key, value) and when deserializing I would reconstruct the map. I am wondering if there is a better approach.

4) If the answers to question 1 and 2 are that it still can't be guaranteed, could someone explain why? If the hashCodes are the same would they go to the same buckets across JVMs?

Thanks, Grace

7
If you are attempting to serialize a Map you need to make sure that your entire object tree is Serializable, including keys, values, and the Map implementation. - cs80
p. 299 in the 2nd edition - tiktak

7 Answers

23
votes

The serialization form of java.util.HashMap doesn't serialize the buckets themselves, and the hash code is not part of the persisted state. From the javadocs:

Serial Data: The capacity of the HashMap (the length of the bucket array) is emitted (int), followed by the size of the HashMap (the number of key-value mappings), followed by the key (Object) and value (Object) for each key-value mapping represented by the HashMap The key-value mappings are emitted in the order that they are returned by entrySet().iterator().

from http://java.sun.com/j2se/1.5.0/docs/api/serialized-form.html#java.util.HashMap

The persisted state basically comprises the keys and values and some housekeeping. When deserialized, the hashmap is completely rebuilt; the keys are rehashed and placed in appropriate buckets.

So, adding String keys should work just fine. I would guess your bug lies elsewhere.

EDIT: Here's a junit 4 test case that serializes and deserializes a map, and minics VMs changing hashcodes. The test passes, despite the hashcodes being different after deserialization.

import org.junit.Assert;
import org.junit.Test;

import java.io.*;
import java.util.HashMap;

public class HashMapTest
{
    @Test
    public void testHashMapSerialization() throws IOException, ClassNotFoundException
    {
        HashMap map = new HashMap();
        map.put(new Key("abc"), 1);
        map.put(new Key("def"), 2);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(map);
        objOut.close();
        Key.xor = 0x7555AAAA; // make the hashcodes different
        ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()));
        HashMap actual = (HashMap) objIn.readObject();
        // now try to get a value
        Assert.assertEquals(2, actual.get(new Key("def")));
    }

    static class Key implements Serializable
    {
        private String  keyString;
        static int xor = 0;

        Key(String keyString)
        {
            this.keyString = keyString;
        }

        @Override
        public int hashCode()
        {
            return keyString.hashCode()^xor;
        }

        @Override
        public boolean equals(Object obj)
        {
            Key otherKey = (Key) obj;
            return keyString.equals(otherKey.keyString);
        }
    }

}
6
votes

I'm 99% sure that the JVM implementation of HashMap and HashSet handle this issue. They have a custom serialization and deserialization handler. I don't have Bloch's book in front of me now, but I believe he is explaining the challange, not saying that you can't reliably serialize a java.util.HashMap in practice.

6
votes

To serialize a hashmap:

I have tried this and used in my app it is working fine. Make a function of this code according to your need.

public static void main(String arr[])
{
    Map<String,String> hashmap=new HashMap<String,String>();
    hashmap.put("key1","value1");
    hashmap.put("key2","value2");
    hashmap.put("key3","value3");
    hashmap.put("key4","value4");

    FileOutputStream fos;
    try {
        fos = new FileOutputStream("c://list.ser");

        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(hashmap);
        oos.close();

        FileInputStream fis = new FileInputStream("c://list.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Map<String,String> anotherList = (Map<String,String>) ois.readObject();

        ois.close();

        System.out.println(anotherList);

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

}
1
votes

When using a correctly implemented hash table (like java.util.HashMap), you don't have to worry about the hashCode() method of your keys. The technique referred to in item #3 of the original post is actually built into a good hash table implementation.

The default serialization mechanism is overridden. A simple list of entries (key–value) pairs is stored instead. When deserializing the hash table, the table's put() method is used to re-add each entry individually. This maintains the consistency of the new, deserialized hash table instance. It won't matter if the hash codes of the keys have changed; the bucket is chosen based on the hash code of the key at the time of deserialization.

1
votes

If all else fails, can you serialize your Map using JSON or YAML or XML or something?

1
votes

Add these methods to the class that contains the map. You have also to add serialisation/de-serialization of any other fields:

private void writeObject(ObjectOutputStream stream) throws IOException {
    stream.writeInt(map.size());
    for (Entry<String, String> entry : map.entrySet()) {
        stream.writeObject(entry.getKey());
        stream.writeObject(entry.getValue());
    }
}


private void readObject(ObjectInputStream stream) throws IOException,
        ClassNotFoundException {
    int mapSize = stream.readInt();
    for (int i = 0; i < mapSize; i++) {
        String key = (String) stream.readObject();
        String value = (String) stream.readObject();
        map.put(key, value);
    }
}
0
votes

If you re-read the paragraph you would notice "Therefore accepting the default serialized form for a hash table would constitute a serious bug", It doesn't mean the Hash implementations in Java use default serialized form, I believe Java uses custom serialization for its Hash implementations.

Hope this information is useful.