为啥这个 Java 代码会编译?

Posted

技术标签:

【中文标题】为啥这个 Java 代码会编译?【英文标题】:Why does this Java code compile?为什么这个 Java 代码会编译? 【发布时间】:2013-03-26 06:55:39 【问题描述】:

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

int x = x = 1;

在类范围内,变量获取默认值的地方,以下给出“未定义引用”错误:

int x = x + 1;

不是第一个x = x = 1 应该以相同的“未定义引用”错误结束吗?或者也许第二行int x = x + 1 应该编译?还是我遗漏了什么?

【问题讨论】:

如果在类作用域变量中添加关键字static,就像在static int x = x + 1;中一样,你会得到同样的错误吗?因为在 C# 中,它是静态的还是非静态的会有所不同。 static int x = x + 1 在 Java 中失败。 在 c# 中,int a = this.a + 1;int b = 1; int a = b + 1; 在类范围内(在 Java 中都可以)失败,可能是由于 §17.4.5.2 -“实例字段的变量初始化程序无法引用正在创建的实例。”我不知道它是否在某处明确允许,但静态没有这样的限制。在 Java 中,规则不同,static int x = x + 1 失败的原因与 int x = x + 1 的原因相同 带有字节码的anwser可以消除任何疑虑。 【参考方案1】:

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

【讨论】:

【参考方案2】:

int x = x + 1; 中 x 加 1,那么x 的值是多少,它还没有创建。

但在int x=x=1; 中编译不会出错,因为您将 1 分配给 x

【讨论】:

【参考方案3】:

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

【讨论】:

【参考方案4】:

大致相当于:

int x;
x = 1;
x = 1;

首先,int <var> = <expression>; 总是等价于

int <var>;
<var> = <expression>;

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

【讨论】:

好的,但是如果它像你说的那样,为什么在类范围内第二个语句会出错?我的意思是你得到整数的默认 0 值,所以我希望结果是 1,而不是 undefined reference 看看@izogfif 的回答。似乎可以工作,因为 C++ 编译器为变量分配了默认值。与 java 对类级变量的处理方式相同。 @Marcin:在 Java 中,int 是局部变量时初始化为 0。如果它们是成员变量,它们只会被初始化为 0。因此,在您的第二行中,x + 1 没有定义值,因为 x 未初始化。 @OpenSauce 但是x 定义为成员变量(“在类范围内”)。 @JacobRaihle:好吧,没发现那部分。如果编译器看到有明确的初始化指令,我不确定将 var 初始化为 0 的字节码是否会由编译器生成。这里有一篇文章详细介绍了类和对象初始化,尽管我认为它没有解决这个确切的问题:javaworld.com/jw-11-2001/jw-1102-java101.html【参考方案5】:

第二个int x=x=1 是编译的,因为您正在将值分配给 x 但在其他情况下int x=x+1 这里变量 x 未初始化,请记住在 java 中局部变量未初始化为默认值。 注意如果它在类范围内也是(int x=x+1),那么它也会给出编译错误,因为没有创建变量。

【讨论】:

【参考方案6】:
int x = x + 1;

在 Visual Studio 2008 中成功编译并出现警告

