使用HashMap进行奇怪的序列化行为

时间:2012-04-05 15:50:15

标签: java serialization deserialization

考虑以下三个类:

  • EntityTransformer 包含将实体与字符串相关联的地图
  • 实体是一个包含ID的对象(由equals / hashcode使用),其中包含对 EntityTransformer 的引用(请注意循环依赖关系)
  • SomeWrapper 包含 EntityTransformer ,并维护一个地图,其中包含实体的标识符和相应的实体 object。

以下代码将创建一个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 :有人了解这里发生了什么?

如果您想测试它,Bellow是一个完整的功能代码:

提前感谢您的帮助:)

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,可以执行以下执行顺序   反序列化---如果密钥之前被写入对象流   hashmap:

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

    由于2a-3在2b之前执行,hashCode()可能返回错误   回答,因为关键的属性尚未完全   反序列化。

现在这并没有完全解释为什么如果删除Wrapper中的HashMap,或者移动到EntityTransformer类,可以修复该问题。

3 个答案:

答案 0 :(得分:4)

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

在AWT中存在类似的问题,其中ComponentEntity)包含对其父ContainerEntityTransformer)的引用。 AWT的作用是在Component transient中制作父参考。

transient Container parent;

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

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

答案 1 :(得分:3)

更奇怪的是,如果你这样做

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

答案 2 :(得分:0)

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

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