Zab*_*uza 103 java final jls language-lawyer effectively-final
到目前为止,我认为有效的 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
Run Code Online (Sandbox Code Playgroud)
显然,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
Run Code Online (Sandbox Code Playgroud)
所以字符串实习在这里的行为也不同(我不想在实际代码中使用这个片段,只是对不同的行为感到好奇)。
Zab*_*uza 65
首先,我们只讨论局部变量。实际上 final不适用于字段。这很重要,因为final字段的语义非常不同,并且受到大量编译器优化和内存模型承诺的影响,请参阅$17.5.1最终字段的语义。
在表面上final和effectively final局部变量确实是相同的。但是,JLS 对两者进行了明确区分,这在这种特殊情况下实际上具有广泛的影响。
从JLS§4.12.4关于final变量:
甲常量变量是一个
final的可变原始类型或键入的字符串,其与初始化常量表达式(§15.29)。变量是否为常量变量可能会影响类初始化(第12.4.1 节)、二进制兼容性(第13.1 节)、可达性(第14.22 节)和明确赋值(第16.1.1 节)。
由于int是原始变量,变量a就是这样的常量变量。
此外,从同一章关于effectively final:
某些未声明为 final 的变量被认为是有效的 final:...
所以从这个措辞的方式,很明显的是,在其他例子中,a是不被认为是恒定的变量,因为它不是最终的,但只有有效的决赛。
现在我们有了区别,让我们看看发生了什么以及为什么输出不同。
您在? :这里使用了条件运算符,因此我们必须检查其定义。从JLS§15.25:
条件表达式分为三种,根据第二和第三操作数表达式分类:布尔条件表达式、数值条件表达式和引用条件表达式。
在这种情况下,我们谈论的是来自JLS§15.25.2的数字条件表达式:
数值条件表达式的类型确定如下:
这就是两种情况分类不同的部分。
effectively final此规则匹配的版本:
否则,通用数值提升(第5.6 节)将应用于第二个和第三个操作数,条件表达式的类型是第二个和第三个操作数的提升类型。
这与您所做的行为相同5 + 'd',即int + char导致int. 见JLS§5.6
数值提升确定数值上下文中所有表达式的提升类型。选择提升的类型,以便每个表达式都可以转换为提升的类型,并且在算术运算的情况下,为提升的类型的值定义操作。数值上下文中的表达式顺序对于数值提升并不重要。规则如下:
[...]
接下来,根据以下规则,对某些表达式应用加宽原语转换(第5.1.2 节)和缩小原语转换(第5.1.3 节):
在数字选择上下文中,以下规则适用:
如果任何表达式属于类型
int并且不是常量表达式(第15.29 节),则提升的类型为int,而其他非类型的表达式会int进行扩展原语转换为int。
所以一切都被提升到int为a是int已。这解释了97.
带有final变量的版本通过此规则匹配:
如果其中一个操作数的类型为
TwhereTisbyte,short, orchar,而另一个操作数是其值可在 type 中表示的类型的常量表达式(第15.29 节),则条件表达式的类型为。intTT
最终变量a是类型int和常量表达式(因为它是final)。它可表示为char,因此结果的类型为char。输出到此结束a。
字符串相等的示例基于相同的核心差异,final变量被视为常量表达式/变量,而effectively final不是。
在 Java 中,字符串实习基于常量表达式,因此
"a" + "b" + "c" == "abc"
Run Code Online (Sandbox Code Playgroud)
也是true(不要在实际代码中使用这个结构)。
此外,字符串字面量始终引用类 String 的同一个实例。这是因为字符串文字——或者更一般地说,作为常量表达式(第15.29 节)的值的字符串——被“嵌入”,以便使用方法
String.intern(第12.5 节)共享唯一实例。
容易被忽视,因为它主要讨论的是文字,但它实际上也适用于常量表达式。
另一个方面是,如果变量在方法体中被声明为 final,它与作为参数传递的 final 变量的行为不同。
public void testFinalParameters(final String a, final String b) {
System.out.println(a + b == "ab");
}
...
testFinalParameters("a", "b"); // Prints false
Run Code Online (Sandbox Code Playgroud)
尽管
public void testFinalVariable() {
final String a = "a";
final String b = "b";
System.out.println(a + b == "ab"); // Prints true
}
...
testFinalVariable();
Run Code Online (Sandbox Code Playgroud)
这是因为编译器知道使用final String a = "a"的a变量总是具有"a"使价值a和"a"可以毫无问题地互换。不同的是,如果a未定义final或已定义final但在运行时分配了其值(如上例中 final 是a参数),编译器在使用前不知道任何内容。因此连接发生在运行时并生成一个新字符串,而不是使用实习池。
基本上行为是:如果编译器知道一个变量是一个常量,可以像使用常量一样使用它。
如果变量不是 final 定义的(或者它是 final 但它的值是在运行时定义的),那么编译器没有理由将它作为常量处理,如果它的值等于常量并且它的值永远不会改变。