Search code examples
javaserializationhashmapignitebinary-serialization

Apache Ignite: Object cannot be found in Hashmap after deserialization


I have a problem with Apache Ignite Serialization/Deserialization related to the order in which fields are deserialized. I need to put an instance of "B" as follow in the Ignite cache:

public class A {
    private final String name;

    public A(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}


public class B extends A {

    private Map<B, String> mapOfB;

    public B(String name) {
        super(name);

        mapOfB = new HashMap<>();
    }

    public void addB(B newB, String someString) {
        mapOfB.put(newB, someString);
    }

    public Map<B, String> getMap() {
        return mapOfB;
    }

    @Override
    public boolean equals(Object obj) {
        if( obj != null && obj instanceof B) {
            if(this.getName() == null && ((B) obj).getName() == null && this == obj) {
                return true;
            } else if(this.getName().equals(((B) obj).getName())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        return this.getName()==null? System.identityHashCode(this):this.getName().hashCode();
    }
}

The if I run the following code:

    public static void main(String[] args) {
        // write your code here
        B b1 = new B("first");
        b1.addB(b1, "some first string");
        B b2 = new B("second");
        b1.addB(b2, "some second string");

        // init Ignite configuration
        // force java.util.Hashtable to be binary serialized, 
        // it prevents infinite recursion and  other problems 
        // occurring with the Optimized Serializer
        IgniteConfiguration cfg = new IgniteConfiguration();
        BinaryConfiguration binConf = new BinaryConfiguration();
        Collection<String> binClassNames = new LinkedList<>();
        binClassNames.add("java.util.Hashtable");
        binConf.setClassNames(binClassNames);
        cfg.setBinaryConfiguration(binConf);
        Ignition.start(cfg);

        // put b1 in cache
        IgniteCache cache = Ignition.ignite().getOrCreateCache("MyCache");
        cache.put(b1.hashCode(), b1);

        //get b1 from cache
        B b1FromCache= (B) cache.get(b1.hashCode());

        // print map values
        System.out.println("b1 map value: " + b1.getMap().get(b1));
        System.out.println("b1 from cache map value: " + b1FromCache.getMap().get(b1));
    }

Output is

b1 map value: some first string

b1 from cache map value: null

The problem is that fields from the child are deserialized before fields from the parent, so when Ignite deserializes B, it first creates an empty B object (with null "name" and "mapOfB"), then it tries to deserialize mapOfB. It creates the Hashtable and then deserialize each object it contains to fill it.

For b2 in the example above there is no problem as no reference to b2 exists yet when it is deserialized, so a new b2 object is created, b2 fields are populated (including the "name" field), then it is added to the Hashmap with correct hash.

For b1 however deserialization started, so the object already exists in Ignit's map of deserialized objects, but with null name (deserialization is in process for b1), and with a hashCode computed with this null name. The Hashtable puts b1 with the hashCode computed at that time, so when we try to find b1 with non null name in the map at the end it cannot be found.

I cannot change A and B classes and the way these objects are created (how the Hashmap is filled,...) so it has to be solved by changing the serialization. Is there a simple way to do that?

Remark: actual code is way more complicated than that, with may classes between actual B and the Hashmap.


Solution

  • You are correct about the reason of such behaviour. mapOfB field is deserialized before the name field, and hashCode() depends on that name. And fields of B are changed after you put it into the map as a key, so it's hashCode changes.

    I'd recommend you to change your data model, but since you can't, here is another option... OptimizedMarshaller doesn't seem to have a problem with Maps, because it uses a simple Java serialization. But you won't be able to use BinaryObject abstraction and a few other features with it. Here is how you can enable OptimizedMarshaller:

    OptimizedMarshaller marshaller = new OptimizedMarshaller();
    cfg.setMarshaller(marshaller);
    

    If you store values, that don't implement Serializable interface, then you may need to configure it appropriately:

    marshaller.setRequireSerializable(false);
    

    But note, that disabling requireSerializable flag may affect serialization performance in a negative way.