为什么要编译此Java代码?


96

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

int x = x = 1;

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

int x = x + 1;

它不是第一个x = x = 1应该以相同的“未定义参考”错误结束吗?还是第二行int x = x + 1应该编译?还是我缺少什么?


1
如果将关键字添加到staticclass-scope变量中(如中)static int x = x + 1;,是否会遇到相同的错误?因为在C#中,它是静态的还是非静态的都会有所不同。
Jeppe Stig Nielsen

static int x = x + 1在Java中失败。
Marcin 2013年

1
在c#中int a = this.a + 1;int b = 1; int a = b + 1;在类范围(在Java中都可以)中失败,可能是由于§17.4.5.2-“实例字段的变量初始化器无法引用正在创建的实例。” 我不知道是否在某处明确允许它,但是static没有这样的限制。在Java中的规则是不同的,static int x = x + 1失败对于同样的原因int x = x + 1
MSAM

带有字节码的答案消除了任何疑问。
rgripper

Answers:


101

tl; dr

对于字段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声明的范围中定义。

相关规则是:

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

§8.3.2.3说:

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

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

实际上,在某些情况下,您可以在声明字段之前先对其进行引用。这些限制旨在防止类似

int j = i;
int i = j;

从编译。Java规范说:“以上限制旨在在编译时捕获循环或其他形式的初始化。”

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

