3

I don't understand this behavior.

This piece of code complies:

public class A {

    private String s;

    private Function<String, String> f = e -> s;

    public A(String s) {
        this.s = s;
    }
}

But if I make s final, then I get a compiler error:

public class A {

    private final String s;

    private Function<String, String> f = e -> s; // Variable 's' might not have been initialized

    public A(String s) {
        this.s = s;
    }
}

Why is that? If it were the other way around, I'd understand, but how is that the compiler complains when I declare a field final (which forces me to initialize its value in the constructor), and it's OK for it when it's not final?

carcaret
  • 3,238
  • 2
  • 19
  • 37

3 Answers3

13

It has nothing to do with the lambda, this example has the same error:

public class Test {
    private final String a;
    private String b = a; // // Variable 'a' might not have been initialized

    public Test(String a) {
        this.a = a;
    }
}

It's because the initialization at the place of declaration is executed before the constructor. Therefore, at the place of the declaration of b, a is still not initialized.

It's clear when you use this example:

public class Test {
    private String a = "init";
    private String b = a;

    public Test(String a) {
        this.a = a;
    }

    public static void main(String[] args) {
        System.out.println(new Test("constructor").b);
    }
}

When you run it, it prints "init" (the value to which field a was originally assigned) and not "constructor" because the initialization of b took place before running the constructor.

The lambda from your example makes the it a bit more convoluted, because we could expect that since the access to a is deferred, it would be OK with the compiler, but apparently the compiler just follows the general rule of "don't access the variable before it's initialized".

You can bypass it using an accessor method:

public class Test {
    private final String a;
    private String b = getA(); // allowed now, but not very useful
    private Function<String, String> f = e -> getA(); // allowed now and evaluated at the time of execution of the function

    public Test(String a) {
        this.a = a;
    }

    public static void main(String[] args) {
        System.out.println(new Test("constructor").b); // prints "null"
        System.out.println(new Test("constructor").f.apply("")); // prints "constructor"
    }

    public String getA() {
        return a;
    }
}
Adam Michalik
  • 9,678
  • 13
  • 71
  • 102
  • 4
    It’s a fundamental property of the lambda expressions to work like expressions in the surrounding context, so the rules regarding access to blank final variables are the same. This is different to inner classes. Note that instead of calling `getA()` you can also circumvent it by using `Test.this.a` to access `a`. – Holger Jul 12 '17 at 12:41
  • 1
    @Holger Thanks, the answers you linked to are very interesting and clear. Interesting that accessing `a` via `a` and `this.a` are errors, but `Test.this.a` is not an error in case of the lambda, but still an error in case of assignment to `b`. – Adam Michalik Jul 12 '17 at 12:57
  • 3
    I tried `b = Test.this.a;` with almost every JDK 8 & 9 and it worked. [The specification](https://docs.oracle.com/javase/specs/jls/se8/html/jls-16.html) clearly refers to “*the simple name of the variable (or, for a field, the simple name of the field qualified by `this`)*” when prohibiting access to a blank final variable. Did you use Eclipse? – Holger Jul 12 '17 at 13:05
  • 2
    Looks like it's IntelliJ's extra check. Compiling `b = Test.this.a` with raw Oracle javac 1.8.0_131 works fine. – Adam Michalik Jul 12 '17 at 13:20
4

A non final member variable will always be initialized (since it has a default value - null in the case of your String variable), so there's no chance of it being uninitialized.

On the other hand, a final variable may only be initialized once, so I'm assuming it is not initialized to its default value.

The closest related thing I found is in JLS 4.12.4.:

4.12.4. final Variables

A variable can be declared final. A final variable may only be assigned to once. It is a compile-time error if a final variable is assigned to unless it is definitely unassigned immediately prior to the assignment

I assume we can understand this last sentence to mean that a final variable is not assigned a default value, since otherwise you'll get a compile time error at this.s = s;.

A better JLS reference (thanks to Holger) is JLS 16:

Chapter 16. Definite Assignment

For every access of a local variable or blank final field x, x must be definitely assigned before the access, or a compile-time error occurs.

The rational behind this requirement is that (in your example) the lambda expression could be invoked before s is initialized:

public A(String s) {
    String v = f.apply("x"); // this.s is not initialized at this point
                             // so it can't be accessed
    this.s = s;
}

Note that you can initialize the lambda expression in the constructor after initializing the final variable (I changed the name of the argument to be different than the member variable, so that the lambda expression won't grab that local variable):

public A(String so) {
    // f = e -> s; // Error: The blank final field s may not have been initialized
    this.s = so;
    f = e -> s; // works fine
}
Community
  • 1
  • 1
Eran
  • 387,369
  • 54
  • 702
  • 768
  • 4
    [JLS §16](https://docs.oracle.com/javase/specs/jls/se8/html/jls-16.html) is the right place: “**For every access of a local variable or blank `final` field `x`, `x` must be definitely assigned before the access, or a compile-time error occurs.**” – Holger Jul 12 '17 at 13:28
0

This also possible way to do

 public class A {

   private final String s;
   private Function<String, String> f;

   public A(String s) {
    this.s = s;
    this.f = e -> s;
   }
}
janith1024
  • 1,042
  • 3
  • 12
  • 25