最终的定义不正确吗?


186

首先,一个难题:以下代码显示什么?

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10);

    private static long scale(long value) {
        return X * value;
    }
}

回答:

0

扰流板如下。


如果您打印X的规模(长),并重新定义X = scale(10) + 3,印刷品会X = 0那么X = 3。这意味着X暂时设置为0,后来又设置为3。这是违反final

静态修饰符与最终修饰符结合使用,还可以定义常量。最后的修饰符指示此字段的值不能更改

来源:https : //docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [添加了重点]


我的问题:这是一个错误吗?是final不明确的?


这是我感兴趣的代码。 X被分配了两个不同的值:03。我认为这是对的违反final

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10) + 3;

    private static long scale(long value) {
        System.out.println("X = " + X);
        return X * value;
    }
}

该问题被标记为Java静态最终字段初始化顺序的可能重复项。我相信这个问题不是重复的,因为另一个问题解决了初始化的顺序,而我的问题解决了与final标签结合的循环初始化。仅从另一个问题来看,我将无法理解问题中的代码为什么不会出错。

通过查看ernesto得到的输出,这尤其清楚:当用a标记时final,他得到以下输出:

a=5
a=5

哪个不涉及我的问题的主要部分:final变量如何更改其变量?


17
这种引用X成员的方式就像在超类构造函数完成之前引用子类成员一样,这是您的问题,而不是的定义final
daniu

4
来自JLS:A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
伊万

1
@Ivan,这与常量无关,而与实例变量有关。但是,您可以添加这一章吗?
AxelH

9
请注意:请勿在生产代码中执行任何此操作。如果有人开始利用JLS中的漏洞,这对于每个人来说都是超级困惑的。
Zabuzard

13
仅供参考,您也可以在C#中创建完全相同的情况。C#承诺常量声明中的循环将在编译时捕获,但对只读声明不作这样的承诺,并且在实践中,您可能会遇到由另一个字段初始化程序观察到该字段的初始零值的情况。如果这样做时很痛,请不要这样做。编译器不会保存您。
埃里克·利珀特

Answers:


217

一个非常有趣的发现。要理解它,我们需要深入研究Java语言规范(JLS)。

原因是final只允许一次分配。但是,默认值为no assignment。实际上,每个这样的变量(类变量,实例变量,数组组件)从一开始就在赋值之前指向其默认值。然后,第一个分配更改参考。


类变量和默认值

看下面的例子:

private static Object x;

public static void main(String[] args) {
    System.out.println(x); // Prints 'null'
}

我们没有为明确分配值x,尽管它指向null,这是默认值。将其与§4.12.5进行比较:

变量的初始值

每个类变量,实例变量或数组组件在创建时都会用默认值初始化(第15.9节,第15.10.2节

请注意,这仅适用于此类变量,例如在我们的示例中。它不适用于局部变量,请参见以下示例:

public static void main(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

在同一JLS段落中:

必须在使用局部变量(第14.4节,第14.14节)之前通过初始化(第14.4节)或赋值(第15.26节)为它明确赋一个值,并且可以使用确定赋值规则(第§)进行验证。16(确定分配))。


最终变量

现在final,从§4.12.4看

最终变量

可以将变量声明为final。甲最终变量可以仅分配给一次。如果将最终变量赋值给它,则是编译时错误,除非在赋值之前绝对未赋值最终变量(第16节(确定赋值))。


说明

现在回到您的示例,稍作修改:

public static void main(String[] args) {
    System.out.println("After: " + X);
}

private static final long X = assign();

private static long assign() {
    // Access the value before first assignment
    System.out.println("Before: " + X);

    return X + 1;
}

它输出

Before: 0
After: 1

回想一下我们学到的东西。在方法assign的变量X没有分配的值呢。因此,由于它是类变量,因此它指向其默认值,并且根据JLS,这些变量总是立即指向其默认值(与局部变量相反)。在该assign方法之后,将为变量X分配值,1并且由于final我们无法再对其进行更改。因此,由于以下原因,以下内容将不起作用final

private static long assign() {
    // Assign X
    X = 1;

    // Second assign after method will crash
    return X + 1;
}

JLS中的示例

感谢@Andrew,我找到了一个JLS段落,它完全涵盖了这种情况,它也演示了这一点。

但首先让我们看一下

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

为什么不允许这样做,而从方法进行访问却是不允许的呢?看看第8.3.3节,它讨论了如果尚未初始化字段时何时限制对字段的访问。

它列出了一些与类变量有关的规则:

对于简单地引用f在class或interface中声明的类变量的引用,如果出现以下情况C,则是编译时错误

  • 该引用出现在(§8.7)的类变量初始化器C或静态初始化器中。和C

  • 该引用显示在f自己的声明器的初始化程序中,或者出现在声明器的左侧f;和

  • 该引用不在赋值表达式的左侧(第15.26节);和

  • 包含引用的最里面的类或接口是C

很简单,X = X + 1被这些规则捕获,方法访问不被捕获。他们甚至列出了这种情况并给出了一个例子:

不以这种方式检查方法的访问,因此:

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

因为变量初始化器i使用类方法peek来访问变量的值,j然后j变量初始化器才对其进行了初始化,此时它仍然具有其默认值(第4.12.5节)。


1
@Andrew是的,班级变量,谢谢。是的,它如果有不会是限制此类访问一些额外的规则工作:§8.3.3。看一下为类变量指定的四个点(第一个条目)。OP实例中的方法方法不受这些规则的约束,因此我们可以X从该方法中进行访问。我不会那么介意。这仅取决于JLS如何准确定义要详细工作的内容。我永远不会使用这样的代码,它只是在利用JLS中的一些规则。
Zabuzard

4
问题是您可以从构造函数中调用实例方法,这可能不应该被允许。另一方面,不允许在调用super之前分配本地人,这将是有用且安全的。去搞清楚。
恢复莫妮卡

1
@Andrew您可能是这里唯一真正提到过的人forwards references(也是JLS的一部分)。这是很简单的,没有这个万能的答案stackoverflow.com/a/49371279/1059372
尤金(Eugene)

1
“然后,第一个任务将更改参考。” 在这种情况下,它不是引用类型,而是原始类型。
fabian

1
如果有点长,这个答案是正确的。:-)我认为tl; dr是OP引用的一个教程说“ [final]字段不能更改”,而不是JLS。尽管Oracle的教程相当不错,但它们并没有涵盖所有的极端情况。对于OP的问题,我们需要转到final的实际JLS定义-并且该定义不会使final域的值永远不会改变(OP正确地挑战了)。
yshavit

