0

A private lambda function is accessing a private final class member that is initialised in the class constructor. However, this code pattern compiles with error: variable num might not have been initialized

public class Main {
    private final int num;
    public Main() {num = 7;}
    private java.util.function.Supplier getNum = () -> num;
    public void printNum() {
        System.out.println(getNum.get());
    }
    public static void main(String[] args) {
        Main main = new Main();
        main.printNum();
    }
}

However, the following patterns are fine:

i. Move lambda definition inside a method

public class Main {
    private final int num;
    public Main() {num = 7;}
    private java.util.function.Supplier getNum;
    public void printNum() {
        getNum = () -> num;
        System.out.println(getNum.get());
    }
    public static void main(String[] args) {
        Main main = new Main();
        main.printNum();
    }
}

ii. Local lambda inside a method

public class Main {
    private final int num;
    public Main() {num = 7;}
    public void printNum() {
        java.util.function.Supplier getNum = () -> num;
        System.out.println(getNum.get());
    }
    public static void main(String[] args) {
        Main main = new Main();
        main.printNum();
    }
}

iii. Add this before the class member and cast this to the class name

public class Main {
    private final int num;
    public Main() {num = 7;}
    private java.util.function.Supplier getNum = () -> ((Main) this).num;
    public void printNum() {
        System.out.println(getNum.get());
    }
    public static void main(String[] args) {
        Main main = new Main();
        main.printNum();
    }
}

Could someone please explain what is going on above for each case? If it is compiler specific, I was testing on OpenJDK 11. Thank you.

P.S. a solution so that no this casting while the lambda can be shared among class methods is to also initialise it inside the class constructor

public class Main {
    private final int num;
    private final java.util.function.Supplier getNum;
    public Main() {num = 7; getNum = () -> num;}

    public void printNum() {
        System.out.println(getNum.get());
    }
    public static void main(String[] args) {
        Main main = new Main();
        main.printNum();
    }
}

1 Answer 1

6

The rules specified in Chapter 16 - Definite Assignment of the language specification are relevant here.

An access to its value consists of the simple name of the variable (or, for a field, the simple name of the field qualified by this) occurring anywhere in an expression except as the left-hand operand of the simple assignment operator =.

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

Just from the first paragraph, we can see that the expression ((Main) this).num is not considered an "access" of a blank final field, as far as definite assignment is concerned, and therefore definite assignment analysis does not apply to it. Just writing num, or this.num are "accesses".

We will show that in the first code snippet, num is not definitely assigned before the access in () -> num.

16.9:

Let C be a class, and let V be a blank final non-static member field of C, declared in C. Then:

  • V is definitely unassigned (and moreover is not definitely assigned) before the leftmost instance initializer or instance variable initializer of C.

() -> num is the variable initialiser for the field getNum. This is also the first (leftmost) variable initialiser of Main. Therefore, num is not definitely assigned before () -> num.

Then 16.1.10 says:

If an expression is a lambda expression, then the following rules apply:

  • V is definitely assigned before the expression or block that is the lambda body iff V is definitely assigned before the lambda expression.

Notice that this is "iff", not just "if". As we have established, num is not definitely assigned before the lambda expression, so num is not definitely assigned before the lambda body expression, where the access occurs.


In cases i and ii, the access occurs in a method. 16.2.2 says:

A blank final member field V is definitely assigned (and moreover is not definitely unassigned) before the block that is the body of any method in the scope of V and before the declaration of any class declared within the scope of V.

Therefore, num is already definitely assigned even before the method body. From here you can easily derive that it is definitely assigned before () -> num, and therefore definitely assigned before the access.

Sign up to request clarification or add additional context in comments.

7 Comments

Thanks. So basically they are all working as designed, though not everyone would agree these definitions make sense. For one, ((Main) this).num is definitely accessing the member. And saying a lambda is accessing a class member at the point it is defined rather than at the point it is called is another.
@user1589188 I understand your points, but they obviously have to make a compromise somewhere, or else the compiler would get ridiculously complicated. Many things can be done, but not all of those things are worth the effort.
@user1589188 it perfectly makes sense to say that a lambda accesses the variable at the point it is defined, because nothing stops you from using the lambda after that point. Just consider private java.util.function.Supplier getNum = () -> num; private Object otherVariable = getNum.supply(); The second field declaration is valid as it only accesses an already initialized field. You surely do not expect the compiler to re-check the first field’s declaration at the point of the second. Using a qualified this.num is deliberately suppressing the field initialization check (even without lambda)
@user1589188 You’d be even more confused if language rules were that complicated. I’m not even sure whether you understood that the order you wrote the members in the source code is misleading. You wrote private final int num; public Main() {num = 7;} private java.util.function.Supplier getNum = () -> num; but the placement of the constructor between the field declarations does not change the fact that the field initializers are executed after the super constructor call but before the body of the constructor is executed. In your last code example you swapped the order, again without effect.
@user1589188 just consider that a lambda expression referring to a final field is not a like a method. Rather private java.util.function.Supplier getNum = () -> num; is like private java.util.function.Supplier getNum = new SupplierAlwaysReturing(num); Then, as said before, the rules are not lambda specific. Using new SupplierAlwaysReturing(((Main) this).num); would bypass the check and allow accessing the uninitialized field.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.