<html><body><div style="font-family: arial, helvetica, sans-serif; font-size: 12pt; color: #000000"><div><br></div><div><br></div><hr id="zwchr" data-marker="__DIVIDER__"><div data-marker="__HEADERS__"><blockquote style="border-left:2px solid #1010FF;margin-left:5px;padding-left:5px;color:#000;font-weight:normal;font-style:normal;text-decoration:none;font-family:Helvetica,Arial,sans-serif;font-size:12pt;"><b>From: </b>"Chen Liang" <chen.l.liang@oracle.com><br><b>To: </b>"Archie Cobbs" <archie.cobbs@gmail.com>, "Tagir Valeev" <amaembo@gmail.com><br><b>Cc: </b>"compiler-dev" <compiler-dev@openjdk.org><br><b>Sent: </b>Friday, November 14, 2025 4:15:47 PM<br><b>Subject: </b>Re: Lambda with anonymous or local class inside unnecessarily captures 'this'<br></blockquote></div><div><style style="display:none;"> P {margin-top:0;margin-bottom:0;} </style></div><div data-marker="__QUOTED_TEXT__"><blockquote style="border-left:2px solid #1010FF;margin-left:5px;padding-left:5px;color:#000;font-weight:normal;font-style:normal;text-decoration:none;font-family:Helvetica,Arial,sans-serif;font-size:12pt;">


<div style="font-family: "Calibri Light", "Helvetica Light", sans-serif; font-size: 12pt; color: rgb(0, 0, 0);" class="elementToProof">
The situation in which non-member nested classes capture the enclosing instance unconditionally can be treated as an implementation artifact of javac; we discussed this within Oracle a while ago. The enclosing instance capture being the leading parameter and
 the null check are for linkage, but local and anonymous classes do not have such linkage links, and it should be JLS-compliant that unused enclosing instance capture is dropped altogether. There are local and anonymous classes in early construction context
 and static context that don't perform enclosing instance capture, yet reflectively they are not distinguished from the ones that do, so I believe the compatibility impact is low.</div>
<div style="font-family: "Calibri Light", "Helvetica Light", sans-serif; font-size: 12pt; color: rgb(0, 0, 0);" class="elementToProof">
<br>
</div>
<div style="font-family: "Calibri Light", "Helvetica Light", sans-serif; font-size: 12pt; color: rgb(0, 0, 0);" class="elementToProof">
Re Archie: I think Tagir means that for 23 and below, the lambdas with local or anonymous classes are still captured. It is interesting withLocalClassInsideNoInstance() is suddenly deduplicated.</div>
<div style="font-family: "Calibri Light", "Helvetica Light", sans-serif; font-size: 12pt; color: rgb(0, 0, 0);" class="elementToProof">
<br>
</div>
<div style="font-family: "Calibri Light", "Helvetica Light", sans-serif; font-size: 12pt; color: rgb(0, 0, 0);" class="elementToProof">
P.S. "Deduplication" is a confusing term - in javac I believe it refers to identical lambda expressions reusing the same InvokeDynamic constant (which still generates different call sites per JVMS rules), but in this thread it refers to the singleton lambda
 instances.</div></blockquote><div><br></div><div>About deduplication, the term is usually used when you do not control the creation of the objects but you want later to have some sharing.</div><div>In the case of javac, javac controls the objects representing the constant pool constants, so it's just sharing.</div><div><br data-mce-bogus="1"></div><blockquote style="border-left:2px solid #1010FF;margin-left:5px;padding-left:5px;color:#000;font-weight:normal;font-style:normal;text-decoration:none;font-family:Helvetica,Arial,sans-serif;font-size:12pt;">
<div style="font-family: "Calibri Light", "Helvetica Light", sans-serif; font-size: 12pt; color: rgb(0, 0, 0);" class="elementToProof">
<br>
</div>
<div style="font-family: "Calibri Light", "Helvetica Light", sans-serif; font-size: 12pt; color: rgb(0, 0, 0);" class="elementToProof">
-Chen</div></blockquote><div><br></div><div>regards,</div><div>Rémi</div><div><br data-mce-bogus="1"></div><blockquote style="border-left:2px solid #1010FF;margin-left:5px;padding-left:5px;color:#000;font-weight:normal;font-style:normal;text-decoration:none;font-family:Helvetica,Arial,sans-serif;font-size:12pt;">

