Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Non-capturing lambda seems to nevertheless capture the enclosing instance

I wrote this code:

import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) throws Exception {
        new Main();
    }

    private Main() throws Exception {
        Supplier<Thread> supplier = (Supplier<Thread> & Serializable) () -> new Thread() {};
        new ObjectOutputStream(System.out).writeObject(supplier);
    }
}

If I run it, I'll get an exception:

Exception in thread "main" java.io.NotSerializableException: Main
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeArray(ObjectOutputStream.java:1378)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1174)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Main.<init>(Main.java:28)

However, my understanding is that I properly made the lambda Serializable and I declared it such that it doesn't refer to any surrounding context, being therefore a non-capturing lambda. Yet the Main instance is captured and the result of the lambda expression fails to be serialized. I realize I'm declaring an anonymous class inside the lambda, but I would expect the lambda instance itself to be its enclosing instance, not the surrounding Main type.

Is this behavior expected by the Java Language Specification and, if yes, how come?

like image 757
William F. Jameson Avatar asked Jun 07 '17 12:06

William F. Jameson


1 Answers

In your lambda expression’s body, you have the anonymous class declaration new Thread() {} and you are not in a static context, so this expression implicitly captures this, which has the same meaning within the lambda expression as outside of it, per JLS §15.27.2, Lambda Body:

Unlike code appearing in anonymous class declarations, the meaning of names and the this and super keywords appearing in a lambda body, along with the accessibility of referenced declarations, are the same as in the surrounding context (except that lambda parameters introduce new names).

The transparency of this (both explicit and implicit) in the body of a lambda expression - that is, treating it the same as in the surrounding context - allows more flexibility for implementations, and prevents the meaning of unqualified names in the body from being dependent on overload resolution.

Since the surrounding context determines the behavior of the anonymous class, you can easily fix the issue by using a static context to create a nested class instead:

import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) throws Exception {
        write();
    }

    static void write() throws Exception {
        Supplier<Thread> supplier = (Supplier<Thread> & Serializable)() -> new Thread() {};
        new ObjectOutputStream(System.out).writeObject(supplier);
    }
}

Then, no surrounding instance will be captured.

Note that this can be seen as the general idea of lambda expressions, to define functions as expressions having exactly the same meaning as within the context they are written, except for the introduction of function parameters. The generation of an instance of a functional interface is only the vehicle to get this concept into the Java programming language in a compatible and useful way, but not a concept to influence the meaning of the lambda expression.

like image 68
Holger Avatar answered Nov 09 '22 17:11

Holger