Different deserialization behavior between Java 8 and Java 11

18
June 13, 2019, at 2:30 PM

I have a problem with deserialization in Java 11 that results in a HashMap with a key that can't be found. I would appreciate if anyone with more knowledge about the issue could say if my proposed solution looks ok, or if there is something better I could do.

Consider the following contrived implementation (the relationships in the real problem are a bit more complex and hard to change):

public class Element implements Serializable {
    private static long serialVersionUID = 1L;
    private final int id;
    private final Map<Element, Integer> idFromElement = new HashMap<>();
    public Element(int id) {
        this.id = id;
    }
    public void addAll(Collection<Element> elements) {
        elements.forEach(e -> idFromElement.put(e, e.id));
    }
    public Integer idFrom(Element element) {
        return idFromElement.get(element);
    }
    @Override
    public int hashCode() {
        return id;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Element)) {
            return false;
        }
        Element other = (Element) obj;
        return this.id == other.id;
    }
}

Then I create an instance that has a reference to itself and serialize and deserialize it:

public static void main(String[] args) {
    List<Element> elements = Arrays.asList(new Element(111), new Element(222));
    Element originalElement = elements.get(1);
    originalElement.addAll(elements);
    Storage<Element> storage = new Storage<>();
    storage.serialize(originalElement);
    Element retrievedElement = storage.deserialize();
    if (retrievedElement.idFrom(retrievedElement) == 222) {
        System.out.println("ok");
    }
}

If I run this code in Java 8 the result is "ok", if I run it in Java 11 the result is a NullPointerException because retrievedElement.idFrom(retrievedElement) returns null.

I put a breakpoint at HashMap.hash() and noticed that:

  • In Java 8, when idFromElement is being deserialized and Element(222) is being added to it, its id is 222, so I am able to find it later.
  • In Java 11, the id is not initialized (0 for int or null if I make it an Integer), so hash() is 0 when it's stored in the HashMap. Later, when I try to retrieve it, the id is 222, so idFromElement.get(element) returns null.

I understand that the sequence here is deserialize(Element(222)) -> deserialize(idFromElement) -> put unfinished Element(222) into Map. But, for some reason, in Java 8 id is already initialized when we get to the last step, while in Java 11 it is not.

The solution I came up with was to make idFromElement transient and write custom writeObject and readObject methods to force idFromElement to be deserialized after id:

...
transient private Map<Element, Integer> idFromElement = new HashMap<>();
...
private void writeObject(ObjectOutputStream output) throws IOException {
    output.defaultWriteObject();
    output.writeObject(idFromElement);
}
@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
    input.defaultReadObject();
    idFromElement = (HashMap<Element, Integer>) input.readObject();
}

The only reference I was able to find about the order during serialization/deserialization was this:

For serializable classes, the SC_SERIALIZABLE flag is set, the number of fields counts the number of serializable fields and is followed by a descriptor for each serializable field. The descriptors are written in canonical order. The descriptors for primitive typed fields are written first sorted by field name followed by descriptors for the object typed fields sorted by field name. The names are sorted using String.compareTo.

Which is the same in both Java 8 and Java 11 docs, and seems to imply that primitive typed fields should be written first, so I expected there would be no difference.

Implementation of Storage<T> included for completeness:

public class Storage<T> {
    private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    public void serialize(T object) {
        buffer.reset();
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(buffer)) {
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
        } catch (Exception ioe) {
            ioe.printStackTrace();
        }
    }
    @SuppressWarnings("unchecked")
    public T deserialize() {
        ByteArrayInputStream byteArrayIS = new ByteArrayInputStream(buffer.toByteArray());
        try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayIS)) {
            return (T) objectInputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}
READ ALSO
UDP-Java Socket how can I exceed args.length &gt; 2 to test how robust the client server connection is?

UDP-Java Socket how can I exceed args.length > 2 to test how robust the client server connection is?

I tried to enter a different IntetAdress object with googlede before it was initialized to null, which does not make a difference in output

35
Saving the output of array permutations into a 2D array

Saving the output of array permutations into a 2D array

I am trying to save the permutation result of an array into a 2d arraylet's say I have an array {1,2,3,4}, I created a 2D array with rows: N! and columns 4

27
How to convert a string list to my specific java class

How to convert a string list to my specific java class

My problem is that I write a method that takes as parameter a list of my specific java class, and convert it to a String thanks to a getter of an information of the class and I return the string listI would now like to do the opposite, that is, I give it a string...

19