22

在这里与最终无关。

由于它处于实例或类级别,因此如果未分配任何内容,它将保留默认值。这就是您0在未分配时访问它的原因。

如果您在X未完全分配的情况下访问,它将保留long的默认值(即)0,从而得到结果。


3
棘手的是,如果您不分配值,则不会分配默认值,但是如果您使用它为自己分配“最终”值,它将...
AxelH

2
@AxelH我明白你的意思了。但这就是它应该如何工作的方式,否则世界就会崩溃;)。
Suresh Atta

20

不是错误。

scale从拨打第一个电话时

private static final long X = scale(10);

它试图评估return X * valueX尚未分配值,因此long使用了a的默认值(即0)。

因此,该行的代码求值X * 100 * 100


8
我不认为这是OP造成的困惑。令人困惑的是X = scale(10) + 3X从方法中引用以来,是0。但是后来是3。因此,OP认为X为分配了两个不同的值,这将与冲突final
Zabuzard

4
@Zabuza这不就是用“说明它试图评估return X * valueX没有被分配一个值却因此采用默认值的long这是0 ”?并不是说X分配了默认值,而是默认值X“替换了”(请不要引用该术语;))。
AxelH

14

这根本不是一个错误,简单地说,它不是正向引用的非法形式,仅此而已。

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

规范仅允许这样做。

以您的示例为例,这正是匹配的地方:

private static final long X = scale(10) + 3;

你是做一个向前引用scale像以前一样说,是不以任何方式违法,但可以让你获得的默认值X。再次,这是规范所允许的(更确切地说是不被禁止),因此它可以正常工作


好答案!我只是很好奇为什么规范确实允许第二种情况进行编译。这是查看最终字段的“不一致”状态的唯一方法吗?
安德鲁·托比尔科

@Andrew这也困扰了我很多时间,我倾向于认为它是C ++或C做到的(不知道这是不是真的)
Eugene

@安德鲁:因为否则将解决图灵不完备定理。
约书亚

9
@Joshua:我想您在这里混淆了许多不同的概念:(1)暂停问题,(2)决策问题,(3)Godel不完全性定理,(4)图灵完备的编程语言。编译器作者不会尝试解决“在使用变量之前是否已明确分配此变量?”的问题。完美是因为该问题等同于解决暂停问题,我们知道我们不能这样做。
埃里克·利珀特

4
@EricLippert:哈哈,哎呀。图灵不完整和停顿问题在我心中占有同一个位置。
约书亚

4

可以在类定义内的代码中初始化类级别成员。编译后的字节码无法内联初始化类成员。(实例成员的处理方式类似,但这与所提供的问题无关。)

当一个人写出如下内容时:

public class Demo1 {
    private static final long DemoLong1 = 1000;
}

生成的字节码将类似于以下内容:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

初始化代码放置在静态初始化程序中,该静态初始化程序在类加载器首次加载类时运行。有了这些知识,您的原始样本将类似于以下内容:

public class RecursiveStatic {
    private static final long X;

    private static long scale(long value) {
        return X * value;
    }

    static {
        X = scale(10);
    }

    public static void main(String[] args) {
        System.out.println(scale(5));
    }
}
  1. JVM加载RecursiveStatic作为jar的入口点。
  2. 加载类定义时,类加载器将运行静态初始化程序。
  3. 初始化程序调用该函数scale(10)来分配static final字段X
  4. scale(long)函数在部分初始化类时运行,读取未初始化的值X是默认值long或0。
  5. 的值0 * 10被分配给X,并且类加载器完成。
  6. JVM运行公共静态void main方法调用scale(5),该方法将5乘以现在初始化的X值0并返回0。

静态final字段X仅分配一次,保留final关键字保留的保证。对于后续在赋值中加3的查询,上面的步骤5成为0 * 10 + 3其值为值的评估,3并且main方法将打印其结果为3 * 5value 的结果15


3

读取对象的未初始化字段应导致编译错误。不幸的是对于Java,事实并非如此。

我认为发生这种情况的根本原因是“隐藏”在实例化和构造对象的定义的内部,尽管我不知道该标准的细节。

从某种意义上说,final定义不明确,因为由于这个问题,它甚至无法实现其既定的目的。但是,如果正确编写了所有类,则不会出现此问题。这意味着所有字段都始终在所有构造函数中设置,并且如果不调用其构造函数之一就不会创建任何对象。在您必须使用序列化库之前,这似乎很自然。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.