最终定义不明确吗?

Lit*_*per 184 java final class-variables static-initialization

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

回答:

0

下面的剧透.


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

static修饰符与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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

通过查看ernesto得到的输出特别清楚:当a标记时final,他得到以下输出:

a=5
a=5
Run Code Online (Sandbox Code Playgroud)

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

Zab*_*uza 216

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

原因是final只允许一项任务.但是,默认值不是赋值.事实上,每个这样的变量(类变量,实例变量,数组组件)在分配之前从头开始指向其默认值.然后第一个分配更改引用.


类变量和默认值

看一下下面的例子:

private static Object x;

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

我们没有明确地指定一个值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
}
Run Code Online (Sandbox Code Playgroud)

来自相同的JLS段落:

一个局部变量(§14.4,§14.14)必须明确给出的数值在使用之前,由要么初始化(§14.4)或分配(§15.26),在可以使用明确赋值的规则进行验证的方式(§ 16(确定任务)).


最终变量

现在我们来看看final,从§4.12.4:

最终变量

变量可以声明为final.甲最终变量可以仅分配给一次.如果指定了最终变量,则它是编译时错误,除非它在赋值之前明确地未分配(§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;
}
Run Code Online (Sandbox Code Playgroud)

它输出

Before: 0
After: 1
Run Code Online (Sandbox Code Playgroud)

回想一下我们学到的东西.在方法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;
}
Run Code Online (Sandbox Code Playgroud)

JLS中的示例

感谢@Andrew我找到了一个JLS段落,它完全涵盖了这个场景,它也展示了它.

但首先让我们来看看

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer
Run Code Online (Sandbox Code Playgroud)

为什么这不允许,而方法的访问是?看一下§8.3.3,它讨论了如果字段尚未初始化则何时限制对字段的访问.

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

对于通过简单名称引用f类或接口中声明的类变量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);
    }
}
Run Code Online (Sandbox Code Playgroud)

产生输出:

0
Run Code Online (Sandbox Code Playgroud)

因为变量初始化器i使用类方法peek来访问变量的值j之前j已经由其变量初始化器初始化,此时它仍然具有其默认值(§4.12.5).

  • 问题是你可以从构造函数中调用实例方法,这可能是不允许的.另一方面,不允许在调用super之前分配locals,这将是有用且安全的.去搞清楚. (4认同)

Sur*_*tta 22

这与决赛没什么关系.

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

如果您在X未完全分配的情况下进行访问,则它将保留long的默认值0,即结果.

  • 对此有些棘手的是,如果你没有分配值,它将不会被赋值为默认值,但是如果你用它来为自己分配"最终"值,它将...... (3认同)
  • @AxelH我明白你是什么意思.但这就是它应该如何工作否则世界崩溃;). (2认同)

Old*_*eon 20

不是错误.

从第一次调用时scale调用

private static final long X = scale(10);
Run Code Online (Sandbox Code Playgroud)

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

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

  • 我不认为那是OP困惑的事情.令人困惑的是`X = scale(10)+ 3`.由于`X`,当从方法引用时,是'0`.但之后它是'3`.所以OP认为`X`被分配了两个不同的值,这将与`final`冲突. (8认同)
  • @Zabuza不是这个解释用"_It试图评估`返回X*值`."X`还没有赋值,因此取's`的默认值为'0`._" ?不是说"X"被赋予了默认值,而是"X"被"替换"(请不要引用该术语;))默认值. (4认同)

Eug*_*ene 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;
}
Run Code Online (Sandbox Code Playgroud)

它只是规范允许的.

举个例子,这正是匹配的地方:

private static final long X = scale(10) + 3;
Run Code Online (Sandbox Code Playgroud)

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

  • @Joshua:我认为你在这里混合了许多不同的概念:(1)暂停问题,(2)决策问题,(3)Godel不完备性定理,以及(4)图灵完备编程语言.编译器编写者不会尝试解决问题"这个变量在使用之前是否已明确分配?" 完美是因为这个问题等同于解决停机问题,我们知道我们不能这样做. (9认同)
  • @EricLippert:哈哈哎呀.图灵不完整和停止问题占据了我心中的同一个地方. (4认同)