Lambda with anonymous or local class inside unnecessarily captures 'this'
Tagir Valeev
amaembo at gmail.com
Thu Nov 13 12:59:26 UTC 2025
Hello!
I was playing with lambda instance deduplication and come up with the
following set of test cases:
import java.util.function.Supplier;
public final class CapturingLambda {
Runnable empty() {
return () -> {};
}
Runnable simple() {
return () -> {
System.out.println("simple");
};
}
Runnable withRunnableInside() {
return () -> {
Runnable r = () -> {};
r.run();
};
}
Runnable withAnonymousRunnableInside() {
return () -> {
Runnable r = new Runnable() {
@Override
public void run() {
}
};
r.run();
};
}
Runnable withLocalClassInside() {
return () -> {
class Local {}
new Local();
};
}
Runnable withLocalClassInsideNoInstance() {
return () -> {
class Local {}
};
}
void checkDeduplicated(String name, Supplier<Runnable> lambdaSupplier) {
Runnable lambda1 = lambdaSupplier.get();
Runnable lambda2 = lambdaSupplier.get();
if (lambda1 == lambda2) {
System.out.println("Deduplicated: "+name);
} else {
System.out.println("Not deduplicated: "+name);
}
}
private void test() {
checkDeduplicated("empty", this::empty);
checkDeduplicated("simple", this::simple);
checkDeduplicated("withRunnableInside", this::withRunnableInside);
checkDeduplicated("withAnonymousRunnableInside",
this::withAnonymousRunnableInside);
checkDeduplicated("withLocalClassInside",
this::withLocalClassInside);
checkDeduplicated("withLocalClassInsideNoInstance",
this::withLocalClassInsideNoInstance);
}
public static void main(String[] args) {
new CapturingLambda().test();
}
}
Here, checkDeduplicated checks whether the particular lambda is
deduplicated when returned several times. We know that lambdas are
deduplicated if they don't capture anything. The output of this code in
Java 24 and Java 25 is the following:
Deduplicated: empty
Deduplicated: simple
Deduplicated: withRunnableInside
Not deduplicated: withAnonymousRunnableInside
Not deduplicated: withLocalClassInside
Deduplicated: withLocalClassInsideNoInstance
(side note: in Java 23 and below, withLocalClassInsideNoInstance is Not
deduplicated)
The problem is that lambdas returned from withAnonymousRunnableInside and
withLocalClassInside capture 'this', thus not deduplicated. For example,
here's the generated bytecode for withLocalClassInside:
private void lambda$withLocalClassInside$0();
descriptor: ()V
flags: (0x1002) ACC_PRIVATE, ACC_SYNTHETIC
Code:
stack=3, locals=1, args_size=1
0: new #73 // class
com/example/CapturingLambda$1Local
3: dup
4: aload_0
5: invokespecial #75 // Method
com/example/CapturingLambda$1Local."<init>":(Lcom/example/CapturingLambda;)V
8: pop
9: return
So 'this' is passed to the generated constructor of the local class
CapturingLambda$1Local. However, inside the constructor, the only thing we
do with this parameter is check it against null:
com.example.CapturingLambda$1Local(com.example.CapturingLambda);
descriptor: (Lcom/example/CapturingLambda;)V
flags: (0x0000)
Code:
stack=2, locals=2, args_size=2
0: aload_1
1: dup
2: invokestatic #1 // Method
java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
5: pop
6: pop
7: aload_0
8: invokespecial #7 // Method
java/lang/Object."<init>":()V
11: return
Is it really necessary? I understand that the mandated parameter can be
probably helpful to maintain the (unspecified) binary compatibility with
pre-Java-18 class files not to break the reflection code. However, can we
probably omit the null-check and pass null here? This would help to
deduplicate lambda instances more and remove the unnecessary strong
reference to the outer object. Currently, the lambda cannot outlive the
enclosing object, and this looks sad. What do you think?
With best regards,
Tagir Valeev
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/compiler-dev/attachments/20251113/94bbcfce/attachment.htm>
More information about the compiler-dev
mailing list