【问题标题】:Weird serialisation behavior with HashMapHashMap 的奇怪序列化行为
【发布时间】:2012-04-05 15:50:15
【问题描述】:

考虑以下三个类:

  • EntityTransformer 包含将 Entity 与字符串相关联的映射
  • Entity 是一个包含 ID 的对象(由 equals / hashcode 使用),其中包含对 EntityTransformer 的引用(注意循环依赖)
  • SomeWrapper 包含一个 EntityTransformer,并维护一个关联 Entity 的标识符和对应的 Entity 对象的 Map。李>

以下代码将创建一个 EntityTransformer 和一个 Wrapper,向 Wrapper 添加两个实体,对其进行序列化、反序列化并测试这两个实体是否存在:

public static void main(String[] args)
    throws Exception {

    EntityTransformer et = new EntityTransformer();
    Wrapper wr = new Wrapper(et);

    Entity a1 = wr.addEntity("a1");  // a1 and a2 are created internally by the Wrapper
    Entity a2 = wr.addEntity("a2");

    byte[] bs = object2Bytes(wr);
    wr = (SomeWrapper) bytes2Object(bs);

    System.out.println(wr.et.map);
    System.out.println(wr.et.map.containsKey(a1));
    System.out.println(wr.et.map.containsKey(a2));
}

输出是:

{a1=whatever-a1, a2=whatever-a2}

是的

所以基本上,序列化以某种方式失败,因为地图应该包含两个实体作为键。我怀疑 Entity 和 EntityTransformer 之间的循环依赖关系,实际上,如果我将 Entity 的 EntityManager 实例变量设为静态,它就可以工作。

问题 1:鉴于我被这种循环依赖所困扰,我该如何克服这个问题?

另一件非常奇怪的事情:如果我删除维护 Wrapper 中标识符和实体之间的关联的 Map,一切正常...??

问题 2:有人明白这里发生了什么吗?

如果你想测试一下,下面是一个完整的功能代码:

在此先感谢您的帮助 :)

public class SerializeTest {

public static class Entity
        implements Serializable
 {
    private EntityTransformer em;
    private String id;

    Entity(String id, EntityTransformer em) {
        this.id = id;
        this.em = em;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Entity other = (Entity) obj;
        if ((this.id == null) ? (other.id != null) : !this.id.equals(
            other.id)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 97 * hash + (this.id != null ? this.id.hashCode() : 0);
        return hash;
    }

    public String toString() {
        return id;
    }
}

public static class EntityTransformer
    implements Serializable
{
    Map<Entity, String> map = new HashMap<Entity, String>();
}

public static class Wrapper
    implements Serializable
{
    EntityTransformer et;
    Map<String, Entity> eMap;

    public Wrapper(EntityTransformer b) {
        this.et = b;
        this.eMap = new HashMap<String, Entity>();
    }

    public Entity addEntity(String id) {
        Entity e = new Entity(id, et);
        et.map.put(e, "whatever-" + id);
        eMap.put(id, e);

        return e;
    }
}

public static void main(String[] args)
    throws Exception {
    EntityTransformer et = new EntityTransformer();
    Wrapper wr = new Wrapper(et);

    Entity a1 = wr.addEntity("a1");  // a1 and a2 are created internally by the Wrapper
    Entity a2 = wr.addEntity("a2");

    byte[] bs = object2Bytes(wr);
    wr = (Wrapper) bytes2Object(bs);

    System.out.println(wr.et.map);
    System.out.println(wr.et.map.containsKey(a1));
    System.out.println(wr.et.map.containsKey(a2));
}



public static Object bytes2Object(byte[] bytes)
    throws IOException, ClassNotFoundException {
    ObjectInputStream oi = null;
    Object o = null;
    try {
        oi = new ObjectInputStream(new ByteArrayInputStream(bytes));
        o = oi.readObject();
    }
    catch (IOException io) {
        throw io;
    }
    catch (ClassNotFoundException cne) {
        throw cne;
    }
    finally {
        if (oi != null) {
            oi.close();
        }
    }

    return o;
}

public static byte[] object2Bytes(Object o)
    throws IOException {
    ByteArrayOutputStream baos = null;
    ObjectOutputStream oo = null;
    byte[] bytes = null;
    try {
        baos = new ByteArrayOutputStream();
        oo = new ObjectOutputStream(baos);

        oo.writeObject(o);
        bytes = baos.toByteArray();
    }
    catch (IOException ex) {
        throw ex;
    }
    finally {
        if (oo != null) {
            oo.close();
        }
    }

    return bytes;
}
}

编辑

对这个问题的潜在影响有一个很好的总结: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4957674

问题在于 HashMap 的 readObject() 实现,按顺序 重新散列地图,调用它的一些键的 hashCode() 方法, 无论这些密钥是否已完全反序列化。

如果键包含(直接或间接)对 map,以下执行顺序是可能的 反序列化 --- 如果之前将密钥写入对象流 哈希图:

