Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 lambdas that access instance fields and methods can't be deserialized

It seems to me it's a bug in the compiler or in the JVM, but maybe someone has a better explanation.

The following code runs fine as is, but if I uncomment the second runnable initialization, that uses 'this' directly, it can't deserialize the object (in.readObject() throws an exception).

public class TestClass implements Serializable {
    String               msg = "HEY!";
    SerializableRunnable runnable;
    public TestClass() {
        TestClass self = this;
        runnable = () -> self.say();  // uses a local copy of 'this'
       // runnable = () -> this.say(); // uses 'this' directly
    }
    public void say() {
        System.out.println(msg);
    }
    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (ObjectOutputStream out = new ObjectOutputStream(buffer)) {
            out.writeObject(new TestClass());
        }
        try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray()))) {
            TestClass s = (TestClass) in.readObject();
            s.say();
        }
    }
}
interface SerializableRunnable extends Runnable, Serializable {
}

This is the stacktrace for the root cause:

java.lang.IllegalArgumentException: Invalid lambda deserialization
    at j8test.TestClass.$deserializeLambda$(TestClass.java:1)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at java.lang.invoke.SerializedLambda.readResolve(SerializedLambda.java:230)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at java.io.ObjectStreamClass.invokeReadResolve(ObjectStreamClass.java:1104)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1810)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1993)
    at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1918)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
    at j8test.TestClass.main(TestClass.java:30)

Is it the expected behavior?

like image 288
tetsuo Avatar asked May 23 '14 19:05

tetsuo


2 Answers

I tried everything but the most obvious.

The problem happens in Eclipse (wherein the java 8 support is still in beta), but not in javac. Thus, a JDT bug.

[EDIT]

I'm running:

Eclipse IDE for Java and Report Developers
Version: Luna RC1 Release (4.4.0RC1)
Build id: 20140522-1310

Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)

OS X 10.9.3

Maybe it's already corrected in a more recent build.

like image 143
tetsuo Avatar answered Oct 24 '22 02:10

tetsuo


That is... rather odd.

This is what the documentation says about serializing lambdas:

You can serialize a lambda expression if its target type and its captured arguments are serializable. However, like inner classes, the serialization of lambda expressions is strongly discouraged.

I am not fully familiar with the captured arguments, but I am assuming that it is referring to all the elements that are being captured by the lambda, meaning in this case that it refers this, so it is a captured element then.

When further exploring that path, we see that TestClass needs to be serializable, which it seems to be as it implements Serializable. Moreover it will use the default lambda serialization (which is moreoften than not not a good idea), and it has as arguments a String and a SerializableRunnable, which both are Serializable again.

So it seems to me that you've hit a bug in the JVM and it could be caused by the target being equal to a captured argument (this).

like image 39
skiwi Avatar answered Oct 24 '22 02:10

skiwi