为什么这个Java代码会编译?

Mar*_*cin 96 java compiler-construction

在方法或类范围中,下面的行编译(带警告):

int x = x = 1;
Run Code Online (Sandbox Code Playgroud)

在类范围中,变量获取其默认值,以下给出"未定义引用"错误:

int x = x + 1;
Run Code Online (Sandbox Code Playgroud)

是不是第一个x = x = 1应该以相同的'未定义引用'错误结束?或者第二行int x = x + 1应该编译?或者有些东西我不见了?

nne*_*neo 101

TL;博士

对于字段,int b = b + 1是非法的,因为b是非法的前向引用b.你实际上可以通过编写来解决这个问题int b = this.b + 1,编写时没有抱怨.

对于局部变量,int d = d + 1是非法的,因为d在使用前未初始化.这是不是对字段,始终为默认初始化的情况.

您可以通过尝试编译来查看差异

int x = (x = 1) + x;

作为字段声明和局部变量声明.前者会失败,但后者会成功,因为语义不同.

介绍

首先,字段和局部变量初始化程序的规则是非常不同的.所以这个答案将分两部分来处理这些规则.

我们将在整个过程中使用此测试程序:

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

声明b无效并因illegal forward reference错误而失败.
声明d无效并因variable d might not have been initialized错误而失败.

这些错误不同的事实应该暗示错误的原因也不同.

字段

Java中的字段初始化程序由JLS§8.3.2,字段初始化控制.

字段的范围JLS§6.3,声明范围中定义.

相关规则是:

  • m由类类型C(第8.1.6节)声明或继承的成员声明的范围是C的整个主体,包括任何嵌套类型声明.
  • 实例变量的初始化表达式可以使用在类中声明或继承的任何静态变量的简单名称,即使其声明稍后以文本形式发生的静态变量.
  • 使用在使用后以声明方式显示声明的实例变量有时会受到限制,即使这些实例变量在范围内也是如此.有关控制实例变量的正向引用的精确规则,请参见§8.3.2.3.

§8.3.2.3说:

只有当成员是类或接口C的实例(分别是静态)字段并且满足以下所有条件时,成员的声明才需要以文本方式显示:

  • 用法发生在C的实例(分别是静态)变量初始化器或C的实例(分别是静态)初始化器中.
  • 用法不在作业的左侧.
  • 用法是通过一个简单的名称.
  • C是封闭用法的最内层类或接口.

除非在某些情况下,您实际上可以在声明字段之前引用这些字段.这些限制旨在防止类似的代码

int j = i;
int i = j;
Run Code Online (Sandbox Code Playgroud)

从编译.Java规范说"上述限制旨在捕获,在编译时,循环或其他错误的初始化."

这些规则实际归结为什么?

总之,这些规则基本上是说,你必须事先到现场参考的声明场如果(a)的引用是一个初始化,(B)引用不会被分配到,(c)证明是简单的名称(没有类似的限定词this.)和(d)它不是从内部类中访问的.因此,满足所有四个条件的前向引用是非法的,但是至少在一个条件上失败的前向引用是可以的.

int a = a = 1;编译,因为它违反了(b):引用被分配,因此在完整声明之前参考a 合法的.aa

int b = this.b + 1也编译,因为它违反了(c):引用this.b不是一个简单的名称(它是合格的this.).这个奇怪的构造仍然是完美定义的,因为this.b它的值为零.

因此,基本上,初始化程序中对字段引用的限制阻止int a = a + 1了成功编译.

观察到字段声明int b = (b = 1) + b无法编译,因为final b仍然是非法的前向引用.

局部变量

局部变量声明由JLS§14.4,局部变量声明声明控制.

局部变量的范围JLS§6.3,声明范围中定义:

  • 块(第14.4节)中局部变量声明的范围是声明出现的块的其余部分,从其自己的初始化器开始,并包括局部变量声明语句中右侧的任何其他声明符.

请注意,初始值设定项在声明的变量范围内.那为什么不int d = d + 1;编译?

原因在于Java对明确赋值的规则(JLS§16).确定赋值基本上表示对局部变量的每次访问都必须具有对该变量的先前赋值,并且Java编译器检查循环和分支以确保在任何使用之前总是发生赋值(这就是为什么明确赋值具有专用的整个规范部分的原因)它).基本规则是:

  • 对于本地变量或空白最终字段的每次访问x,x必须在访问之前明确分配,否则发生编译时错误.

