Search code examples
javaserializationdeserializationtype-erasure

How does Java Object (De-)serialization work with generic classes?


As far as I know the java compiler performs type erasure for generic classes so that the type information for type parameters is replaced by Object or it's upper bound. How can the serialization and the deserialization afterwards restore the orignal type?

For example if you have a generic class like this:

public class TestGeneric<T extends Serializable> implements Serializable {
    T value;
    public TestGeneric(T value) {
        this.value = value;
    }

    public T getValue() {
        return this.value;
    }
}

I would expect that the JVM sees this at runtime:

public class TestGeneric implements Serializable {
    Serializable value;
    public TestGeneric(Serializable value) {
        this.value = value;
    }

    public Serializable getValue() {
        return this.value;
    }
}

but if I execute the following code, I get a class cast exception:

public static void main(String argv[]) {
        File newFile = new File("./test.txt");
        try {
            if (!newFile.exists()) {
                TestGeneric<Double> testGeneric = new TestGeneric<>(0.2d);

                try (FileOutputStream fos = new FileOutputStream(newFile); ObjectOutputStream oos = new ObjectOutputStream(fos)) {
                    oos.writeObject(testGeneric);
                }
            } else {
                try (FileInputStream fis = new FileInputStream(newFile); ObjectInputStream ois = new ObjectInputStream(fis)) {
                    try {
                        TestGeneric<Boolean> genericRead = (TestGeneric<Boolean>) ois.readObject();
                        Boolean isNotABoolean = genericRead.getValue();
                        System.out.println(isNotABoolean);
                    } catch (ClassNotFoundException e) {
                        System.err.println("Class not found!");
                    }
                }
            }
        } catch(IOException e) {
            System.err.println("IO Exception occurred, reason: " + e.getMessage());
        }
}

I get the following error after deserialization of the object (while 2nd run):

Exception in thread "main" java.lang.ClassCastException: class java.lang.Double cannot be cast to class java.lang.Boolean (java.lang.Double and java.lang.Boolean are in module java.base of loader 'bootstrap')

It looks like that even after type erasure has been performed, the JVM is aware of the exact type of the field at runtime. How can this work?


Solution

  • Your question doesn't really have anything to do with generics or type erasure - if you change your TestGeneric class to not be generic:

    public static class TestGeneric implements Serializable {
        Serializable value;
        public TestGeneric(Serializable value) {
            this.value = value;
        }
    
        public Serializable getValue() {
            return this.value;
        }
    }
    

    (and make corresponding changes to the main method), you will still get the exact same error.

    While type erasure does erase the generic type information from the containing class, the object stored in the value member field still knows its own type, regardless of the declared type.

    Java serialisation is based on the runtime type information of the data. So the actual type of the value field (i.e. Double) is serialised along with the value. And when the data is deserialised back in, the original Double value is reconstructed, including the runtime type information that says it is a Double. When you then attempt to cast that object to a Boolean you get the ClassCastException as you would expect.