最终定义不清楚吗?

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

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标签相结合的循环初始化。 仅从另一个问题来看,我不能理解为什么我的问题中的代码没有出错。

通过观察欧内斯托得到的产出,这一点尤为明显: 当 a被标记为 final时,他会得到以下输出:

a=5
a=5

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

11210 次浏览

不是窃听器。

调用 scale的第一个调用时

private static final long X = scale(10);

它试图评估 return X * value。还没有为 X分配值,因此使用了 long的默认值(即 0)。

这行代码的计算结果是 X * 10也就是 0 * 10也就是 0

这和期末考试没关系。

因为它处于实例或类级别,所以如果还没有分配任何东西,它保存默认值。这就是为什么你看到 0时,你访问它没有分配。

如果访问 X时没有完全赋值,那么它保存的默认值是 long,即 0,因此会得到结果。

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

原因是 final只允许一个 任务。但是,默认值不是 任务。事实上,每个 如此多变(类变量、实例变量、数组组件)从一开始就指向它的 默认值,在 任务之前。然后,第一个赋值更改引用。


类变量和默认值

看看下面的例子:

private static Object x;


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

我们没有显式地赋值给 x,尽管它指向 null,这是默认值。与 4.12.5相比:

变量的初始值

每个 类变量、实例变量或者阵列组件在初始化为 被创造出来(15.915.10.2)时都使用一个 默认值进行初始化

请注意,这只适用于那些类型的变量,就像我们的示例一样。它不适用于局部变量,请参见下面的示例:

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

来自联合律师协会同一段落:

局部变量局部变量(14.414.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,它讨论了如果字段尚未初始化,那么何时对字段的访问受到限制。

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

对于通过简单名称引用在类或接口 C中声明的类变量 f,它是 编译时错误,如果:

  • 引用可以出现在 C的类变量初始值设定项中,也可以出现在 C的静态初始值设定项(8.7)中; 以及

  • 该引用或者出现在 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的变量初始化器在 j被它的变量初始化器初始化之前使用类方法 Peak 来访问变量 j的值,此时它是 仍然有它的默认值(4.12.5)。

它根本不是一个 bug,简单地说,它不是一个转发引用的 非法形式,仅此而已。

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的默认值。同样,这是允许的规范(更确切地说,它不是被禁止的) ,所以它工作得很好

读取对象的未初始化字段应该会导致编译错误。

我认为这种情况的根本原因在于对象是如何实例化和构造的定义中“隐藏”得很深,尽管我不知道标准的细节。

在某种意义上,期末考试定义不明确,因为它甚至没有完成由于这个问题而设定的目标。但是,如果所有类都编写正确,就不会出现这个问题。这意味着所有字段总是在所有构造函数中设置,没有一个对象是在不调用其构造函数的情况下创建的。在必须使用序列化库之前,这似乎是很自然的。

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

当一个人写下这样的东西:

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 运行调用 scale(5)的 public static void main 方法,该方法将5乘以现在初始化的返回0的 X值。

静态 final 字段 X只分配一次,保留了由 final关键字保持的保证。对于后续的在赋值中添加3的查询,上面的步骤5将成为 0 * 10 + 3的求值,0 * 10 + 3是值 3,主方法将打印 3 * 5的结果,3 * 5是值 15