最终的定义是否不明确?

首先,一个难题:下面的代码打印了什么?

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

下面有剧透。


如果以比例(长)打印并重新定义 ,则打印件将为 。这意味着 暂时设置为 ,稍后设置为 。这是违反 !XX = scale(10) + 3X = 0X = 3X03final

静态修饰符与最终修饰符结合使用也用于定义常量。最后一个修饰符指示此字段的值无法更改

资料来源:https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html[着重号为作者所加]


我的问题:这是一个错误吗?定义不清吗?final


这是我感兴趣的代码。 分配有两个不同的值:和 。我认为这是违反.X03final

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 获得的输出,这一点尤其清楚:当被标记为 时,他将获得以下输出:afinal

a=5
a=5

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


答案 1

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

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


类变量和默认值

请看以下示例:

private static Object x;

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

我们没有显式地将值赋给 ,尽管它指向 ,它是默认值。将其与 §4.12.5 进行比较xnull

变量的初始值

每个类变量、实例变量或数组组件在创建时都使用默认值进行初始化 (§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(定赋值))进行验证。


最终变量

现在我们看一下 §4.12.4 中的 :final

最后变量

变量可以声明为 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

回想一下我们学到的东西。在方法中,变量尚未赋值。因此,它指向其默认值,因为它是一个类变量,并且根据JLS,这些变量总是立即指向它们的默认值(与局部变量相反)。在方法之后,变量被分配了值,因此我们无法再更改它。因此,由于以下原因,以下内容将不起作用:assignXassignX1finalfinal

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,它讨论了如果字段尚未初始化,则何时限制对字段的访问。

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

对于通过简单名称对类或接口中声明的类变量的引用,如果出现以下情况,则为编译时错误fC

  • 引用出现在 (§8.7) 的类变量初始值设定项中或静态初始值设定项中。和CC

  • 引用出现在 自己的声明子的初始值设定项中,或者出现在 声明符左侧的某个点;和ff

  • 引用不在赋值表达式的左侧 (§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

因为 的变量初始值设定项使用类方法 peek 来访问变量的值之前已由其变量初始值设定项初始化,此时它仍然具有其默认值§4.12.5)。ijj


答案 2

这里与最终无关。

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

如果您在没有完全分配的情况下访问,则它保留 long 的默认值,即 ,因此得到结果。X0


推荐