为什么尝试打印未初始化的变量并不总是导致错误消息

Ash*_*hul 55 java final initialization

有些人可能会发现它类似于SO问题Java Final变量是否具有默认值?但是这个答案并没有完全解决这个问题,因为这个问题并没有直接在实例初始化程序块中打印x的值.

当我尝试在实例初始化程序块内直接打印x时,问题出现了,同时在块结束之前为x分配了一个值:

情况1

class HelloWorld {

    final int x;

    {
        System.out.println(x);
        x = 7;
        System.out.println(x);    
    }

    HelloWorld() {
        System.out.println("hi");
    }

    public static void main(String[] args) {
        HelloWorld t = new HelloWorld();
    }
}
Run Code Online (Sandbox Code Playgroud)

这给出了编译时错误,指出变量x可能尚未初始化.

$ javac HelloWorld.java
HelloWorld.java:6: error: variable x might not have been initialized
        System.out.println(x);
                           ^
1 error
Run Code Online (Sandbox Code Playgroud)

案例2

我没有直接打印,而是调用一个打印功能:

class HelloWorld {

    final int x;

    {
        printX();
        x = 7;
        printX();
    }

    HelloWorld() {
        System.out.println("hi");
    }

    void printX() {
        System.out.println(x);
    }

    public static void main(String[] args) {
        HelloWorld t = new HelloWorld();
    }
}
Run Code Online (Sandbox Code Playgroud)

这正确编译并提供输出

0
7
hi
Run Code Online (Sandbox Code Playgroud)

这两种情况之间的概念差异是什么?

Kon*_*kov 32

在JLS中,§8.3.3.前向引用在字段初始化期间,它声明在以下情况下存在编译时错误:

使用在使用后以声明方式显示声明的实例变量有时会受到限制,即使这些实例变量在范围内也是如此.具体来说,如果满足以下所有条件,则为编译时错误:

  • 在使用实例变量之后,类或接口C中的实例变量的声明以文本形式出现;

  • 在C的实例变量初始值设定项或C的实例初始值设定项中使用是一个简单的名称;

  • 使用不在作业的左侧;

  • C是封闭使用的最内层类或接口.

以下规则附带一些示例,其中最接近您的示例是:

class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}
Run Code Online (Sandbox Code Playgroud)

通过方法访问[到静态或实例变量] 不会以这种方式检查,因此上面的代码产生输出0,因为变量初始化器i使用类方法peek()访问变量的值j之前j已由其变量初始化程序初始化,at它仍然有其默认值(§4.12.5变量的初始值).

因此,总而言之,您的第二个示例编译并执行正常,因为编译器x在您调用printX()时不检查变量是否已经初始化,并且printX()在运行时实际发生x变量时,将为变量分配其默认值(0).

  • 我仍然很困惑你所提出的JLS的引用如何适用于这个问题.在这两种情况下,"x"的声明在使用之前以文本形式出现. (3认同)

Tun*_*aki 12

阅读JLS,答案似乎在第16.2.2节:

空白final成员字段V在块(第14.2节)之前是明确分配的(并且绝对不是未分配的),该块是在范围内V和在范围内声明的任何类的声明范围内的任何方法的主体V.

这意味着当调用方法时,在调用方法之前将最终字段分配给其默认值0,因此当您在方法内引用它时,它会成功编译并打印值0.

但是,当您访问方法之外的字段时,它被视为未分配,因此编译错误.以下代码也将无法编译:

public class Main {
    final int x;
    {
        method();
        System.out.println(x);
        x = 7;
    }
    void method() { }
    public static void main(String[] args) { }
}
Run Code Online (Sandbox Code Playgroud)

因为:

  • VS在块的任何其他语句之前是[un]赋值iff V在紧接S在块之前的语句之后被[un]赋值.

由于最终字段x在方法调用之前未分配,因此在它之后仍然未分配.

JLS中的这个注释也是相关的:

请注意,没有任何规则可以让我们得出结论,V在块之前肯定是未分配的,该块是在其中声明的任何构造函数,方法,实例初始化程序或静态初始化程序的主体C.我们可以非正式地断定V在块之前没有明确地未分配,该块是在C中声明的任何构造函数,方法,实例初始化器或静态初始化器的主体,但是不需要明确声明这样的规则.

  • 字节码表明您的答案是正确的.在`printX()`方法中,使用当前实例上的`getField`读取值"0".在调用`printX()`之前,调用invoke并将所有字段初始化为其默认值.我想你和Kocko都会回答这个问题的一部分*两部分答案*:) (2认同)