4

I can't understand why the implementers of the java language made it so that the variable used in a lambda and passed there from a function scope has to be final.

I decompiled this code:

public class Main {
    @FunctionalInterface
    interface Test {
        void method(int t);
    }

    static void test(Test t) {
        t.method(3);
    }

    public static void main(String... args) {
        int a = 3;
        test((i)-> {
            System.out.println(a + i);
        });
    }
}

and what the compiler does is copy that variable as if it was passed through a constructor. I got these 3 classes:

1:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import Main.1;
import Main.Test;

public class Main {
    public Main() {
    }

    static void test(Test var0) {
        var0.method(3);
    }

    public static void main(String... var0) {
        byte var1 = 3;
        test(new 1(var1));
    }
}

2:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import Main.Test;

final class Main$1 implements Test {
    Main$1(int var1) {
        this.val$a = var1;
    }

    public void method(int var1) {
        System.out.println(this.val$a + var1);
    }
}

3:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

@FunctionalInterface
interface Main$Test {
    void method(int var1);
}

Why couldn't the implementers just copy the variable regardless of whether it was modified or not so we could do this:

public class Main {
    @FunctionalInterface
    interface Test {
        void method(int t);
    }

    static void test(Test t) {
        t.method(3);
    }

    public static void main(String... args) {
        int a = 3;
        test((i)-> {
            a += 1; // allow modification, a is copied anyway, why not?
            System.out.println(a + i);
        });
    }
}
Coder-Man
  • 2,391
  • 3
  • 11
  • 19
  • The reason is to simplified the code and prevent the complexity of concurrency. You declare a value that is used in a lambda that could be executed anytime, hard to follow so it was a good choice to force it to be final. And this is not new, this was already the case for anonymous inner class by the way. And effectively final are just a syntaxical sugar (that I don't see the need) since it style need to be final in the end. So this is simpler to declare it final and prevent any complication during an update. – AxelH May 25 '18 at 12:38
  • See [Vince Emigh answer about the JLS](https://stackoverflow.com/a/50341404/4391450) pointing the reason. – AxelH May 25 '18 at 12:41

1 Answers1

9

There's no technical reason for this. It's just that if you allow non-final fields to be used in a lambda then you could write code that looks fine, but actually doesn't work.

For example:

void printSum(Collection<Integer> numbers) {
  int sum = 0;
  numbers.forEach(i -> sum += i);
  System.out.println(sum);
}

Currently the compiler won't let you do that, since you can't access the non-final sum inside the lambda. As you noted the variable gets copied into the lambda anyway, so it could allow that.

If it did, then this code would compile, but always print 0, since only the sum-copy inside the lambda is modified and not the "real one".

Allowing only "effectively final" variables to be referenced is a good compromise of not requiring the final keyword everywhere while still avoiding misunderstandings like that.

Joachim Sauer
  • 302,674
  • 57
  • 556
  • 614
  • >> But actually doesn't work. Why does it not work? – Coder-Man May 25 '18 at 12:47
  • 1
    @POrekhov: it doesn't work since `sum` inside the lambda is actually a copy of the `sum` in the method (as you've proven) and changes to the lambda's own copy of `sum` will not propagate back out to the original method. So if you access `sum` inside the original method after supposedly modifying it in the lambda, then you'll disappointingly note that its value is still 0. – Joachim Sauer May 25 '18 at 12:48
  • I know. I think you didn't understand my question. I don't want that change to propagate back to the original function. – Coder-Man May 25 '18 at 12:53
  • 1
    @POrekhov: I think I understood that. If you allow modifying variables in the lambda and *don't* propagate it back (which would be easily possible), then that would allow code like this to compile which wouldn't work without any obvious diagnostic messages to the user as to what is wrong. Disallowing modifications of "shared" variables altogether makes this problem go away without any major drawback. – Joachim Sauer May 25 '18 at 12:55
  • >>Then that would allow code like this to compile which wouldn't work without any obvious diagnostic messages to the user as to what is wrong. What diagnostic messages? Please provide an example. I can debug the lambda and see its value, what's wrong here? – Coder-Man May 25 '18 at 12:56
  • 1
    @POrekhov: look at it this way: if you allow modification of variables *without* propagating the values back to the "original" then you end up with two variables even though you only defined one. That is confusing and non-obvious, so they've decided against it. – Joachim Sauer May 25 '18 at 12:58
  • oh, I got it, it's one of those things you just gotta accept and learn how it works, and not ask questions why it works this way, lol. Thanks. – Coder-Man May 25 '18 at 13:00
  • 1
    @POrekhov: I disagree with the "not ask questions" part here. There is a reason, it's just more about understanability and simplicity of the code and its behaviour than about strong technical reasons. – Joachim Sauer May 25 '18 at 13:00
  • 1
    Note that there are *three* `sum` variables. The local variable of the surrounding context, the `final` instance field of the generated class and the parameter variable of the synthetic method holding the lambda’s body. Allowing to change `sum` in the lambda’s body would only change the synthetic method’s parameter variable, not even the instance field holding the captured value. So `sum` would again be `0` for each element of the collection. That’s even less intuitive than not propagating it back to the surrounding context. – Holger May 25 '18 at 14:08