首先,一个谜题: 以下代码打印了什么?
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
!
static修饰符与final修饰符结合使用,也用于定义常量。 最终修饰符表示此字段的值不能更改。
来源:https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [强调补充]
我的问题:
这是一个错误吗?
final
定义不明确吗?
这是我感兴趣的代码。
X
分配了两个不同的值:0
和3
。
我认为这违反了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 static final field initialization order的可能副本。
我认为这个问题是不重复,因为
另一个问题解决了初始化的顺序
我的问题涉及与final
标签结合的循环初始化。
仅从另一个问题来看,我就无法理解为什么我的问题中的代码不会出错。
通过查看ernesto获得的输出,这一点尤为明显:
当a
被final
标记时,他会得到以下输出:
a=5
a=5
这不涉及我的问题的主要部分:final
变量如何改变其变量?
答案 0 :(得分:213)
一个非常有趣的发现。为了理解它,我们需要深入研究Java语言规范(JLS)。
原因是final
只允许一个分配。但是,默认值为赋值。实际上,每个这样的变量(类变量,实例变量,数组组件)从赋值开始之前指向默认值。然后第一个分配更改引用。
看看下面的例子:
private static Object x;
public static void main(String[] args) {
System.out.println(x); // Prints 'null'
}
我们没有明确地为x
分配值,但它指向null
,它是默认值。将其与§4.12.5进行比较:
变量的初始值
请注意,这仅适用于那些变量,例如我们的示例。它不适用于局部变量,请参见以下示例:
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)在使用之前必须显式赋予值,通过初始化({{ 3}})或赋值(§14.4),可以使用明确赋值规则(§15.26)进行验证。
现在我们来看看final
中的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;
}
:
最终变量
变量可以声明为 final 。 final 变量只能分配一次。如果 final 变量被分配给它是一个编译时错误,除非在分配之前明确地未分配(§16 (Definite Assignment))。
现在回到你的例子,稍加修改:
Before: 0
After: 1
输出
assign
回想一下我们学到的东西。在方法X
内,变量assign
尚未分配值。因此,它指向其默认值,因为它是类变量,并且根据JLS,这些变量总是立即指向其默认值(与局部变量相反)。在X
方法之后,变量1
被赋值为final
,由于final
,我们无法再对其进行更改。因此,由于private static long assign() {
// Assign X
X = 1;
// Second assign after method will crash
return X + 1;
}
:
private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer
感谢@Andrew我找到了一个JLS段落,它完全涵盖了这个场景,它也展示了它。
但首先让我们来看看
f
为什么不允许这样做,而方法的访问权限是?看看§4.12.4,其中讨论了如果字段尚未初始化,则何时限制访问字段。
它列出了一些与类变量相关的规则:
对于通过简单名称引用类或接口
C
中声明的类变量C
,如果,则编译时错误:
引用显示在
C
的类变量初始值设定项中或f
(§16 (Definite Assignment))的静态初始值设定项中;以及引用出现在
f
自己的声明者的初始值设定项中,或者出现在C
的声明者左侧的点;以及引用不在赋值表达式的左侧(§8.3.3);以及
封闭引用的最里面的类或接口是
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
的变量初始值设定项使用类方法peek在j
由其变量初始化程序初始化之前访问变量p
的值,此时它仍然有默认值(§8.7)。
答案 1 :(得分:22)
与最终决赛没什么关系。
由于它是实例级或类级别,如果尚未分配任何内容,它将保留默认值。这就是你在没有分配时访问它时看到0
的原因。
如果您在未完全分配的情况下访问X
,则会保留默认值long 0
,从而得到结果。
答案 2 :(得分:20)
不是错误。
从
调用对scale
的第一次调用时
private static final long X = scale(10);
它会尝试评估return X * value
。尚未为X
分配值,因此使用long
的默认值(0
)。
因此该代码行的评估结果为X * 10
,即0 * 10
0
。
答案 3 :(得分: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
。再次,这是规范允许的(更确切地说,它是不被禁止的),所以它可以正常工作
答案 4 :(得分: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));
}
}
scale(10)
来指定static final
字段X
。scale(long)
函数在类部分初始化时运行,读取未初始化的X
值,默认值为long或0。0 * 10
的值已分配给X
,类加载器完成。scale(5)
的public static void main方法,该方法将5乘以现在初始化的X
值0,返回0。静态最终字段X
仅分配一次,保留final
关键字所保留的保证。对于在分配中添加3的后续查询,上面的步骤5将成为0 * 10 + 3
的值3
的评估,主方法将打印3 * 5
的结果,即值15
{{1}}。
答案 5 :(得分:3)
读取对象的未初始化字段应该导致编译错误。不幸的是,它没有。
我认为这种情况的根本原因是“隐藏”在对象实例化和构造的定义中,尽管我不知道标准的细节。
从某种意义上说,最终定义不明确,因为它甚至没有达到其声明的目的是由于这个问题。但是,如果所有类都已正确编写,则不会出现此问题。意味着所有字段始终在所有构造函数中设置,并且不会在不调用其构造函数的情况下创建任何对象。在您必须使用序列化库之前,这似乎很自然。