Java中如何使用“final”关键字进行向下转型

38

请考虑:

class UnderstandingConversion {
    public static void main(String[] args) {

        int a=10, b=20;

        byte c = (a>b) ? 40 : 50;

        System.out.println(c);
        // output : lossy conversion error
    }
}

这段代码出现了“丢失精度转换”的错误,我理解了。但是,如果我在代码中使用如下所示的final关键字,则可以正常工作。

class UnderstandingConversion {
    public static void main(String[] args) {

        final int a=10, b=20;

        byte c = (a>b)? 40 : 50;

        System.out.println(c);
        // Output: 50

    }
}

final 在这里是如何起作用的?向下转型是如何进行的?


5
假定“最终值”能够为编译器提供足够的信息,以推断出转换实际上是正确的。 - kutschkem
4
将具有常量值的最终变量称为常量表达式,这使得整个三元表达式成为常量表达式。如果常量值在字节范围内,则int类型的常量表达式可以赋值给byte类型的变量。 - Erwin Bolwidt
4
final关键字可以使编译器确信变量a和b的值不会发生改变。因此,编译器知道(a>b)? 40 : 50;将始终为(10>20)? 40 : 50;,这等同于(false)? 40 : 50;,进而相当于50。因此,当您使用final关键字时,编译器将节省我们在每次调用main方法时评估(10>20)? 40 : 50;的时间,并将其替换为结果50(因为它永远不会改变)。这意味着您最终得到的是byte c = (a>b)? 40 : 50;(我没有时间查找规范并引用它,因此不想将其发布为答案)。 - Pshemo
2
非最终版本被视为 (condition) ? someInt : someInt,这导致编译器看到 byte c = (a>b)? 40 : 50;byte c = someInt;,这就是为什么会出现错误的原因。其中一个解决方案可以将其中一个结果强制转换为 byte,例如 byte c = (a>b) ? 40 : (byte)50;,这将强制另一个结果也被视为 byte(需要在规范中找到它)。 - Pshemo
1
@MatteoNNZ 是的,编译器在这里可能会更聪明一些,但显然没有太多收益(这种情况可能太少了),因此没有必要为这种情况进行改进。 关于“..为什么将40或50转换为字节(如您所示)会使其再次工作?”因为编译器必须确保两个结果是相同类型的,在这种情况下,它不会将byte扩展为int,而是设计为缩小类型。因此,byte c = (a>b) ? (byte)40 : 50;强制将50(即int)视为byte(将int缩小为byte,因为另一个值是byte)。 - Pshemo
显示剩余7条评论
3个回答

33

JLS规定了赋值转换的预定义规则

如果表达式是byte、short、char或int类型的常量表达式(§15.28):

如果变量的类型是byte、short或char,并且常量表达式的值可以表示为变量的类型,则可以使用缩小原始转换。

常量表达式的定义:

  • 原始类型和字符串类型的字面值
  • 简单名称 (§6.5.6.1),指代常量变量 (§4.12.4)。
  • 关系运算符 <、<=、> 和 >=(但不包括 instanceof)(§15.20)

§4.12.4 定义了什么是 final 变量。

一个基本类型或字符串类型的变量,如果它是 final 的并且被初始化为编译时常量表达式 (§15.28),则称为常量变量。


@SotiriosDelimanolis 谢谢您指出,已添加关系运算符。 - aviad
抱歉,我不明白那如何解释这种行为。(a>b)? 40 : 50既不是“[原始类型]的文字”也不是“[字符串类型]的文字”,也不是“[简单名称]”,更不是“[关系运算符<、<=、>或>]”。 - Heinzi
3
@Heinzi,报价还应包括“常量表达式是指原始类型或字符串的值,不会突然中断并仅使用以下内容组成:”并将“三元条件运算符? :”列为允许的结构之一。 - HTNW

25

编译器能够百分之百确定 a>b 将会得出 false - 因此它只考虑了三目运算符的 else 部分 - 而且编译器知道常量 50 能适配 byte 的范围,所以它在编译时不需要强制转换,而是直接定义为 byte。

要证明编译器只考虑了 else 的值,可以这样做:

    final int a=10, b=20;
    byte c = (a>b) ? 1234 : 50;

这将能够正常编译,但是会这样做

final int a=10, b=20;
byte c = (a>b) ? 50 : 1234;

会产生错误。1234超出了字节范围,但第一个示例中它完全被忽略,所以它是可以的。

我相信有更好专业知识的人可以指出JLS段落,解释这里发生了什么。

观察更新

事实证明,如果我们检查字节码,我们很可能会看到ab被替换为常量值(也许根本没有分支)。我已经使用调试器对该代码进行了调试,并在条件运算符之前将A值更改为10000,但执行结果仍然为c=50,尽管A>B现在为真。

这是一个有趣的事实,谢谢您提出问题,让我学到了新的东西。


1
“编译器知道常量50适合byte范围。”是的,但40也一样,对吧? - Eric Duminil
当然,但是再次强调,40被忽略了 - 这就是为什么非字节值会通过编译的原因。 - Antoniossss
@EricDuminil 语言设计者可能没有在条件为非常数时检查两个分支的常量表达式。 - xehpuk
@xehpuk 当条件是恒定的时。这是正确的。 - Antoniossss
这应该是被接受的答案!恭喜你提供了一个简单的解释,完全解决了这个问题。 - Panagiotis Bougioukos
显示剩余2条评论

2

如果您反编译使用了final的代码,并仅集中于对abc的赋值,您将得到以下内容:

   L0
    LINENUMBER 4 L0
    BIPUSH 10
    ISTORE 1
   L1
    BIPUSH 20
    ISTORE 2
   L2
    LINENUMBER 5 L2
    BIPUSH 50
    ISTORE 3

因此,可以看到,字节码分配给c的元素是50,这是一个常量字节值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接