简而言之,规则基本上说,如果(a)引用位于初始化程序中,(b)引用未分配给该字段,(c)引用是a ,则必须在对该字段的引用之前声明一个字段简单名称(没有限定词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无法编译,因为最终声明b仍然是非法的前向引用。

局部变量

局部变量声明受JLS§14.4局部变量声明语句约束

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

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

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

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

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

在中int d = d + 1;,对的访问d被解析为局部变量fi​​ne,但是由于d之前未分配访问权限d,因此编译器会发出错误。在中int c = c = 1c = 1首先发生,它分配c,然后c初始化为该分配的结果(为1)。

请注意,由于有明确的赋值规则,局部变量声明int d = (d = 1) + d; 成功编译( field声明不同int b = (b = 1) + b),因为dd到达最终变量时已明确赋值。


+1为参考,但是我认为您的措辞是错误的:“ int a = a = 1;编译是因为它违反了(b)”,如果它违反了4个要求中的任何一项,则将无法编译。但是它没有,因为它IS在赋值的左侧(双负在JLS的措辞没有太大的帮助这里)。在int b = b + 1b为右侧(不是左侧)的分配,因此它会违反这个...
MSAM

...我不太确定以下几点:如果声明在赋值之前没有按文本出现,则必须满足这4个条件,在这种情况下,我认为声明确实在赋值之前以“按文本形式”出现int x = x = 1,其中这种情况都不适用。
msam 2013年

@msam:这有点令人困惑,但基本上,您必须违反四个条件之一才能做出前向参考。如果您的前向参考文献满足所有四个条件,那么这是非法的。
nneonneo 2013年

@msam:此外,完整的声明仅在初始化程序之后生效。
nneonneo 2013年

@mrfishie:很好的答案,但是Java规范中的深度令人惊讶。这个问题并不像表面上那样简单。(我曾经写过Java的子集编译器,所以我对JLS的很多细节都很熟悉)。
nneonneo

86
int x = x = 1;

相当于

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

而在

int x = x + 1; 

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


4
这加上我发现来自OpenSauce的正确关联的提示非常有用。
TobiMcNamobi 2013年

1
我以为赋值的返回值是赋值,而不是变量值。
zzzzBov

2
@zzzzBov是正确的。int x = x = 1;等价于int x = (x = 1) 等于x = 1; x = x;。您不应该为此收到编译器警告。
nneonneo

int x = x = 1;s等于int,x = (x = 1)因为=运算符具有右相关性
Grijesh Chauhan 2013年

1
@nneonneo并int x = (x = 1)等同于int x; x = 1; x = x;(变量声明,字段初始值设定项的评估,将变量分配给所述评估的结果),因此警告
msam 2013年

41

大致相当于:

int x;
x = 1;
x = 1;

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

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

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


好的,但是,如果按照您说的去做,为什么第二个语句在类范围内给出错误?我的意思是您获得0整数的默认值,所以我希望结果为1,而不是undefined reference
Marcin

看看@izogfif答案。似乎工作正常,因为C ++编译器将默认值分配给变量。Java对类级变量的处理方式相同。
Marcin

@Marcin:在Java中,int 是局部变量时不会初始化为0。如果它们是成员变量,则仅将它们初始化为0。因此,在您的第二行中x + 1,由于x未初始化,因此没有定义的值。
OpenSauce

1
@OpenSauce但是x 定义为成员变量(“在类范围内”)。
Jacob Raihle 2013年

@JacobRaihle:好的,没发现那部分。我不确定如果看到明确的初始化指令,编译器将生成将var初始化为0的字节码。这里有一篇文章详细介绍了类和对象的初始化,尽管我认为它
不能解决

12

在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.

警告位于x = x而非x = 1上
Asim Ghaffar

8

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;


5

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


5

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


5

让我们一步一步地分解它,正确关联

int x = x = 1

x = 1,将1分配给变量x

int x = x,将x本身的值指定为int。由于x以前曾被分配为1,所以它保留了1,尽管这是多余的。

这样编译就可以了。

int x = x + 1

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

int x = x + 1,因此这行会编译错误,因为等号的右侧部分不会编译将未分配的变量加1


不,当有两个=运算符时,它是右关联的,因此与相同int x = (x = 1);
Jeppe Stig Nielsen

啊,我的订单了。对于那个很抱歉。应该把它们倒退。我现在已经改变了。
steventnorris

3

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


2
int x = x + 1;

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

warning C4700: uninitialized local variable 'x' used`

2
有趣的。是C / C ++吗?
Marcin

@Marcin:是的,它是C ++。@msam:对不起,我想我看到的c不是标签,java但显然是另一个问题。
izogfif

之所以进行编译,是因为在C ++中,编译器会为原始类型分配默认值。使用bool y;y==true将返回false。
Sri Harsha Chilakapati

@SriHarshaChilakapati,这是C ++编译器中的某种标准吗?因为当我void main() { int x = x + 1; printf("%d ", x); }在Visual Studio 2008中进行编译时,在Debug中我得到了异常,Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.而在Release中,我得到了1896199921打印在控制台中的数字。
izogfif

1
@SriHarshaChilakapati谈论其他语言:在C#中,对于static字段(类级静态变量),适用相同的规则。例如,public static int x = x + 1;在Visual C#中,声明为的字段在编译时不会发出警告。在Java中可能一样吗?
Jeppe Stig Nielsen

2

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

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

查看原始数据类型


3
使用变量的值之前需要初始化变量与静态类型无关。静态类型:您需要声明变量的类型。使用前初始化:需要先证明有一个值,然后才能使用该值。
乔恩·布莱特

@JonBright:声明变量类型的需要也与静态类型无关。例如,存在具有类型推断的静态类型语言。
hammar 2013年

@hammar,按照我的观察方式,您可以用两种方式争论它:使用类型推断,您以系统可以推断的方式隐式声明变量的类型。或者,类型推断是第三种方式,其中变量不是在运行时动态键入的,而是在源代码级别的,具体取决于变量的用途和由此得出的推断。无论哪种方式,该声明都是正确的。但是您是对的,我没有考虑其他类型的系统。
乔恩·布莱特

2

由于代码实际上是如何工作的,因此该行代码不会带有警告编译。在运行代码时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

-1

编译器从右到左读取语句,我们设计相反。这就是为什么它一开始很烦。习惯于从右到左读取语句(代码),这样就不会出现问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.