  1. 实例化密钥
  2. 反序列化键的属性 2a.反序列化HashMap(直接或间接被key指向) 2a-1。实例化 HashMap 2a-2。读取键和值 2a-3。在键上调用 hashCode() 以重新散列地图 2b。反序列化密钥的剩余属性

由于2a-3在2b之前执行,hashCode()可能会返回错误 回答,因为key的属性还没有完全 反序列化。

现在这并不能完全解释为什么如果从 Wrapper 中删除 HashMap 或移至 EntityTransformer 类可以解决问题。

【问题讨论】:

  • 为什么你的equals 逻辑如此复杂?为什么不只是return (this.id == null) ? (other.id == null ) : this.id.equals( other.id );
  • 此示例的等号/哈希码是由 NetBeans 自动生成的,因为这不是我问题的真正根源,所以我没有花时间查看它
  • @dty 我的意思是用它替换 if 语句和最后一个 return 语句。我不是白痴。
  • 我同意,但如果我每次有人写一个看起来和你写的一模一样的 equals() 方法时都有一美元,我会是一个非常富有的人。我什至不是在开玩笑。有很多人认为你写的东西作为整个 equals() 方法是完全可以接受的。另外,作为一名软件工程师,我更喜欢精确的语言,而不是希望人们以特定的方式解释我的通信。
  • 大概是this reported bug的体现。

标签: java serialization deserialization


【解决方案1】:

这是循环初始化的问题。虽然 Java 序列化可以处理任意循环,但初始化必须按某种顺序进行。

在 AWT 中存在类似问题,其中 Component (Entity) 包含对其父 Container (EntityTransformer) 的引用。 AWT 所做的就是在Componenttransient 中进行父引用。

transient Container parent;

所以现在每个Component 都可以在Container.readObject 重新添加之前完成其初始化:

    for(Component comp : component) {
        comp.parent = this;

【讨论】:

  • 嗯,这似乎是一个有趣的解决方案...现在,如果我无法更改 EntityTransformer(例如,它是商业库中的外部类)怎么办?
  • 我发现将 Wrapper 中包含的地图直接放入 EntityTransformer 似乎可以解决问题。很奇怪……
  • 序列化对 EntityTransformer 的引用完成了这项工作,并且似乎是一个更清洁的解决方案,谢谢 :)
【解决方案2】:

即使是陌生人,如果你这样做了

Map<Entity, String> map = new HashMap<>(wr.et.map);
System.out.println(map.containsKey(a1));
System.out.println(map.containsKey(a2));

序列化和反序列化后,你会得到正确的输出。

还有:

for( Entity a : wr.et.map.keySet() ){
    System.out.println(a.toString());
    System.out.println(wr.et.map.containsKey(a));
}

给予:

a1
false
a2
true

我认为你发现了一个错误。最有可能的是,序列化以某种方式破坏了散列。 其实我想你可能已经找到this bug

【讨论】:

  • 我玩得越多,我就越倾向于同意。
【解决方案3】:

是否可以重写序列化,在序列化之前将引用转换为键值,然后在反序列化时将其转换回?

在序列化时找到 EntityTransformer 的哈希键并改用该值(可能在名为 parentKey 的结构中提供一个值)并将引用清空似乎是非常简单的。然后在重新序列化时,您会找到与该键值关联的 EntityTransformer 并分配其引用。

【讨论】:

  • 嗯,我不确定我是否理解你的建议。如果 EntityTransformer 实际上没有被序列化,我应该如何在重新序列化时找到它? (请注意,在我的用例中,序列化对于将对象传递给不一定共享同一 VM 的其他程序很有用)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多