Java Final变量会有默认值吗?

82

我有这样一个程序:

class Test {

    final int x;

    {
        printX();
    }

    Test() {
        System.out.println("const called");
    }

    void printX() {
        System.out.println("Here x is " + x);
    }

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

}

如果我尝试执行它,我会得到编译器错误:变量 x 可能未被初始化。根据 Java 的默认值,我应该得到以下输出结果,对吗?
"Here x is 0".

最终变量会有默认值吗?

如果我将代码更改为以下内容:

class Test {

    final int x;

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

    Test() {
        System.out.println("const called");
    }

    void printX() {
        System.out.println("Here x is " + x);
    }

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

}

我得到的输出是:
Here x is 0                                                                                      
Here x is 7                                                                                     
const called

能否有人解释一下这种行为。。

8个回答

62

http://docs.oracle.com/javase/tutorial/java/javaOO/initial.html,章节“初始化实例成员”:

Java编译器将初始化块复制到每个构造函数中。

也就是说:

{
    printX();
}

Test() {
    System.out.println("const called");
}

行为与以下完全相同:

Test() {
    printX();
    System.out.println("const called");
}

正如您所看到的,一旦创建了一个实例,最终字段就没有被确定分配,而根据http://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.3.1.2

在声明它的类的每个构造函数结束时,必须明确分配空白的最终实例变量;否则会发生编译时错误。

虽然文档中似乎没有明确说明(至少我没有找到),但是一个最终字段必须在构造函数结束前临时采用其默认值,以便在分配之前读取它时具有可预测的值

默认值:http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5

在您的第二个片段中,x在实例创建时初始化,因此编译器不会报错:

Test() {
    printX();
    x = 7;
    printX();
    System.out.println("const called");
}

请注意,以下方法不起作用。只有通过方法才允许使用final变量的默认值。
Test() {
    System.out.println("Here x is " + x); // Compile time error : variable 'x' might not be initialized
    x = 7;
    System.out.println("Here x is " + x);
    System.out.println("const called");
}

1
在你的一个例子中,值得注意的是隐式(或显式)调用super()的位置。 - Patrick
2
这并没有回答为什么不初始化 final 字段会导致编译错误。 - justhalf
@sp00m 不错的参考资料 - 我会记住的。 - Bohemian
2
@justhalf 的回答漏了一个重要的点。你可以通过方法访问默认状态下的 final,但如果你在构建过程结束前没有初始化它,编译器将会报错。这就是为什么第二次尝试成功了(它实际上对 x 进行了初始化),而第一次尝试没有成功。如果你尝试直接访问空白的 final,编译器也会报错。 - Luca

28

JLS表明,在构造函数(或初始化块,这两者基本相同)中必须为空白final实例变量分配默认值。这就是为什么你在第一个案例中会出现错误的原因。然而,它并没有说你不能在构造函数之前访问它。看起来有点奇怪,但你可以在赋值之前访问它,并查看int类型的默认值为0。

更新。正如@I4mpi所提到的,JLSdefines规定,在任何访问之前,每个值都必须被明确定义

每个局部变量(§14.4)和每个空白final字段(§4.12.4,§8.3.1.2)在其值的任何访问发生时必须具有明确定义的值。

然而,它还有一个与构造函数和字段有关的有趣规则

如果C至少有一个实例初始化程序或实例变量初始化程序,则在显式或隐式超类构造函数调用后,如果V在C的最右侧实例初始化程序或实例变量初始化程序之后为[未]分配,则V为[未]分配。

因此,在第二种情况下,变量x在构造函数开始时已经被赋值,因为它包含在其末尾的赋值语句。


实际上,它确实说在赋值之前不能访问它:“每个局部变量(§14.4)和每个空白的最终字段(§4.12.4、§8.3.1.2)在其值的任何访问发生时必须具有明确定义的值。” - l4mpi
1
它应该是“明确赋值”,但在构造函数方面,这个规则有奇怪的行为。我已经更新了答案。 - udalmik
如果有一种代码方法,根据某些复杂条件,可能会或可能不会读取 final字段,并且该代码在字段被写入之前和之后都可以运行,那么编译器通常无法知道它是否会在字段写入之前实际读取该字段。 - supercat