warning C4700: uninitialized local variable 'x' used`

【讨论】:

有趣。是 C/C++ 吗? @Marcin:是的,它是 C++。 @msam:对不起,我想我看到了标签 c 而不是 java 但显然这是另一个问题。 它可以编译是因为在 C++ 中,编译器会为原始类型分配默认值。使用bool y;y==true 将返回false。 @SriHarshaChilakapati,它是 C++ 编译器中的某种标准吗?因为当我在 Visual Studio 2008 中编译 void main() int x = x + 1; printf("%d ", x); 时,在 Debug 中我得到异常 Run-Time Check Failure #3 - The variable 'x' is being used without being initialized. 而在 Release 中我得到数字 1896199921 打印在控制台中。 @SriHarshaChilakapati 谈论其他语言:在 C# 中,对于 static 字段(类级静态变量),同样的规则适用。例如,声明为 public static int x = x + 1; 的字段在 Visual C# 中编译时不会发出警告。在 Java 中可能相同?【参考方案7】:
int x = x = 1;

等价于

int x = 1;
x = x; //warning here

int x = x + 1; 

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

【讨论】:

这加上来自 OpenSauce 的右关联提示我发现非常有用。 我认为赋值的返回值是被赋值的值,而不是变量值。 @zzzzBov 是正确的。 int x = x = 1; 等价于int x = (x = 1)不是 x = 1; x = x;。你不应该因为这样做而收到编译器警告。 int x = x = 1; s 等效于 int x = (x = 1) 因为 = 运算符的右关联性 @nneonneo 和 int x = (x = 1) 等价于 int x; x = 1; x = x;(变量声明、字段初始值设定项的评估、将变量分配给所述评估的结果),因此发出警告【参考方案8】:

x 未在 x = x + 1; 中初始化。

Java 编程语言是静态类型的,这意味着所有变量都必须先声明后才能使用。

见primitive data types

【讨论】:

在使用变量值之前需要初始化变量与静态类型无关。静态类型:你需要声明一个变量是什么类型。 Initialise-before-use:它需要可证明有一个值,然后才能使用该值。 @JonBright:声明变量类型的需要也与静态类型无关。例如,有类型推断的静态类型语言。 @hammar,在我看来,您可以通过两种方式争论:通过类型推断,您以系统可以推断的方式隐式声明变量的类型。或者,类型推断是第三种方式,其中变量不是在运行时动态类型化,而是在源级别,这取决于它们的使用和由此做出的推断。无论哪种方式,该声明仍然正确。但你说得对,我没有考虑其他类型系统。【参考方案9】:

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

假设如果你有两个变量 x 和 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.

【讨论】:

【参考方案10】:

让我们一步一步分解,右联想

int x = x = 1

x = 1,给变量x赋值1

int x = x,将 x 本身赋值为 int。由于 x 之前被指定为 1,因此它保留了 1,尽管是以冗余方式。

编译得很好。

int x = x + 1

x + 1,给变量 x 加一。但是, x 未定义这将导致编译错误。

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

【讨论】:

不,当有两个=操作符时是右结合的,所以和int x = (x = 1);一样。 啊,我的订单取消了。对于那个很抱歉。应该倒着做。我现在换了。【参考方案11】:

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;

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

如果我错了,请纠正。我不明白为什么引用许多 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

最右边的表达式只是一一赋值给变量,没有任何递归。我们可以用任何我们喜欢的方式来混淆变量:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;

【讨论】:

【参考方案12】:

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;
    

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

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

字段

Java 中的字段初始值设定项由 JLS §8.3.2,字段初始化。

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

相关规则有:

在类类型 C(第 8.1.6 节)中声明或继承的成员 m 的声明范围是整个 C 主体,包括任何嵌套类型声明。 实例变量的初始化表达式可以使用在类中声明或继承的任何静态变量的简单名称,即使是稍后在文本中声明的静态变量。 有时会限制使用在使用后以文本形式出现声明的实例变量,即使这些实例变量在范围内也是如此。有关管理对实例变量的前向引用的精确规则,请参见第 8.3.2.3 节。

§8.3.2.3 说:

成员的声明需要以文本形式出现在它之前 仅当成员是一个实例(分别为静态)字段时才使用 一个类或接口 C 并且满足以下所有条件:

在 C 的实例(分别为静态)变量初始化程序或 C 的实例(分别为静态)初始化程序中使用。 用法不在作业的左侧。 用法是通过一个简单的名称。 C 是包含用法的最内层类或接口。

您实际上可以在声明字段之前引用它们,但在某些情况下除外。这些限制旨在防止类似

的代码
int j = i;
int i = j;

来自编译。 Java 规范说“上述限制旨在在编译时捕获循环或其他格式错误的初始化。”

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

简而言之,规则基本上说您必须在对该字段的引用之前声明一个字段,如果 (a) 引用在初始化程序中,(b)分配给,(c) 引用是一个简单名称(没有像this. 这样的限定符)和(d) 它不是从内部类中访问的。因此,满足所有四个条件的前向引用是非法的,但至少在一个条件上失败的前向引用是可以的。

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 仍然是非法的前向引用。

局部变量

局部变量声明由JLS §14.4,局部变量声明语句管理。

局部变量的作用域定义在JLS §6.3,声明的作用域:

块中局部变量声明的范围(第 14.4 节)是声明出现的块的其余部分,从它自己的初始化程序开始,并在局部变量声明语句的右侧包括任何进一步的声明符。

注意初始化器在被声明的变量的范围内。那么为什么int d = d + 1; 不编译呢?

原因是由于 Java 的明确分配规则 (JLS §16)。定义赋值基本上是说每次访问局部变量都必须先对该变量进行赋值,Java 编译器会检查循环和分支以确保赋值总是在任何使用之前发生(这就是为什么定义分配有一个专门的规范部分)。基本规则是:

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

int d = d + 1; 中,对d 的访问被解析为局部变量fi​​ne,但是由于在访问d 之前还没有分配d,所以编译器会报错。在int c = c = 1 中,首先发生c = 1,它分配c,然后c 被初始化为该分配的结果(即1)。

注意,由于明确的赋值规则,局部变量声明int d = (d = 1) + d;编译成功(不像字段声明int b = (b = 1) + b),因为d是在达到最终的d 时确定分配。

【讨论】:

+1 用于参考,但是我认为您的措辞错误:“int a = a = 1; 编译是因为它违反了 (b)”,如果它违反了它的 4 个要求中的任何一个不会编译。但是它没有,因为它 IS 在作业的左侧(JLS 措辞中的双重否定在这里没有多大帮助)。在int b = b + 1 b 位于分配的右侧(而不是左侧),因此它会违反此... ... 我不太确定的是:如果声明在赋值之前没有以文本形式出现,则必须满足这 4 个条件,在这种情况下,我认为声明确实出现了“在分配int x = x = 1 之前以文本方式”,在这种情况下,这些都不适用。 @msam:这有点令人困惑,但基本上你必须违反四个条件之一才能进行前向引用。如果您的前向引用满足所有四个条件,则它是非法的。 @msam:另外,完整的声明只有在初始化之后才生效。 @mrfishie:答案很大,但 Java 规范的深度令人惊讶。这个问题并不像表面上看起来那么简单。 (我曾经写过一个 Java 编译器的子集,所以我对 JLS 的许多细节非常熟悉)。【参考方案13】:

由于代码的实际工作方式,该代码行编译时不会出现警告。 当您运行代码int x = x = 1 时,Java 首先按照定义创建变量x然后它运行分配代码 (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

【讨论】:

【参考方案14】:

编译器从右到左读取语句,而我们则设计相反。这就是为什么它一开始很生气的原因。养成从右到左阅读语句(代码)的习惯,您就不会有这样的问题。

【讨论】:

以上是关于为啥这个 Java 代码会编译?的主要内容,如果未能解决你的问题,请参考以下文章

如果 Java 是强类型的,那么为啥这段代码会编译? [关闭]

C++ 唯一指针;为啥这个示例代码会出现编译错误?错误代码太长了,我无法指定

java 为啥文件不加入Source中编译就出现source not found

为啥 Java 会出现“无法访问的语句”编译器错误?

当我的代码超出函数范围时,为啥会出现编译器错误“未命名类型”?

为啥这个不允许编译器执行的示例会导致使用 cmov 取消引用空指针?