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

Why attempt to print uninitialized variable does not always result in an error message

有些人可能会发现它类似于 SO 问题 Will Java Final variables have default values?,但该答案并未完全解决此问题,因为该问题并未直接在实例初始化程序块中打印 x 的值。

当我尝试在实例初始化块中直接打印 x 时出现问题,同时在块结束之前为 x 赋值:

案例一

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

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

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

案例二

我不是直接打印,而是调用一个函数来打印:

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

这会正确编译并给出输出

0
7
hi

这两种情况在概念上有什么区别?

案例一:

给你一个编译错误,

因为在 System.out.println(x);

you are trying to print x which was never initialized.

案例二:

之所以有效,是因为您没有直接使用任何文字值,而是调用了一些正确的方法。

一般规则是,

If you are trying to access any variable which is never initialized then it will give a compilation error.

不同之处在于,在第一种情况下,您从 初始化块 调用 System.out.println,因此在构造函数之前调用的块。第一行

System.out.println(x);

变量 x 尚未初始化,因此您会遇到编译错误。

但在第二种情况下,您调用的实例方法不知道变量是否已经初始化,因此您没有编译错误,您可以看到 x[=14= 的默认值]

好的,这是我的 2 美分。

我们都知道final变量只能在声明时或稍后在构造函数中初始化。牢记这一事实,让我们看看到目前为止发生了什么。

无错误案例:

So when you use inside a method, it have already a value.

 1) If you initialize it, that value.
 2) If not, the default value of data type. 

错误情况:

When you do that in an initialization block, which you are seeing errors.

如果你看 docs of initialization block

{
    // whatever code is needed for initialization goes here
}

The Java compiler copies initializer blocks into every constructor. Therefore, this approach can be used to share a block of code between multiple constructors.

在编译器看来,您的代码实际上等于

class HelloWorld {

    final int x;
    HelloWorld() {
        System.out.println(x);  ------------ ERROR here obviously
        x = 7;
        System.out.println(x);  
        System.out.println("hi");
    }

    public static void main(String[] args) {
        HelloWorld t = new HelloWorld();
    }
}

您甚至在初始化之前就在使用它。

阅读 JLS,答案似乎在 section 16.2.2:

A blank final member field V is definitely assigned (and moreover is not definitely unassigned) before the block (§14.2) 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.

这意味着当一个方法被调用时,final字段在调用它之前被赋值为默认值0,所以当你在方法内部引用它时,它编译成功并打印值0。

但是,当您在方法之外访问该字段时,它被认为是未分配的,因此会出现编译错误。以下代码也不会编译:

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

因为:

  • V is [un]assigned before any other statement S of the block iff V is [un]assigned after the statement immediately preceding S in the block.

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

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

Note that there are no rules that would allow us to conclude that V is definitely unassigned before the block that is the body of any constructor, method, instance initializer, or static initializer declared in C. We can informally conclude that V is not definitely unassigned before the block that is the body of any constructor, method, instance initializer, or static initializer declared in C, but there is no need for such a rule to be stated explicitly.

在 JLS 中,§8.3.3. Forward References During Field Initialization,它指出在以下情况下存在编译时错误:

Use of instance variables whose declarations appear textually after the use is sometimes restricted, even though these instance variables are in scope. Specifically, it is a compile-time error if all of the following are true:

  • The declaration of an instance variable in a class or interface C appears textually after a use of the instance variable;

  • The use is a simple name in either an instance variable initializer of C or an instance initializer of C;

  • The use is not on the left hand side of an assignment;

  • C is the innermost class or interface enclosing the use.

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

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

通过方法访问[静态或实例变量]不会以这种方式检查,所以上面的代码产生输出0,因为[的变量初始值设定项=12=] 使用 class 方法 peek()j 被它的变量初始化器初始化之前访问变量 j 的值,此时它仍然有它的默认值 (§4.12.5 Initial Values of Variables).

因此,总而言之,您的第二个示例编译并执行得很好,因为编译器不会在您调用 printX()printX() 时检查 x 变量是否已经初始化实际上发生在运行时,x 变量将被赋予其默认值 (0)。

我们在这里处理初始化块。 Java 编译器将初始化程序块复制到每个构造函数中。

第二个例子没有出现编译错误,因为在另一个Frame中打印x,请参考规范