将字符串作为键的映射序列化和反序列化

19

我打算序列化和反序列化一个键为字符串的哈希表。

根据Josh Bloch的《Effective Java》中第222页的解释,我理解以下内容:

例如,考虑一个哈希表的情况。它的物理表示是一系列包含键值项的哈希桶。一个条目被放在哪个桶中取决于键的哈希码,这在JVM实现之间通常不保证相同。事实上,即使在同一个JVM实现上运行多次,它也不能保证相同。因此,接受哈希表的默认序列化形式将构成严重的错误。序列化和反序列化哈希表可能会产生一个不满足不变量条件的对象。

我的问题是: 1)通常情况下,如果覆盖key类的equals和hashCode方法,是否能解决这个问题并恢复哈希表? 2)如果我的key是String,而String类已经覆盖了hashCode()方法,那么我仍然会遇到以上描述的问题吗?(我看到一个bug,让我认为即使key是String并覆盖了hashCode,这可能仍然是个问题。) 3)以前,我通过序列化条目(key,value)数组来绕过这个问题,并在反序列化时重建哈希表。我想知道是否有更好的方法。 4)如果问题1和2的答案是仍然无法保证,有人能解释一下原因吗?如果哈希码相同,它们会在JVM之间的相同桶中吗?

谢谢,Grace


2
如果您尝试序列化一个Map,您需要确保整个对象树都是可序列化的,包括键、值和Map实现。 - cs80
第二版的第299页 - tiktak
7个回答

23
java.util.HashMap 的序列化形式不会序列化桶本身,哈希码也不是持久状态的一部分。根据 javadocs:
“序列化数据:HashMap 的容量(桶数组的长度)被发出(作为 int),其次是 HashMap 的大小(键值映射数目)(也作为 int),然后是 HashMap 中每个键值映射的键(Object)和值(Object)。键值映射按它们由 entrySet().iterator() 返回的顺序发出。”
取自http://java.sun.com/j2se/1.5.0/docs/api/serialized-form.html#java.util.HashMap 持久状态基本上包括键、值和一些管理工作。当反序列化时,HashMap 将被完全重建;键将被重新散列并放置在适当的桶中。
因此,添加字符串键应该可以正常工作。我猜你的 bug 在别的地方。
编辑:这里有一个 JUnit 4 测试用例,它序列化和反序列化了一个映射,并模拟 VM 改变哈希码。测试通过,尽管反序列化后哈希码不同。
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);
        }
    }

}

你对这篇文章的看法如何?它建议在正确序列化哈希映射时需要做一些额外的工作。谢谢。(我觉得它与上面引用的Josh Bloch的段落是一致的。)http://obscured.info/2007/02/15/serializable-override-hashcode/ - Grace K
这篇文章说在实现Serializable时要重写hashCode/equals,但实际上规则更普遍 - 如果您的对象将用作Map中的键,则需要重写。请参见我的编辑以演示即使在更改哈希码的情况下,序列化映射也可以正常工作的测试用例。 - mdma
有几件事情需要注意。1)这个语句似乎没有任何影响:“Key.xor = 0x7555AAAA”,如果我将其删除并运行测试,它仍然可以正常工作。2)当HashMap包含在另一个对象中时(这是我们正在尝试的),此方法不起作用。 - Dan Doyon
当然,测试仍然有效,哈希码是相同的值。测试的重点是要表明即使哈希码发生变化,反序列化仍然有效。如果在对象中的哈希映射不适用于我们的情况,则故障出现在您的对象序列化中。您说该方法对您无效,但这里没有方法 - 我只是在展示哈希映射可以安全地在哈希码可能不同的VM之间进行序列化。 - mdma
抱歉,我没有仔细阅读那个编辑关于虚拟机的内容。在我们的情况下,我们有一个包含哈希映射<Key, V>及其压缩的对象。您认为我需要为类中的每个可序列化对象编写writeObject/readObject吗? - Dan Doyon

6

将哈希表序列化:

我尝试过这个方法并在我的应用程序中使用,它很好地工作了。 根据您的需要将此代码制作为函数。

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();
    }

}

6

我99%确定JVM实现的HashMap和HashSet处理了这个问题。它们有自定义的序列化和反序列化处理程序。我现在没有Bloch的书在手边,但我相信他是在解释这个挑战,而不是说你不能可靠地序列化java.util.HashMap。


1

将这些方法添加到包含地图的类中。您还需要添加任何其他字段的序列化/反序列化:

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);
    }
}

1
当使用正确实现的哈希表(例如java.util.HashMap)时,你不必担心键的hashCode()方法。原帖中提到的技巧实际上已经内置在一个良好的哈希表实现中。
默认的序列化机制被覆盖。而是存储了一个简单的条目(键-值)对列表。在反序列化哈希表时,使用表的put()方法逐个重新添加每个条目。这样保持了新的、反序列化的哈希表实例的一致性。无论键的哈希码是否改变,都会根据反序列化时键的哈希码选择桶。

你对下面的帖子有何看法?该帖子提出了,在正确序列化一个哈希映射时需要进行额外的操作。谢谢。你认为他正在使用错误实现的哈希表吗?谢谢。http://obscured.info/2007/02/15/serializable-override-hashcode/ - Grace K
是的,他的哈希表出了问题,而不是他的键。或者也许他完全诊断错误了问题。 - erickson

1
如果其他方法都失败了,你可以使用 JSON、YAML 或 XML 等格式序列化你的 Map 吗?

谢谢。我已经使用了我在上面帖子中提到的第三种方法,它运行得很好。我只是想更好地理解我的其他问题。 - Grace K

0
如果您重新阅读该段落,您会注意到“因此接受哈希表的默认序列化形式将构成严重的错误”,这并不意味着Java中的哈希实现使用默认序列化形式,我相信Java为其哈希实现使用自定义序列化。
希望这些信息对您有用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接