Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weird serialisation behavior with HashMap

Consider the three following classes:

  • EntityTransformer contains a map associating an Entity with a String
  • Entity is an object containing an ID (used by equals / hashcode), and which contains a reference to an EntityTransformer (note the circular dependency)
  • SomeWrapper contains an EntityTransformer, and maintains a Map associating Entity's identifiers and the corresponding Entity object.

The following code will create an EntityTransformer and a Wrapper, add two entities to the Wrapper, serialize it, deserialize it and test the presence of the two entitites:

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

The output is:

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

false

true

So basically, the serialization failed somehow, as the map should contain both entities as Keys. I suspect the cyclic dependency between Entity and EntityTransformer, and indeed if I make static the EntityManager instance variable of Entity, it works.

Question 1: given that I'm stuck with this cyclic dependency, how could I overcome this issue ?

Another very weird thing: if I remove the Map maintaining an association between identifiers and Entities in the Wrapper, everything works fine... ??

Question 2: someone understand what's going on here ?

Bellow is a full functional code if you want to test it:

Thanks in advance for your help :)

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

EDIT

There is a good summary of what is potentially in play for this issue: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4957674

The problem is that HashMap's readObject() implementation , in order to re-hash the map, invokes the hashCode() method of some of its keys, regardless of whether those keys have been fully deserialized.

If a key contains (directly or indirectly) a circular reference to the map, the following order of execution is possible during deserialization --- if the key was written to the object stream before the hashmap:

  1. Instantiate the key
  2. Deserialize the key's attributes 2a. Deserialize the HashMap (which was directly or indirectly pointed to by the key) 2a-1. Instantiate the HashMap 2a-2. Read keys and values 2a-3. Invoke hashCode() on the keys to re-hash the map 2b. Deserialize the key's remaining attributes

Since 2a-3 is executed before 2b, hashCode() may return the wrong answer, because the key's attributes have not yet been fully deserialized.

Now that does not explain fully why the issue can be fixed if the HashMap from Wrapper is removed, or move to the EntityTransformer class.

like image 270
ecniv Avatar asked Apr 05 '12 15:04

ecniv


2 Answers

This is a problem with circular initialisation. Whilst Java Serialisation can handle arbitrary cycles, the initialisation has to happen in some order.

There's a similar problem in AWT where Component (Entity) contains a reference to its parent Container (EntityTransformer). What AWT does is to make the parent reference in Component transient.

transient Container parent;

So now each Component can complete its initialisation before Container.readObject adds it back in:

    for(Component comp : component) {
        comp.parent = this;
like image 58
Tom Hawtin - tackline Avatar answered Nov 14 '22 11:11

Tom Hawtin - tackline


Even stranger, if you do

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

After serializing and de-serializing, you will get the correct output.

Also:

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

Gives:

a1
false
a2
true

I think you found a bug. Most likely, serialization broke the hashing somehow. In fact, I think you might have found this bug.

like image 36
trutheality Avatar answered Nov 14 '22 13:11

trutheality