javadesign-patternslambdainitializationjava-11

Java does not allow using private final class member in lambda under certain pattern


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();
    }
}

Solution

  • 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.