Can Gencer
Can Gencer

Reputation: 8885

Serialization and deserialization of lambda

This code below throws

Exception in thread "main" java.lang.ClassCastException: test.Subclass2 cannot be cast to test.Subclass1
at test.LambdaTest.main(LambdaTest.java:17)

public class LambdaTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ToLongFunction<B> fn1 = serde((ToLongFunction<B> & Serializable) B::value);
        ToLongFunction<C> fn2 = serde((ToLongFunction<C> & Serializable) C::value);
        fn1.applyAsLong(new B());
        fn2.applyAsLong(new C()); // Line 17 -- exception here!
    }

    private static <T extends Serializable> T serde(T t) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        new ObjectOutputStream(bos).writeObject(t);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos
                .toByteArray()));
        return (T) ois.readObject();
    }
}

class A {
    public long value() {
        return 0;
    }
}

class B extends A { }

class C extends A { }

The reason seems to be that after serialization and deserialization, both fn1 and fn2 end up as the same class. Is this a JDK/compiler bug or am I missing something about serialization and deserialization of lambdas?

Upvotes: 15

Views: 1122

Answers (3)

Nisheeth Shah
Nisheeth Shah

Reputation: 608

If you want to make it work, then remove the default implementation of value() from BaseClass interface and override it in the implementing classes like this:

interface BaseClass {
    long value();
}
public class LambdaTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ToLongFunction<Subclass1> fn1 = serDe((Serializable & ToLongFunction<Subclass1>) Subclass1::value);
        fn1.applyAsLong(new Subclass1());
        ToLongFunction<Subclass2> fn2 = serDe((Serializable & ToLongFunction<Subclass2>) Subclass2::value);
        fn2.applyAsLong(new Subclass2());
    }

    private static <T extends Serializable> void printType(T t) {
        System.out.println(t.getClass().getSimpleName());
    }

    private static <T extends Serializable> T serDe(T t) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        new ObjectOutputStream(bos).writeObject(t);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        T readT = (T) ois.readObject();
        ois.close();
        bos.close();
        return readT;
    }
}
class Subclass1 implements BaseClass {

    @Override
    public long value() {
        return 0;
    }
}
class Subclass2 implements BaseClass {

    @Override
    public long value() {
        return 1;
    }
}

Upvotes: 0

Marko Topolnik
Marko Topolnik

Reputation: 200236

Have a look at this Open JDK issue raised back in 2016:

Deserialization of lambda causes ClassCastException

It quite precisely matches your scenario:

  • Two (distinct) classes, B and C, both of which extend the same base class, A, which has a method, String f().
  • Create a Supplier reference to method f() for an object of type B; call this bf [new B()::f].
  • Create a Supplier reference to method f() for an object of type C; cal this cf [new C()::f].
  • Serialize cf (ObjectOutputStream#writeObject)
  • When the serialized cf is deserialized (ObjectInputStream#readObject), a ClassCastException is thrown saying that class C cannot be cast to class B

There's an interesting discussion on the issue, but the very last comment by Dan Smith seems to nail it:

Important observation for this particular test case: the "qualifying type" (i.e., the class named by the bytecode) of a method reference should be the same as the qualifying type of an invocation: the type of the receiver. javac is wrong to be using the type of the declaring class. See JDK-8059632.

Fix that bug, and I think the issue with different captured types goes away.

Upvotes: 9

davidxxx
davidxxx

Reputation: 131496

I don't know how to explain that but the behavior is particular because executing the main code with only one invocation of serde() works for both cases.

That is this :

ToLongFunction<B> fn1 = serde((ToLongFunction<B> & Serializable) A::value);
fn1.applyAsLong(new B());

or this :

ToLongFunction<C> fn2 = serde((ToLongFunction<C> & Serializable) A::value);
fn2.applyAsLong(new C());

While as the two invocations are performed in the same program, I notice an expected thing : the value of the parameter (T t) is not the same for the two cases.
But I notice an unexpected thing : at the second invocation of serde() the object returned by ois.readObject() is the same reference as which one returned by the first ois.readObject() invocation.
As if the first one was cached and reused for the second invocation.

It seems a bug.

Upvotes: 0

Related Questions