为什么这个Java代码编译?
在方法或类范围内,下面的代码编译(带警告):
int x = x = 1;
在类范围中, variables获取其默认值时 ,以下给出“未定义的引用”错误:
int x = x + 1;
是不是第一个x = x = 1
应该以相同的“未定义参考”错误? 或者也许第二行int x = x + 1
应该编译? 或者有什么我失踪?
TL;博士
对于字段 , int b = b + 1
是非法的,因为b
是对b
的非法前向引用。 你可以通过编写int b = this.b + 1
来解决这个问题,这个编译没有任何抱怨。
对于局部variables , int d = d + 1
是非法的,因为d
在使用前没有被初始化。 对于总是默认初始化的字段,情况并非如此。
您可以通过尝试编译来看到差异
int x = (x = 1) + x;
作为一个字段声明和一个局部variables声明。 前者会失败,但由于语义的不同,后者将会成功。
介绍
首先,字段和局部variables初始化器的规则是非常不同的。 所以这个答案将分两部分来处理规则。
我们将全程使用这个testing程序:
public class test { int a = a = 1; int b = b + 1; public static void Main(String[] args) { int c = c = 1; int d = d + 1; } }
b
的声明是无效的,并且带有illegal forward reference
错误。
d
的声明是无效的,并且失败, variable d might not have been initialized
错误。
这些错误不一样的事实应该暗示错误的原因也是不同的。
字段
Java中的字段初始化程序由JLS第8.3.2节 “字段的初始化”进行pipe理。
JLS第6.3节 “声明的范围”中定义了一个字段的范围。
相关规则是:
- 在类typesC(§8.1.6)中声明或inheritance的成员
m
声明的范围是C的整个主体,包括任何嵌套types声明。 - 实例variables的初始化expression式可以使用在类中声明或由类inheritance的任何静态variables的简单名称,即使是稍后以文本方式发生的声明。
- 尽pipe这些实例variables在范围之内,但是在使用之后使用声明在文本上出现的实例variables有时会受到限制。 有关对实例variables进行前向引用的精确规则,请参阅第8.3.2.3节。
§8.3.2.3说:
成员声明只有在成员是类或接口C的实例(分别为静态)字段并且以下所有条件成立时,才需要以文本方式显示:
- 用法发生在C的一个实例(分别是静态)variables初始值设定项或C中的一个实例(分别是静态)初始值设定项中。
- 用法不在作业的左侧。
- 用法是通过一个简单的名字。
- C是封闭用法的最里面的类或接口。
实际上,除了在某些情况下,您可以在声明之前引用这些字段。 这些限制是为了防止类似的代码
int j = i; int i = j;
从编译。 Java规范说“上面的限制是为了在编译时捕获循环或其他格式不正确的初始化。
这些规则实际上归结为什么?
总之,规则基本上说,如果(a)引用是在初始化器中,(b)引用没有被分配给,(c)引用是一个引用,那么你必须在引用该字段之前声明一个字段简单的名字 (没有像this.
限定词)和(d)它不在内部类中被访问。 因此,满足所有四个条件的前向引用是非法的,但是在至less一个条件下失败的前向引用是可以的。
int a = a = 1;
编纂,因为它违反(b):参考a
是分配给,所以是a
完整的声明之前提到a
合法的。
int b = this.b + 1
也编译,因为它违反了(c):引用this.b
不是一个简单的名字(它是用this.
限定的)。 这个奇怪的结构仍然是完美定义的,因为this.b
的值为零。
所以,基本上,初始化器中的字段引用的限制可以防止int a = a + 1
被成功编译。
请注意,字段声明int b = (b = 1) + b
将无法编译,因为最后的b
仍然是非法的前向引用。
局部variables
局部variables声明受JLS第14.4节 “局部variables声明语句”的pipe辖。
局部variables的范围在JLS第6.3节 “声明的范围”中定义:
- 块(14.4)中的局部variables声明的范围是声明出现的块的其余部分,从其自己的初始化程序开始,并在本地variables声明语句右侧包含任何其他声明程序。
请注意,初始化程序在声明的variables的范围内。 那么为什么不int d = d + 1;
编译?
原因是Java对明确赋值的规则( JLS§16 )。 明确的赋值基本上说,每个对局部variables的访问都必须具有对该variables的前一个赋值,Java编译器检查循环和分支以确保赋值始终在任何使用之前发生(这就是为什么明确赋值具有专用的整个规范部分到它)。 基本规则是:
- 对于每个访问局部variables或空白的最终字段
x
,必须在访问之前明确指定x
,否则会发生编译时错误。
在int d = d + 1;
,对d
的访问被parsing为局部variablesfine,但是由于在访问d
之前d
没有被赋值,编译器会发出一个错误。 在int c = c = 1
, c = 1
首先发生,它赋值c
,然后c
被初始化为赋值结果(即1)。
请注意,由于明确的分配规则,局部variables声明int d = (d = 1) + d;
将会成功编译( 不同于字段声明int b = (b = 1) + b
),因为d
是在最后d
到达的时候明确赋值的。
int x = x = 1;
相当于
int x = 1; x = x; //warning here
而在
int x = x + 1;
首先我们需要计算x+1
但是x+1
的值是未知的,所以你得到一个错误(编译器知道x的值是未知的)
这大致相当于:
int x; x = 1; x = 1;
首先, int <var> = <expression>;
总是相当于
int <var>; <var> = <expression>;
在这种情况下,你的expression式是x = 1
,这也是一个声明。 x = 1
是一个有效的语句,因为var x
已被声明。 它也是一个值为1的expression式,然后再赋给x
。
在Java或任何现代语言中,赋值来自右边。
假设如果你有两个variablesx和y,
int z = x = y = 5;
这个语句是有效的,这是编译器如何拆分它们。
y = 5; x = y; z = x; // which will be 5
但在你的情况
int x = x + 1;
编译器给出了一个例外,因为它像这样分裂。
x = 1; // oops, it isn't declared because assignment comes from the right.
int x = x = 1;
不等于:
int x; x = 1; x = x;
javap再次帮助我们,这些是为这个代码生成的JVM指令:
0: iconst_1 //load constant to stack 1: dup //duplicate it 2: istore_1 //set x to constant 3: istore_1 //set x to constant
更像:
int x = 1; x = 1;
这里没有理由抛出未定义的引用错误。 现在在初始化之前有variables的使用,所以这个代码完全符合规范。 实际上根本没有使用variables ,只是任务。 而且JIT编译器会更进一步,它会消除这样的结构。 说实话,我不明白这个代码是如何连接到JLS的variables初始化和使用的规范。 没有用法没有问题。 ;)
如果我错了,请纠正。 我不知道为什么其他答案,这涉及到许多JLS段落收集这么多的优势。 这些段落与本案没有任何共同之处。 只有两个连续分配,没有更多。
如果我们写:
int b, c, d, e, f; int a = b = c = d = e = f = 5;
等于:
f = 5 e = 5 d = 5 c = 5 b = 5 a = 5
最右边的expression式只是分配给variables,没有任何recursion。 我们可以用任何方式搞乱variables:
a = b = c = f = e = d = a = a = a = a = a = e = f = 5;
在int x = x + 1;
你给x添加1,那么x的值是什么,它还没有创build。
但是在int x=x=1;
将编译没有错误,因为您将1分配给x
。
你的第一块代码包含第二个=
而不是一个加号。 这将在任何地方编译,而第二段代码不会在任何地方编译。
在第二段代码中,x在声明之前被使用,而在第一段代码中它被分配两次,这是没有意义的,但是是有效的。
让我们一步一步分解吧,正确的联想
int x = x = 1
x = 1
,给variablesx赋值1
int x = x
,将int x = x
指定给自己,作为int。 由于x先前被指定为1,因此保留1,尽pipe是冗余的。
编译好。
int x = x + 1
x + 1
,给variablesx加1。 但是,x未定义,这将导致编译错误。
int x = x + 1
,因此这条线编译错误,因为等号的右边部分不会编译,将一个添加到未指定的variables
第二个int x=x=1
是编译的,因为你将值赋给x,但是在其他情况下int x=x+1
这里variablesx没有被初始化,记住在java局部variables中没有初始化为默认值。 注意如果它是( int x=x+1
)在类范围内,那么也会给编译错误,因为这个variables没有被创build。
int x = x + 1;
在Visual Studio 2008中成功编译并出现警告
warning C4700: uninitialized local variable 'x' used`
x在x = x + 1
;中未被初始化。
Java编程语言是静态types的,这意味着所有variables必须先声明才能使用。
查看原始数据types
由于代码的实际工作方式,代码行不会编译出警告。 当你运行代码int x = x = 1
,Java首先根据定义创buildvariablesx
。 然后运行分配代码( x = 1
)。 由于x
已经定义,系统没有错误将x
设置为1.这将返回值1,因为那现在是x
的值。 因此, x
现在最终被设置为1。
Java基本上执行的代码就好像是这样的:
int x; x = (x = 1); // (x = 1) returns 1 so there is no error
然而,在你的第二段代码中, int x = x + 1
, + 1
语句要求定义x
到那时为止。 由于赋值语句总是意味着=
右边的代码先运行,代码将会失败,因为x
是未定义的。 Java会像这样运行代码:
int x; x = x + 1; // this line causes the error because `x` is undefined
编译器从右向左读取语句,我们devise做相反的事情。 这就是为什么起初它感到烦恼。 使这个习惯从右到左阅读语句(代码),你将不会有这样的问题。