有效终态 vs 终态 - 不同的行为

103

到目前为止,我认为 effectively final final 或多或少是等价的,并且JLS在实际行为上会将它们视为相似甚至相同。然后我发现了这种人为制造的情况:

final int a = 97;
System.out.println(true ? a : 'c'); // outputs a

// versus

int a = 97;
System.out.println(true ? a : 'c'); // outputs 97

显然,JLS在这两者之间有重要的区别,但我不确定为什么。

我阅读了其他类似的主题,比如:

但它们没有进一步解释。总体上看,它们似乎基本相同,但深入挖掘后,它们似乎存在差异。

是什么导致了这种行为?有人可以提供一些解释这个问题的JLS定义吗?


编辑:我发现另一个相关场景:

final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true

// versus

String a = "a";
System.out.println(a + "b" == "ab"); // outputs false

因此,字符串加速在这里的行为也有所不同(我不想在真实代码中使用此片段,只是对其不同的行为感到好奇)。


2
非常有趣的问题!我本来期望Java在这两种情况下的行为是相同的,但现在我已经有了新的认识。我在思考这是否一直是这样的行为,或者在之前的版本中有所不同。 - Lino
8
下面这个伟大答案中最后一句话的措辞一直保持不变,直到Java 6:“如果操作数之一是类型为T,其中Tbyteshortchar,另一个操作数是类型为int且值可在T类型中表示的常量表达式,则条件表达式的类型为T。”甚至在伯克利还发现了一个Java 1.0文档。相同的文本。是的,它一直保持那样。 - Andreas
1
你“找”东西的方式很有趣 :P 不用谢 :) - phant0m
2个回答

65

首先,我们只讨论关于局部变量的问题。 有效地被声明为 final 不适用于字段。这一点很重要,因为对于final字段的语义是非常明确的,并且受到编译器优化和内存模型承诺的重大影响,请参见$17.5.1有关最终字段语义的内容。

在表面上看,局部变量的final有效地被声明为 final确实是相同的。但是,JLS在两者之间做出了明确的区分,这实际上在特殊情况下产生了广泛的影响。


前提条件

根据JLS§4.12.4关于final变量的说明:

常量变量是一个final基本类型或String类型变量,它被初始化为一个常量表达式§15.29。一个变量是否为常量变量可能会涉及到类初始化§12.4.1、二进制兼容性§13.1、可达性§14.22和明确赋值§16.1.1方面的问题。

由于int是原始类型,变量a是一个常量变量

此外,在关于effectively final的同一章节中:

某些未声明为final的变量被视为有效地final:...

因此,从这种措辞方式可以清楚地看出,在另一个示例中,a不被认为是一个常量变量,因为它不是final,而仅仅是有效地final。


行为

既然我们有了区别,让我们查找一下正在发生什么以及为什么输出不同。

你在这里使用了条件运算符? :,因此我们必须检查它的定义。来自JLS§15.25

根据第二个和第三个操作数表达式分类,有三种条件表达式:布尔条件表达式数值条件表达式引用条件表达式

在这种情况下,我们谈论的是一个数值条件表达式,来自JLS§15.25.2

数值条件表达式的类型如下确定:

这就是两种情况被分类不同的部分。

有效地final

版本有效地final与此规则匹配:

否则,对第二个和第三个操作数应用一般的数值提升(§5.6),并且条件表达式的类型是第二个和第三个操作数的提升类型。

这与执行5 + 'd'时的行为相同,即int + char,其结果为int。请参见JLS§5.6
数字提升决定了数值上下文中所有表达式的提升类型。选择提升类型的原则是每个表达式都可以转换为提升类型,并且在算术运算的情况下,该操作对提升类型的值是有定义的。数字上下文中表达式的顺序对于数字提升来说不重要。规则如下:
[...]
接下来,根据以下规则将§5.1.2扩展原始转换和§5.1.3缩小原始转换应用于某些表达式:
在数字选择上下文中,遵循以下规则:
如果任何表达式的类型为int并且不是常量表达式(§15.29),则提升类型为int,而不是int类型的其他表达式会被转换为int进行扩展原始转换。
所以,由于a已经是int类型,因此所有内容都被提升为int。这就解释了输出结果97
使用final变量的版本符合以下规则:
如果一个操作数是T类型(其中Tbyteshortchar),另一个操作数是常量表达式§15.29)且类型为int,它的值可以表示为T类型,则条件表达式的类型为T
最终变量aint类型,并且是常量表达式(因为它是final)。它可以表示为char,因此结果的类型为char。这就是输出结果a的原因。

