如果从数组中复制最终变量,为啥 Java 需要对最终变量进行显式强制转换?

Posted

技术标签:

【中文标题】如果从数组中复制最终变量,为啥 Java 需要对最终变量进行显式强制转换?【英文标题】:Why does Java require an explicit cast on a final variable if it was copied from an array?如果从数组中复制最终变量,为什么 Java 需要对最终变量进行显式强制转换? 【发布时间】:2017-08-18 23:17:17 【问题描述】:

从下面的代码开始...

byte foo = 1;
byte fooFoo = foo + foo;

当我尝试编译此代码时,我会收到以下错误...

错误:(5, 27) java: 不兼容的类型: 从 int 到 byte 的可能有损转换

...但是如果foo 是最终...

final byte foo = 1;
final byte fooFoo = foo + foo;

文件将编译成功。

继续下面的代码...

final byte[] fooArray = new byte[1];
fooArray[0] = 1;

final byte foo = fooArray[0];
fooArray[0] = 127;

System.out.println("foo is: " + foo);

... 将打印

foo is: 1

... 这很好。该值被复制到最终变量中,并且不能再更改。使用数组中的值不会改变 foo 的值(正如预期的那样......)。

为什么以下需要强制转换?

final byte[] fooArray = new byte[1];
fooArray[0] = 1;
final byte foo = fooArray[0];
final byte fooFoo = foo + foo;

这与该问题中的第二个示例有何不同?为什么编译器会出现以下错误?

错误:(5, 27) java: 不兼容的类型: 从 int 到 byte 的可能有损转换

怎么会这样?

【问题讨论】:

只是关于第二个示例的注释,您在其中从数组初始化 foo 并测试它不会改变。即使foo 不是final,它的值也不会改变。赋值(在这种情况下是初始化)在它发生的那一刻复制值(1),就是这样。 fooArray[0] 到 127 的后续更改将不会自动传播到 foo,无论它是否是最终的。 【参考方案1】:

这是因为

byte foo = 1;
byte fooFoo = foo + foo;

foo + foo = 2 将被回答,但 2 不是字节类型,因为 java 具有整数变量的默认数据类型,它的类型是 int。所以你需要强行告诉编译器 该答案必须是明确的字节类型。

class Example
    public static void main(String args[])

        byte b1 = 10;
        byte b2 = 20;
        byte b1b2 = (byte)(b1 + b2); 

        //~ b1 += 100;  // (+=) operator automaticaly type casting that means narrow conversion

        int tot = b1 + b2;

        //~ this bellow statement prints the type of the variable
        System.out.println(((Object)(b1 + b2)).getClass());  //this solve your problem
    

【讨论】:

【参考方案2】:

这确实是编译器在与final 一起使用时在常量折叠中所做的,正如我们从字节码中看到的那样:

    byte f = 1;
    // because compiler still use variable 'f', so `f + f` will 
    // be promoted to int, so we need cast
    byte ff = (byte) (f + f);
    final byte s = 3;
    // here compiler will directly compute the result and it know
    // 3 + 3 = 6 is a byte, so no need cast
    byte ss = s + s;
    //----------------------
    L0
    LINENUMBER 12 L0
    ICONST_1 // set variable to 1
    ISTORE 1 // store variable 'f'
    L1
    LINENUMBER 13 L1
    ILOAD 1 // use variable 'f'
    ILOAD 1
    IADD
    I2B        
    ISTORE 2 // store 'ff'
    L2

    LINENUMBER 14 L2
    ICONST_3 // set variable to 3
    ISTORE 3 // store 's'
    L3
    LINENUMBER 15 L3
    BIPUSH 6 // compiler just compute the result '6' and set directly
    ISTORE 4 // store 'ss'

如果你把最后一个字节改成 127,它也会报错:

    final byte s = 127;
    byte ss = s + s;

在这种情况下,编译器计算结果并知道它超出了限制,所以它仍然会抱怨它们不兼容。

更多:

而here 是另一个关于字符串常量折叠的问题:

【讨论】:

我只有手机可以接听,所以我跳过了字节码部分。很高兴你自愿这样做:-) 也展示了动机:当编译为字节码时,byte 变量被扩大到int 以便存储它们并用它们进行计算,除非右侧的操作数是final 常量,在这种情况下,计算可以在编译时静态发生。【参考方案3】:

值 1 非常适合一个字节; 1+1也是如此;当变量为final时,编译器可以执行constant folding。 (换句话说:编译器在执行 + 操作时不使用 foo;而是使用“原始”1 值)

但是当变量不是最终变量时,所有关于转换和提升的有趣规则都会生效(请参阅here;您想阅读第 5.12 节关于扩大原始转换的内容)。

第二部分:将数组设为 final 仍然允许您更改它的任何字段;又是这样;不可能不断折叠;这样“加宽”操作又开始了。

【讨论】:

【参考方案4】:

JLS (§5.2) 具有使用常量表达式进行赋值转换的特殊规则:

另外,如果表达式是byteshortcharint类型的常量表达式(§15.28):

如果变量的类型是byteshortchar,并且常量表达式的值可以用变量的类型表示,则可以使用缩小原语转换。

如果我们按照上面的链接,我们会在常量表达式的定义中看到这些

原始类型的文字和String 类型的文字 加法运算符+- 引用常量变量 (§4.12.4) 的简单名称 (§6.5.6.1)。

如果我们点击上面的第二个链接,我们会看到

原始类型或String 类型的变量,即final 并使用编译时常量表达式(§15.28) 进行初始化,称为常量变量

因此,foo + foo 只能分配给fooFoo,如果foo 是一个常量变量。将其应用于您的案例:

byte foo = 1; 没有定义一个常量变量,因为它不是final

final byte foo = 1; 确实定义了一个常量变量,因为它是final,并使用常量表达式(原始文字)进行了初始化。

final byte foo = fooArray[0]; 没有定义常量变量,因为它没有用常量表达式初始化。

注意fooFoo 本身是否是final 并不重要。

【讨论】:

但是最后一个问题如何:“这怎么会发生?” (其中 this = 从 int 到 byte 的可能有损转换) @KorayTugay 实际上,在您的示例中不会发生这种情况。但是编译器不知道;规范不允许它进行扣除。

以上是关于如果从数组中复制最终变量,为啥 Java 需要对最终变量进行显式强制转换?的主要内容,如果未能解决你的问题,请参考以下文章

为啥从类构造函数调用的方法应该是最终的? [复制]

为啥java中的serialVersionUID必须是静态的、最终的和long类型的? [复制]

为啥从类中访问类变量需要“自我”。在 Python 中? [复制]

如果存在 VLA,为啥仍然需要 malloc? [复制]

为啥使用类变量的这段代码不能像在 Java 中那样工作? [复制]

如果它是引用类型,为啥我可以清空一个数组? [复制]