7
如果你不初始化x,你会得到一个编译时错误,因为x从未被初始化。
x声明为final意味着它只能在构造函数或初始化块中进行初始化(因为编译器将复制此块到每个构造函数中)。
在变量初始化之前打印出0的原因是由于manual中定义的行为(请参见:“默认值”部分):

默认值

当字段声明但未初始化时,不一定需要赋值。编译器将通过合理的默认值设置未初始化的字段。一般来说,这个默认值将是零或null,具体取决于数据类型。然而,依靠这样的默认值通常被认为是不好的编程风格。

以下图表总结了上述数据类型的默认值。

Data Type   Default Value (for fields)
--------------------------------------
byte        0
short       0
int         0
long        0L
float       0.0f
double      0.0d
char        '\u0000'
String (or any object)      null
boolean     false

4

第一个错误是编译器抱怨你有一个final字段,但没有初始化它的代码 - 很简单。

在第二个示例中,您有分配值的代码,但执行顺序意味着您在分配之前和之后都引用了该字段。

任何字段的预分配值都是默认值。


2

类的所有非 final 字段都会初始化为默认值(数值数据类型为 0,布尔值为 false,引用类型(有时称为复杂对象)为 null)。这些字段在构造函数(或实例化代码块)执行之前初始化,无论字段是在构造函数之前还是之后声明。

final 类型的字段没有默认值,必须在类构造函数完成前显式初始化一次。

执行块内部的局部变量(例如方法)没有默认值。这些字段在第一次使用之前必须显式初始化,无论局部变量是否标记为 final。


1
据我所知,编译器将始终将类变量初始化为默认值(即使是 final 变量)。例如,如果您将一个 int 初始化为它自己,这个 int 将被设置为它的默认值 0。请参见下面的示例:
class Test {
    final int x;

    {
        printX();
        x = this.x;
        printX();
    }

    Test() {
        System.out.println("const called");
    }

    void printX() {
        System.out.println("Here x is " + x);
    }

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

上面的代码将会输出以下内容:
Here x is 0
Here x is 0
const called

1
OP的代码中,最终变量x不是静态的。 - JamesB
我可以很容易地修改原始帖子的代码,将其初始化为this.x,结果是一样的。这与它是否为静态无关紧要。 - Michael D.
我建议您删除这里的静态内容,因为看起来您没有阅读 OP 的问题。 - JamesB
如果我从 OP 的代码基线开始,这有帮助吗?就像我说的那样,变量是否为静态并不重要。我的观点是,在将变量初始化为其本身并获取默认值之前,暗示该变量在显式初始化之前会被隐式初始化。 - Michael D.
1
无法编译,因为你试图在第6行访问(直接访问)一个尚未初始化的 final 变量。 - Luca

1
让我简单地解释一下。 final变量需要初始化,这是语言规范要求的。 话虽如此,请注意,在声明时不必初始化。
在对象初始化之前必须对其进行初始化。
我们可以使用初始化块来初始化final变量。现在,初始化块有两种类型:staticnon-static 你使用的是非静态初始化块。因此,当你创建一个对象时,运行时将调用构造函数,进而调用父类的构造函数。
之后,它将调用所有的初始化程序(在你的情况下是非静态初始化程序)。
在你的问题中,case 1:即使在初始化块完成后,final变量仍未初始化,这是编译器会检测到的错误。
case 2中:初始化程序将初始化final变量,因此编译器知道在对象初始化之前,final已经初始化了。因此,它不会抱怨。
现在的问题是,为什么x取零。原因在于编译器已经知道没有错误,因此在调用init方法时,所有finals将被初始化为默认值,并设置一个标志,表示它们可以在实际赋值语句(例如x=7)中更改。 请参见以下init调用:

enter image description here


1
如果我尝试执行它,我会得到编译错误:基于Java默认值,变量x可能尚未初始化。 我应该得到以下输出,对吗?“这里x是0”。不是的。首先你会得到一个编译时错误,因此你看不到该输出。最终变量确实会获得默认值,但Java语言规范(JLS)要求您在构造函数(LE:此处包括初始化块)结束时对其进行初始化,否则您将收到编译时错误,这将防止您的代码被编译和执行。第二个示例符合要求,这就是为什么(1)您的代码编译并且(2)您获得了预期行为的原因。将来请尝试熟悉JLS。没有比Java语言更好的信息来源。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接