为什么编译器没有为此添加操作提供错误?

时间:2016-02-22 06:07:29

标签: java

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

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

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

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

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

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

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

让我感到困惑的是,当我这样说时,编译器不会抛出错误:

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

为什么不给我一个错误?

7 个答案:

答案 0 :(得分:22)

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

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

由于overflow,它不会引发运行时异常。

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

它将要解决的主要原因是Java处理原始值转换的方式;在这种情况下,我们谈论的是a narrowing conversion。也就是说,即使生成的总和大于<{em>>,但缩小转换将导致信息被丢弃以允许数据适合byte,如此转换从不会导致运行时异常。

逐步分解您的情景:

  • Java将bytea = 127加在一起以生成132。
  • Java了解b = 5a属于b类型,因此结果也必须是byte类型。
  • 此结果的整数结果仍为132,但此时,Java将执行强制转换以将结果缩小到一个字节内 - 有效地为您提供byte
  • 现在,由于环绕,(byte)(a += b)a都包含结果-124。

答案 1 :(得分:5)

  

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

不,那不是原因。静态类型语言的编译器以这种方式工作:必须声明和键入任何变量,因此即使在编译时未知其值,其类型也是已知的。隐式常量也是如此。基于这一事实,计算尺度的规则基本上就是:

  • 任何变量必须与其右侧的表达式具有相同或更高的比例。
  • 任何表达式都与其所涉及的最大术语具有相同的比例。
  • 一个明确的施放力量,即科西嘉,右侧表达的规模。

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

将它应用于您的案例:

byte d = 1 + b

实际尺度为:

byte = int + byte

...(因为1被视为隐式int常量)。因此,应用第一个规则时,变量必须至少具有int比例。

在这种情况下:

byte z = (a+=b);

实际尺度为:

byte = byte += byte

......没关系。

<强>更新

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

正如我所说,java中的实际类型规则更复杂:虽然一般规则适用于所有类型,但原始byteshort类型更受限制:编译器假定添加/减去两个或多个字节/短路有可能导致溢出(如@Makoto所述),因此需要将其存储为被认为“更安全”的下一个类型:int

答案 2 :(得分:5)

答案由JLS 15.26.2提供:

  

例如,以下代码是正确的:

     

short x = 3;

     

x += 4.6;

     

并导致x的值为7,因为它相当于:

     

short x = 3;

     

x = (short)(x + 4.6);

因此,正如您所看到的,最新的情况实际上是有效的,因为添加赋值(与任何其他运算符赋值一样)对左手类型执行隐式强制转换(在您的情况下a是{{1 }})。扩展,相当于byte,它将很快编译。

答案 3 :(得分:4)

基本原因是当涉及常量时,编译器的行为略有不同。所有整数文字都被视为int常量(除非它们末尾有Ll)。通常,您无法将int分配给byte。但是,有一个特殊的规则,其中涉及常数;见JLS 5.2。基本上,在byte b = 5;之类的声明中,5int,但将“缩小”转换为byte 是合法的,因为 { {1}}是常量,因为符合5的范围。这就是为什么byte被允许而byte b = 5不被允许的原因。

但是,byte b = 130是另一种情况。 byte z = (a += b);只需将a += b添加到b,并返回新值a;该值已分配给a。由于a是一个字节,因此不涉及缩小转换 - 您将字节分配给一个字节。 (如果aa,则该程序将始终是非法的。)

规则规定int(因此a + ba = a + b)不会溢出。如果结果在运行时对于一个字节来说太大了,那么高位就会丢失 - 值会回绕。此外,编译器不会“重视”注意a += b大于127;即使我们可以判断该值大于127,编译器也不会跟踪以前的值。据它所知,当它看到a + b时,它只知道程序在运行时会向a += b添加b,并且它不会查看先前的声明以查看价值观将是。 (一个好的优化编译器实际上可能会做那种工作。但是我们讨论的是什么使程序合法化,而合法性的规则并不关心优化。)

答案 4 :(得分:4)

我之前在一个项目中遇到过这个,这就是我所学到的:

与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适用于所有操作数,如+ = - = / = ...)

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)因为类型转换你不会得到任何错误。

答案 5 :(得分:0)

/*
 * 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;
    }
}

反编译代码后

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

在内部,Java使用a+=b代码替换了您的(byte)(n+n2)运算符。

答案 6 :(得分:0)

表达式byte1+byte2相当于(int)byte1+(int)byte2,类型为int。虽然表达式x+=y;通常等同于var1=var1+var2;,但这样的解释会使+=无法使用小于int的值,因此编译器会处理byte1+=byte2 1}}为byte1=(byte)(byte1+byte2);

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

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

在现实世界中,人们不会试图绕过长常数,但是如果出现这样的情况可能会出现这种情况:

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

被更改为消除比例因子[和getDistance()返回long]。 你期望l1和l2应该得到什么价值?你能弄明白他们为什么会得到其他价值吗?