4

Apparently Java serialization mechanism somehow manages to create an instance of subclass using superclass constructor. I wonder, how is it possible?

Here's a test which demonstrates this:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.MessageFormat;

public class Test {

    public static class A {
        public final int a;

        public A() {
            this.a = 0;
            System.out.println(
                    MessageFormat.format(
                        "new A() constructor is called to create an instance of {0}.",
                    getClass().getName()));
        }

        public A(int a) {
            this.a = a;
            System.out.println(
                    MessageFormat.format(
                        "new A(int) constructor is called to create an instance of {0}.", 
                    getClass().getName()));
        }
    }

    public static class B extends A implements Serializable {
        public final int b;

        public B(int a, int b) {
            super(a);
            this.b = b;
            System.out.println(
                    MessageFormat.format(
                        "new B(int, int) constructor is called to create an instance of {0}.",
                    getClass().getName()));
        }

        @Override
        public String toString() {
            return "B [a=" + a + ", b=" + b + "]";
        }


    }

    public static void main(String[] args) throws Exception {

        B b1 = new B(10,20);

        System.out.println(b1);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try(ObjectOutputStream oos = new ObjectOutputStream(bos)) {
            oos.writeObject(b1);
        }

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        try (ObjectInputStream ois = new ObjectInputStream(bis)) {
            B b2 = (B)ois.readObject();
            System.out.println(b2);
        }
    }
}

Output:

new A(int) constructor is called to create an instance of Test$B.
new B(int, int) constructor is called to create an instance of Test$B.
B [a=10, b=20]
new A() constructor is called to create an instance of Test$B.
B [a=0, b=20]

(You can try it out live on Ideone).

As you see, the A() constructor is called during deserialization to produce an instance of B. Under the hood this is invoked in ObjectStreamClass.newInstance() and the instance is created by the Constructor.newInstance() call. In the debugger, the constructor cons is Test$A():

Screenshot from the the debugger showing that <code>cons</code> is <code>Test$A()</code>

Stepping out in the debugger, the created object is finally returned from ObjectInputStream.readObject(...) and it is casted without problems to B.

So if I am not mistaken, it seems that the A() constructor was used (via reflection) to create an instance of B.

I wonder how is this possible.

11
  • 1
    Use a byte code viewer: new Type() first creates an instruction NEW Type and then calls the constructor with INVOKESPECIAL ... on this instance. So a constructor always expects the object of the final type on the stack. Calling super does not create an object of B, it receives it. Commented Apr 15, 2018 at 15:13
  • 1
    @CoronA This sounds logical, but I don't see how cons.newInstance(); would know what the "final type" is. cons is Test$A(), I don't see where B is involved here at all. Commented Apr 15, 2018 at 15:20
  • I would point to a field in the java.reflect.Constructor: private volatile ConstructorAccessor constructorAccessor. It is filled with an object of type GeneratedSerializationConstructorAccesor1@.... Its use can be found in Constructor.newInstance Commented Apr 15, 2018 at 16:04
  • @CoronA Getting closer, but I don't have the full picture yet. Commented Apr 15, 2018 at 16:14
  • @CoronA I think this constructor accessor is just "reflection-optimized-by-bytecode-generation". Still unclear. Commented Apr 15, 2018 at 16:17

2 Answers 2

2
+250

I had the suspicion that something must be wrong with the constructor cons. And I have found the location where the ordinary constructor of A is changed to a serializable constructor of B.

First I looked where cons is first set. In the serialization case this is the constructor of ObjectStreamClass:

if (externalizable) {
   cons = getExternalizableConstructor(cl);
} else {
   cons = getSerializableConstructor(cl); //here
   ...

So I stepped through and found in ObjectStreamClass.getSerializableConstructor:

Constructor<?> cons = initCl.getDeclaredConstructor((Class<?>[]) null);
...
cons = reflFactory.newConstructorForSerialization(cl, cons); //this does change
cons.setAccessible(true);
return cons;

Putting a debug watch on cons.newInstance()

  • before the marked line => type is A.
  • after the marked line => type is B.

that means that the constructor used for serialization is not the ordinary constructor of A but a modified one for serialization, which is adapted to the final class.

Sign up to request clarification or add additional context in comments.

2 Comments

I've temporarily unaccepted your answer, I'll put/award you a bounty and the accept again. Thank you very much for your persistence.
Maybe the downvoter could give a hint where his/her expectation was not met? Thank you @lexicore, for pointing me to this interesting detail.
0

JVM during deserialization doesn't call a constructor of the class being deserialized. But in order to create an instance of the deserialized class it need to create its super classes first. So JVM calls the no args constructor of first parent that doesn't implement Serializable. Though it doesn't create an instance of your class in this constructor. If your parent class would be Serializable, there wasn't constructor calls at all.

27 Comments

ObjectStreamClass.newInstance() does call Test$A() constructor via reflection. I wonder how the result can be B.
It isn't final result. It's about how JVM implementing inheritance and serialization. To create object JVM should creates all its parents. Serializable parents are created without constructor calls. But JVM can't create not Serializable parent without constructor call.
So JVM creates not serializable parent using constructor and then continue deserializing without constructor calls
Is serialization a part of JVM? I don't see any JVM magic in ObjectInputStream and co. Some reflection usage, but nothing too peculiar.
So JVM creates not serializable parent using constructor and then continue deserializing without constructor calls - Ok, let's leave it with JVM. But how does it work that an instance created with the Test$A() constructor is an instance of B? Because this is what happens here.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.