13

I am executing below in main method:

int secrete = 42;
for (int i = 0; i < 5; i++) {
    Consumer<String> myprinter2 =
                    msg -> {
                        System.out.println("consuming " + msg + " ," + secrete);
                    };
     myprinter2.accept(myprinter2.toString());
}

The output for the above code is:

consuming Main$$Lambda$1/1324119927@6d311334 ,42
consuming Main$$Lambda$1/1324119927@682a0b20 ,42
consuming Main$$Lambda$1/1324119927@3d075dc0 ,42
consuming Main$$Lambda$1/1324119927@214c265e ,42
consuming Main$$Lambda$1/1324119927@448139f0 ,42

If I change secrete to be final, then the output is:

consuming Main$$Lambda$1/2003749087@41629346 ,42
consuming Main$$Lambda$1/2003749087@41629346 ,42
consuming Main$$Lambda$1/2003749087@41629346 ,42
consuming Main$$Lambda$1/2003749087@41629346 ,42
consuming Main$$Lambda$1/2003749087@41629346 ,42

secrete is effectively final even if I do not declare it final, so why is each lambda considered a new object when I do not declare it final?

akuzminykh
  • 4,522
  • 4
  • 15
  • 36
amit sahu
  • 131
  • 2
  • 3
    Interesting observation. I imagine that the difference is that the explicitly final variable can be treated as a compile-time constant, but the effectively final variable cannot. – khelwood Dec 17 '21 at 17:06
  • 1
    Also interesting, if you move `final int secrete` into the `for` loop, you get the second output as well. – QBrute Dec 17 '21 at 17:12
  • 1
    @khelwood Your theory does not make sense to me. The compiler is the one who decides what is effectively final, is it not? So, if the compiler recognizes effectively final, then I would think the compiler should be able to treat explicitly final and effectively final the same. (Not that I have any idea of what is going on in this code.) – Basil Bourque Dec 17 '21 at 17:13
  • 3
    @BasilBourque Well, effectively final means that the variable doesn't change after initialisation, not that it necessarily is a compile-time constant. In this case it could be made one, but maybe the compiler doesn't look for this specific set of circumstances. But maybe someone has more technical insight. – khelwood Dec 17 '21 at 17:33
  • 2
    @BasilBourque there is no technical obstacle in treating final and effectively final the same, but as explained in [this answer](https://stackoverflow.com/a/50194401/2711488) the specification does not consider effectively final variable compile-time constants. So whenever formal semantics matter, the compiler must not treat them like compile-time constants. In this specific example, it does not matter, [as explained here](https://stackoverflow.com/a/27524543/2711488) it’s an implementation detail. However, optimizing this case would be a special case, making the compiler more complex. – Holger Dec 20 '21 at 18:29
  • @Holger Very interesting, thank you. I suppose the take-away is that if you want `final` behavior, mark it explicitly. – Basil Bourque Dec 20 '21 at 19:14

1 Answers1

2

"Effectively final" is not technically required, it could've been done without. But the language designers put this restriction to avoid confusion, because if the variable kept changing, what value would the lambda see, the initial, or the latest? Other languages that have lambda don't have this restriction, and the spec sets the expectation for this use case.

Given the following code:

import java.util.function.Consumer;


class Main {  
  public static void main(String args[]) { 
    int i = 42;
    final int j = 41;
    for (int k = 0; k < 5; k++) {
      Consumer<String> x = msg -> System.out.printf("x=%s, i=%d%n", msg, i);
      Consumer<String> y = msg -> System.out.printf("y=%s, j=%d%n", msg, j);
      Consumer<String> z = msg -> System.out.printf("z=%s%n", msg);
      x.accept(x.toString());
      y.accept(y.toString());
      z.accept(z.toString());
    }
  } 
}

When we inspect the generated bytecode with javap -c -v Main.class, we see:

11: invokedynamic #7,  0              // InvokeDynamic #0:accept:(I)Ljava/util/function/Consumer;
16: astore_3
17: invokedynamic #11,  0             // InvokeDynamic #1:accept:()Ljava/util/function/Consumer;
22: astore        4
24: invokedynamic #14,  0             // InvokeDynamic #2:accept:()Ljava/util/function/Consumer;

We can see how the lambdas are translated. The corresponding static methods show that the first lambda is a capturing lambda and has an integer 1st parameter (#71) after translation, while the other ones don't.

BootstrapMethods:
  0: #63 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #70 (Ljava/lang/Object;)V
      #71 REF_invokeStatic Main.lambda$main$0:(ILjava/lang/String;)V
      #74 (Ljava/lang/String;)V
  1: #63 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #70 (Ljava/lang/Object;)V
      #75 REF_invokeStatic Main.lambda$main$1:(Ljava/lang/String;)V
      #74 (Ljava/lang/String;)V
  2: #63 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #70 (Ljava/lang/Object;)V
      #78 REF_invokeStatic Main.lambda$main$2:(Ljava/lang/String;)V
      #74 (Ljava/lang/String;)V

So, it's just how lambdas are translated. You can find more details in this article.

Abhijit Sarkar
  • 21,927
  • 20
  • 110
  • 219