<hr style="display:inline-block;width:98%">
<div id="divRplyFwdMsg" dir="ltr"><font face="Calibri, sans-serif" style="font-size:11pt" color="#000000"><b>From:</b> compiler-dev <compiler-dev-retn@openjdk.org> on behalf of Archie Cobbs <archie.cobbs@gmail.com><br><b>Sent:</b> Thursday, November 13, 2025 3:42 PM<br><b>To:</b> Tagir Valeev <amaembo@gmail.com><br><b>Cc:</b> compiler-dev@openjdk.org <compiler-dev@openjdk.org><br><b>Subject:</b> Re: Lambda with anonymous or local class inside unnecessarily captures 'this'</font>
<div> </div>
</div>
<div>
<div dir="ltr">
<div>Hi Tagir,</div>
<div><br>
</div>
<div>This is speculation here...</div>
<div><br>
</div>
<div>Possibly the new behavior is a side effect of <a href="https://bugs.openjdk.org/browse/JDK-8164714" target="_blank">
JDK-8164714</a> (<a href="https://github.com/openjdk/jdk/pull/23875/files" target="_blank">pr #23875</a>) which ensures outer instances get provided as parameters, even if they are not otherwise used, so they can be checked non-null.</div>
<div><br>
</div>
<div>I believe that parameter would later get detected by <a href="https://github.com/openjdk/jdk/blob/6322aaba63b235cb6c73d23a932210af318404ec/src/java.base/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java#L226-L232" target="_blank">
this logic</a> in <span style="font-family:monospace">InnerClassLambdaMetafactory</span>, which would cause it to not deduplicate the lambda instance.</div>
<div><br>
</div>
<div>If the above is true, then to fix this, one could imagine that the metafactory logic might be made smarter, so it could filter out such "null check only" captured outer instances. I'm not sure exactly how that would be detectable... maybe by adding a new
 boolean BSM flag?</div>
<div><br>
</div>
<div>-Archie</div>
</div>
<br>
<div class="x_gmail_quote">
<div dir="ltr" class="x_gmail_attr">On Thu, Nov 13, 2025 at 7:00 AM Tagir Valeev <<a href="mailto:amaembo@gmail.com" target="_blank">amaembo@gmail.com</a>> wrote:<br>
</div>
<blockquote class="x_gmail_quote" style="margin:0px 0px 0px 0.8ex; border-left:1px solid rgb(204,204,204); padding-left:1ex">
<div dir="ltr">Hello!<br>
<div><br>
</div>
<div>I was playing with lambda instance deduplication and come up with the following set of test cases:</div>
<div><br>
</div>
<div>import java.util.function.Supplier;<br>
<br>
public final class CapturingLambda {<br>
    Runnable empty() {<br>
        return () -> {};<br>
    }<br>
<br>
    Runnable simple() {<br>
        return () -> {<br>
            System.out.println("simple");<br>
        };<br>
    }<br>
<br>
    Runnable withRunnableInside() {<br>
        return () -> {<br>
            Runnable r = () -> {};<br>
            r.run();<br>
        };<br>
    }<br>
<br>
    Runnable withAnonymousRunnableInside() {<br>
        return () -> {<br>
            Runnable r = new Runnable() {<br>
                @Override<br>
                public void run() {<br>
                }<br>
            };<br>
            r.run();<br>
        };<br>
    }<br>
<br>
    Runnable withLocalClassInside() {<br>
        return () -> {<br>
            class Local {}<br>
            new Local();<br>
        };<br>
    }<br>
<br>
    Runnable withLocalClassInsideNoInstance() {<br>
        return () -> {<br>
            class Local {}<br>
        };<br>
    }<br>
<br>
    void checkDeduplicated(String name, Supplier<Runnable> lambdaSupplier) {<br>
        Runnable lambda1 = lambdaSupplier.get();<br>
        Runnable lambda2 = lambdaSupplier.get();<br>
        if (lambda1 == lambda2) {<br>
            System.out.println("Deduplicated: "+name);<br>
        } else {<br>
            System.out.println("Not deduplicated: "+name);<br>
        }<br>
    }<br>
<br>
    private void test() {<br>
        checkDeduplicated("empty", this::empty);<br>
        checkDeduplicated("simple", this::simple);<br>
        checkDeduplicated("withRunnableInside", this::withRunnableInside);<br>
        checkDeduplicated("withAnonymousRunnableInside", this::withAnonymousRunnableInside);<br>
        checkDeduplicated("withLocalClassInside", this::withLocalClassInside);<br>
        checkDeduplicated("withLocalClassInsideNoInstance", this::withLocalClassInsideNoInstance);<br>
    }<br>
<br>
    public static void main(String[] args) {<br>
        new CapturingLambda().test();<br>
    }<br>
}<br>
</div>
<div><br>
</div>
<div>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:</div>
<div><br>
</div>
<div>Deduplicated: empty<br>
Deduplicated: simple<br>
Deduplicated: withRunnableInside<br>
Not deduplicated: withAnonymousRunnableInside<br>
Not deduplicated: withLocalClassInside<br>
Deduplicated: withLocalClassInsideNoInstance</div>
<div><br>
</div>
<div>(side note: in Java 23 and below, withLocalClassInsideNoInstance is Not deduplicated)</div>
<div><br>
</div>
<div>The problem is that lambdas returned from withAnonymousRunnableInside and withLocalClassInside capture 'this', thus not deduplicated. For example, here's the generated bytecode for withLocalClassInside:</div>
<div><br>
</div>
<div>  private void lambda$withLocalClassInside$0();<br>
    descriptor: ()V<br>
    flags: (0x1002) ACC_PRIVATE, ACC_SYNTHETIC<br>
    Code:<br>
      stack=3, locals=1, args_size=1<br>
         0: new           #73                 // class com/example/CapturingLambda$1Local<br>
         3: dup<br>
         4: aload_0<br>
         5: invokespecial #75                 // Method com/example/CapturingLambda$1Local."<init>":(Lcom/example/CapturingLambda;)V<br>
         8: pop<br>
         9: return<br>
</div>
<div><br>
</div>
<div>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:</div>
<div><br>
</div>
<div>  com.example.CapturingLambda$1Local(com.example.CapturingLambda);<br>
    descriptor: (Lcom/example/CapturingLambda;)V<br>
    flags: (0x0000)<br>
    Code:<br>
      stack=2, locals=2, args_size=2<br>
         0: aload_1<br>
         1: dup<br>
         2: invokestatic  #1                  // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;<br>
         5: pop<br>
         6: pop<br>
         7: aload_0<br>
         8: invokespecial #7                  // Method java/lang/Object."<init>":()V<br>
        11: return<br>
</div>
<div><br>
</div>
<div>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?</div>
<div><br>
</div>
<div>With best regards,</div>
<div>Tagir Valeev</div>
</div>
</blockquote>
</div>
<div><br clear="all">
</div>
<br>
<span class="x_gmail_signature_prefix">-- </span><br>
<div dir="ltr" class="x_gmail_signature">Archie L. Cobbs<br>
</div>
</div><br></blockquote></div></div></body></html>