final 定义不明确吗?

Is final ill-defined?

先来个谜题: 以下代码打印什么?

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

以下剧透。


如果你在 scale(long) 中打印 X 并重新定义 X = scale(10) + 3, 印刷品将是 X = 0,然后是 X = 3。 这意味着 X 被暂时设置为 0 并且稍后设置为 3。 这违反了 final!

The static modifier, in combination with the final modifier, is also used to define constants. The final modifier indicates that the value of this field cannot change.

来源: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;
    }
}

此问题已被标记为可能与 重复。 我认为这个问题 不是 重复问题,因为 另一个问题解决了初始化的顺序,而 我的问题解决了与 final 标记相结合的循环初始化。 单从另一个问题来看,我无法理解为什么我问题中的代码没有出错。

通过查看 ernesto 获得的输出,这一点尤其明显: 当 a 被标记为 final 时,他得到以下输出:

a=5
a=5

这不涉及我的问题的主要部分:final变量如何改变它的变量?

不是错误。

当对 scale 的第一次调用是从

调用时
private static final long X = scale(10);

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

因此该行代码的计算结果为 X * 100 * 100.

此处与final无关。

因为它处于实例或 class 级别,如果尚未分配任何内容,它会保留默认值。这就是您在未分配的情况下访问它时看到 0 的原因。

如果您在没有完全赋值的情况下访问 X,它将保留 long 的默认值,即 0,因此结果。

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

原因是final只允许一个赋值。但是,默认值是没有 assignment。事实上,每一个这样的变量(class变量,实例变量,数组组件)从一开始就指向它的默认值,之前作业。然后第一个赋值改变了引用。


Class 变量和默认值

看看下面的例子:

private static Object x;

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

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

Initial Values of Variables

Each class variable, instance variable, or array component is initialized with a default value when it is created (§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 段落:

A local variable (§14.4, §14.14) must be explicitly given a value before it is used, by either initialization (§14.4) or assignment (§15.26), in a way that can be verified using the rules for definite assignment (§16 (Definite Assignment)).


最终变量

现在我们来看看final,来自§4.12.4

final Variables

A variable can be declared final. A final variable may only be assigned to once. It is a compile-time error if a final variable is assigned to unless it is definitely unassigned immediately prior to the assignment (§16 (Definite Assignment)).


说明

现在回到你的例子,稍作修改:

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 尚未分配 值。因此,它指向它的默认值,因为它是一个 class 变量 并且根据 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,其中讨论了如果字段尚未初始化,何时限制对字段的访问。

它列出了一些与 class 变量相关的规则:

For a reference by simple name to a class variable f declared in class or interface C, it is a compile-time error if:

  • The reference appears either in a class variable initializer of C or in a static initializer of C (§8.7); and

  • The reference appears either in the initializer of f's own declarator or at a point to the left of f's declarator; and

  • The reference is not on the left hand side of an assignment expression (§15.26); and

  • The innermost class or interface enclosing the reference is C.

很简单,X = X + 1 被那些规则捕获,方法访问不是。他们甚至列出了这种情况并举例说明:

Accesses by methods are not checked in this way, so:

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

produces the output:

0

because the variable initializer for i uses the class method peek to access the value of the variable j before j has been initialized by its variable initializer, at which point it still has its default value (§4.12.5).

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

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 进行 前向引用 ,这在任何方面都不违法,但允许您获得 [=13] 的默认值=].同样,这是规范允许的(更准确地说,它不是被禁止的),所以它工作得很好

读取对象的未初始化字段应该会导致编译错误。不幸的是 Java,它没有。

我认为出现这种情况的根本原因是 "hidden" 对象如何实例化和构造的定义很深,尽管我不知道标准的细节。

从某种意义上说,final 是 ill-defined,因为由于这个问题,它甚至没有实现其声明的目的。不过,如果你的类都写对了,就没有这个问题了。这意味着所有字段始终在所有构造函数中设置,并且在不调用其构造函数之一的情况下不会创建任何对象。在您必须使用序列化库之前,这似乎很自然。

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

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

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

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

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

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

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. class 加载程序 运行 是加载 class 定义时的静态初始化程序。
  3. 初始化器调用函数scale(10)static final字段赋值X
  4. scale(long) 函数 运行s 而 class 被部分初始化读取 X 的未初始化值,默认值为 long 或 0。
  5. 0 * 10 的值分配给 X 并且 class 加载程序完成。
  6. JVM 运行调用 public static void main 方法 scale(5) 将 5 乘以现在初始化的 X 值 0,返回 0。

静态最终字段 X 仅分配一次,保留 final 关键字所持有的保证。对于后续在赋值中添加 3 的查询,上面的第 5 步变成了 0 * 10 + 3 的评估,即值 3 并且 main 方法将打印 3 * 5 的结果,即值15.