字符串示例

字符串相等性示例基于同样的核心差异,final变量被视为常量表达式/变量,而effectively final不是。

在Java中,字符串池化是基于常量表达式的,因此

"a" + "b" + "c" == "abc"

true也是这样的(在实际代码中不要使用这种构造方式)。

详见JLS§3.10.5

此外,字符串字面值总是引用类String的同一实例。这是因为字符串字面值或者更一般地,作为常量表达式的值(§15.29)的字符串,会被“interned”以共享唯一实例,使用方法String.intern§12.5)。

虽然主要讨论了字面值,但它实际上也适用于常量表达式,很容易被忽视。


8
问题在于,你本来期望 ... ? a : 'c'a变量常量 时的行为应该相同。这个表达式看起来并没有什么问题。相比之下,a + "b" == "ab" 是一个糟糕的表达式,因为字符串需要使用equals()进行比较(如何在Java中比较字符串?)。当 a常量时它“偶然”地工作只是因为字符串字面值的内部化机制导致的怪癖。 - Andreas
5
是的,但请注意字符串驻留(string interning)是Java的一个明确定义的特性。这不是一个可能会在明天或不同的JVM上改变的巧合。在任何有效的Java实现中,“a”+“b”+“c”==“abc”都必须为真。 - Zabuzard
10
没错,这是一个明确定义的怪癖,但是a + "b" == "ab"仍然是一个错误的表达式。即使你知道a是一个常量,不调用equals()仍然很容易出错。或者说脆弱可能更合适,也就是当代码在未来维护时容易崩溃。 - Andreas
2
请注意,即使在 effectively final 变量的主要领域——即它们在 lambda 表达式中的使用中,差异也可能会改变运行时行为,这可能导致捕获和非捕获 lambda 表达式之间的区别,后者计算为一个 singleton,但前者会产生一个新的对象。换句话说,(final) String str = "a"; Stream.of(null, null). <Runnable>map( x -> () -> System.out.println(str)) .reduce((a,b) -> () -> System.out.println(a == b)) .ifPresent(Runnable::run);str 是(不是)final 时会改变其结果。 - Holger

7
另一个方面是,如果变量在方法体中被声明为final,则其行为与作为参数传递的final变量不同。
public void testFinalParameters(final String a, final String b) {
  System.out.println(a + b == "ab");
}

...
testFinalParameters("a", "b"); // Prints false

while

public void testFinalVariable() {
   final String a = "a";
   final String b = "b";
   System.out.println(a + b == "ab");  // Prints true
}

...
testFinalVariable();

这种情况发生是因为编译器知道使用 final String a = "a" 时,变量 a 将始终具有值 "a",因此可以毫无问题地互换 a"a"
如果变量 a 没有定义为 final,或者定义为 final 但其值在运行时被分配(如上例中的 a 参数),编译器在使用之前就什么都不知道。因此字符串连接发生在运行时,并生成新的字符串,而不使用int pool。
基本上的行为是:如果编译器知道变量是常量,可以将其与常量一样使用。
如果该变量未定义为 final(或者它是 final,但其值在运行时定义),即使其值等于常量且其值永远不会更改,编译器也没有理由将其视为常量来处理。

4
那没有什么奇怪的 :) - dbl
2
这是问题的另一个方面。 - Davide Lorenzo MARINO
5
将关键字 finel 应用于参数,其语义与应用于局部变量的 final 关键字不同,等等... - dbl
6
这里使用参数是不必要的混淆。你只需这样做:final String a; a = "a";,就可以得到相同的结果。 - yawkat

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