关于java:为什么编译器没有为这个加法操作给出错误?

Why does the compiler not give an error for this addition operation?

本问题已经有最佳答案,请猛点这里访问。

我知道编译器对整型文本执行隐式类型转换。例如:

1
byte b = 2; // implicit type conversion, same as byte b = (byte)2;

如果范围溢出,编译器会给我一个错误:

1
byte b = 150; // error, it says cannot convert from int to byte

当向变量传递表达式时,编译器会给出相同的错误:

1
2
3
4
5
byte a = 3;
byte b = 5;
byte c = 2 + 7; // compiles fine
byte d = 1 + b; // error, it says cannot convert from int to byte
byte e = a + b; // error, it says cannot convert from int to byte

我得出的结论是,涉及变量的表达式的结果是不能保证的。结果值可以在字节范围之内或之外,因此编译器会抛出一个错误。

令我困惑的是,当我这样放置编译器时,它不会抛出错误:

1
2
3
byte a = 127;
byte b = 5;
byte z = (a+=b); // no error, why ?

为什么它不给我一个错误?


虽然反编译您的代码将解释Java正在做什么,但它为什么这样做通常可以在语言规范中找到。但在开始之前,我们必须建立一些重要的概念:

  • 字面数字总是被当作一个int来使用。


    An integer literal is of type long if it is suffixed with an ASCII letter L or l (ell); otherwise it is of type int (§4.2.1).

  • byte只能包含-128和127之间的整数值。

  • 如果试图分配的文本大于可容纳该文本的类型,则会导致编译错误。这是您遇到的第一个场景。

所以我们回到这个场景:为什么添加两个明显大于字节所能处理的字节不会产生编译错误?

它不会因为溢出而引发运行时异常。

在这种情况下,两个数字加在一起会突然产生一个非常小的数字。由于byte的范围很小,很容易溢出;例如,加1到127就可以了,结果是-128。

它的主要原因是Java处理原始值转换的方式;在这种情况下,我们讨论的是缩小转换。也就是说,即使产生的总和大于byte,但缩小转换将导致丢弃信息,以允许数据放入byte,因为此转换不会导致运行时异常。

要逐步分解场景:

  • Java将EDCOX1×5和EDCOX1〔6〕相加在一起产生132。
  • Java理解EDCOX1×7和EDCOX1×8是EDCOX1×1的类型,所以结果也必须是EDCOX1×1的类型。
  • 这个整数的结果仍然是132,但是在这一点上,Java将执行一个CAST以将结果缩小到一个字节内——有效地给您EDCOX1×11。
  • 现在,由于环绕,az都包含结果-124。


答案由JLS 15.26.2提供:

For example, the following code is correct:

short x = 3;

x += 4.6;

and results in x having the value 7 because it is equivalent to:

short x = 3;

x = (short)(x + 4.6);

因此,正如您所看到的,最新的情况实际上是有效的,因为加法赋值(与任何其他运算符赋值一样)对左侧类型执行隐式强制转换(在您的情况下,abyte)。扩展,相当于byte e = (byte)(a + b);,可以很好地编译。


I came to the conclusion that the result of an expression that involves variables cannot be guaranteed. The resulting value can be within or outside the byte range so compiler throws off an error.

不,这不是原因。静态类型语言的编译器是这样工作的:任何变量都必须声明和类型化,因此即使在编译时它的值未知,它的类型也是已知的。隐式常量也是如此。基于这一事实,计算比例的规则基本上是:

  • 任何变量的小数位数必须与表达式右侧的小数位数相同或更高。
  • 任何表达式都具有与所涉及的最大项相同的比例。
  • 一个明确的铸造力,corse,右侧表达式的比例。

(这些实际上是一个简化的视图;实际上可能更复杂一些)。

适用于您的案例:

1
byte d = 1 + b

实际比例为:

1
byte = int + byte

…(因为1被认为是一个隐含的int常数)。因此,应用第一个规则,变量必须至少具有int尺度。

在这种情况下:

1
byte z = (a+=b);

实际比例为:

1
byte = byte += byte

…没关系。

更新

那么,为什么byte e = a + b会产生编译时错误?

正如我所说的,Java中的实际类型规则更为复杂:虽然一般规则适用于所有类型,但原始EDCOX1 6和EDCOX1 36种类型更受限制:编译器假定添加/减除两个或多个字节/短裤可能导致溢出(如@ Makto声明),因此需要存储为下一个类型。规模被认为是"更安全":一个int