int d = d + 1;,访问权限d被解析为本地变量罚款,但由于dd访问之前尚未分配,因此编译器会发出错误.In int c = c = 1,c = 1首先发生,然后分配c,然后c初始化为该赋值的结果(即1).

请注意,由于明确的赋值规则,局部变量声明int d = (d = 1) + d; 成功编译(字段声明不同int b = (b = 1) + b),因为dd到达最终时必须分配.


msa*_*sam 86

int x = x = 1;
Run Code Online (Sandbox Code Playgroud)

相当于

int x = 1;
x = x; //warning here
Run Code Online (Sandbox Code Playgroud)

而在

int x = x + 1; 
Run Code Online (Sandbox Code Playgroud)

首先我们需要计算,x+1但x的值是未知的,所以你得到一个错误(编译器知道x的值是未知的)

  • 这加上我对OpenSauce的右关联性的暗示我觉得非常有用. (4认同)
  • @zzzzBov是对的.`int x = x = 1;`相当于`int x =(x = 1)`,**不是**`x = 1; x = x;`.这样做不应该得到编译器警告. (2认同)

Ope*_*uce 41

它大致相当于:

int x;
x = 1;
x = 1;
Run Code Online (Sandbox Code Playgroud)

首先,int <var> = <expression>;总是相当于

int <var>;
<var> = <expression>;
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您的表达式x = 1也是一个声明.x = 1是一个有效的语句,因为x已经声明了var .它也是值为1的表达式,然后再将其赋值x.


Sri*_*ati 12

在java或任何现代语言中,赋值来自右侧.

假设你有两个变量x和y,

int z = x = y = 5;
Run Code Online (Sandbox Code Playgroud)

此语句有效,这是编译器分割它们的方式.

y = 5;
x = y;
z = x; // which will be 5
Run Code Online (Sandbox Code Playgroud)

但在你的情况下

int x = x + 1;
Run Code Online (Sandbox Code Playgroud)

编译器给出了一个异常,因为它会像这样分裂.

x = 1; // oops, it isn't declared because assignment comes from the right.
Run Code Online (Sandbox Code Playgroud)


Mik*_*ail 8

int x = x = 1; 不等于:

int x;
x = 1;
x = x;
Run Code Online (Sandbox Code Playgroud)

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

更像:

int x = 1;
x = 1;
Run Code Online (Sandbox Code Playgroud)

这里没有理由抛出未定义的引用错误.现在在初始化之前使用变量,因此该代码完全符合规范.实际上根本没有使用变量,只是赋值.而且JIT编译器会更进一步,它将消除这种结构.说实话,我不明白这段代码是如何与JLS的变量初始化和使用规范相关联的.没用就没问题.;)

如果我错了,请更正.我无法弄清楚为什么其他答案,涉及许多JLS段落收集了这么多的优点.这些段落与此案例没有任何共同之处.只是两个串行任务,而不是更多.

如果我们写:

int b, c, d, e, f;
int a = b = c = d = e = f = 5;
Run Code Online (Sandbox Code Playgroud)

等于:

f = 5
e = 5
d = 5
c = 5
b = 5
a = 5
Run Code Online (Sandbox Code Playgroud)

最正确的表达式只是逐个分配给变量,没有任何递归.我们可以以任何方式混淆变量:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;
Run Code Online (Sandbox Code Playgroud)


Aly*_*mal 7

int x = x + 1;你添加1到x,所以它的价值是什么x,它尚未创建.

但是在int x=x=1;编译时没有错误,因为你指定了1 x.


Joe*_*son 5

您的第一段代码包含第二段=而不是加号.这将在任何地方编译,而第二段代码不会在任何一个地方编译.


Wil*_*lQu 5

在第二段代码中,x在其声明之前使用,而在第一段代码中,它只被分配两次,这没有意义但是有效.


ste*_*ris 5

让我们一步一步地分解,正确的联想

int x = x = 1
Run Code Online (Sandbox Code Playgroud)

x = 1,将1赋值给变量x

int x = x,将一个x指定给它自己,作为一个int.由于x先前被指定为1,因此它保留1,尽管是多余的方式.

编译好.

int x = x + 1
Run Code Online (Sandbox Code Playgroud)

x + 1,将一个添加到变量x.但是,x未定义,这将导致编译错误.

int x = x + 1,因此这行编译错误,因为equals的右边部分不会编译将一个添加到未分配的变量