我以前在一个项目中遇到过这种情况,我学到了:

与C/C++不同,Java总是使用有符号的原语。一个字节是从-128到+127,所以如果在这个范围之后分配任何内容,它将给您带来编译错误。

如果您显式地转换为像(byte) 150这样的字节,仍然无法得到您想要的(您可以使用调试器进行检查,看到它将转换为其他东西)。

当使用像x = a + b这样的变量时,因为编译器在运行时不知道这些值,并且无法计算-128 <= a+b <= +127是否会出错。

关于您的问题,为什么编译器不会在诸如a+=b之类的东西上给出错误:

从OpenJDK中挖掘Java编译器

http://hg.openjdk.java.net/jdk9/jdk9/langtools.

我跟踪了操作数的树处理过程,在一个编译器文件Lower.java中找到了一个有趣的表达式,该文件部分负责遍历编译器树。下面是一些有趣的代码(assignop用于所有操作数,如+=-=/=…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void visitAssignop(final JCAssignOp tree) {
                        ...
                        Symbol newOperator = operators.resolveBinary(tree,
                                                                      newTag,
                                                                      tree.type,
                                                                      tree.rhs.type);
                        JCExpression expr = lhs;
                        //Interesting part:
                        if (expr.type != tree.type)
                            expr = make.TypeCast(tree.type, expr);
                        JCBinary opResult = make.Binary(newTag, expr, tree.rhs);
                        opResult.operator = newOperator;:

                        ....

如您所见,如果rhs的类型与lhs的类型不同,则会发生类型转换,因此,即使您声明floatdouble在右侧(a+=2.55)上,也不会因类型转换而出现错误。


基本原因是编译器在涉及常量时的行为略有不同。所有整型文字都被视为int常量(除非它们在末尾有LL)。通常情况下,不能将int分配给byte。但是,在涉及常量的地方有一个特殊的规则;请参见JLS 5.2。基本上,在像byte b = 5;这样的声明中,5int的一种,但将其"窄化"转换为byte是合法的,因为5是一个常数,并且它符合byte的范围。这就是为什么允许使用byte b = 5,而不允许使用byte b = 130

但是,byte z = (a += b);是另一种情况。a += b只是将b加到a上,返回a的新值,该值分配给a。由于a是一个字节,因此不涉及收缩转换——您将一个字节分配给一个字节。(如果aint的话,这个程序总是非法的。)

规则规定,a + b(因此a = a + ba += b不会溢出。如果结果在运行时对于一个字节来说太大,那么上面的位就会丢失——值会被环绕。此外,编译器不会"value follow"注意到a + b将大于127;即使我们可以知道该值将大于127,编译器也不会跟踪以前的值。据它所知,当它看到a += b时,它只知道程序在运行时会将b添加到a中,而不查看以前的声明来查看值是什么。(一个好的优化编译器实际上可以做这种工作。但是我们讨论的是什么使程序合法与否,而有关合法性的规则本身并不关心优化。)


表达byte1+byte2相当于(int)byte1+(int)byte2,具有int型。虽然表达式x+=y;通常等同于var1=var1+var2;,但这样的解释将使使用值小于int+=成为不可能,因此编译器将byte1+=byte2视为byte1=(byte)(byte1+byte2);

请注意,Java的类型系统最初是为了简单起见而设计的,它的规则在很多情况下被选择为有意义的,但是因为使规则简单化比使它们始终合理更重要,所以有许多类型系统规则产生无意义行为的情况。其中一个更有趣的例子是:

1
2
long l1 = Math.round(16777217L)
long l2 = Math.round(10000000000L)

当然,在现实世界中,人们不会试图将长常量取整,但如果出现以下情况,情况可能会出现:

1
long distInTicks = Math.round(getDistance() * 2.54);

已更改以消除比例因子[并且getDistance()返回long]。您希望l1和l2能收到什么值?你能弄明白他们为什么会得到其他价值吗?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
 * Decompiled Result with CFR 0_110.
 */

class Test {
    Test() {
    }

    public static /* varargs */ void main(String ... arrstring) {
        int n = 127;
        int n2 = 5;
        byte by = (byte)(n + n2);
        n = by;
        byte by2 = by;
    }
}

在代码反编译之后

1
2
3
4
5
6
7
class Test{
public static void main(String... args){
byte a = 127;
byte b = 5;
byte z = (a+=b); // no error, why ?
}
}

在内部,Java用EDCOX1×1代码替换了您的EDCOX1×0运算符。