问题描述
到目前为止,我认为有效的最终和最终的是等效的,并且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在这两者之间起了重要的作用,我不确定为什么。
我读了其他主题,例如
- Difference between final and effectively final
- Effectively final variable vs final variable
- What does a variable being “effectively final” mean?
,但它们没有详细说明。毕竟,从更广泛的意义上讲,它们似乎是等效的。但是深入研究,它们显然有所不同。
是什么原因导致这种行为,任何人都可以提供一些解释此问题的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
因此,字符串实习生在这里的行为也有所不同(我不想在实际代码中使用此代码段,只是对不同的行为感到好奇)。
解决方法
首先,我们仅谈论局部变量。 最终有效不适用于字段。这很重要,因为final
字段的语义非常不同,并且需要进行大量的编译器优化和内存模型保证,请参阅$17.5.1关于最终字段的语义。
在表面级别final
和effectively final
上局部变量的确相同。但是,JLS明确区分了这两者,而在这样的特殊情况下,它们实际上具有广泛的影响。
前提
来自JLS§4.12.4约final
个变量:
常量变量是原始类型或类型为 String 且已初始化为 constant的
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)应用于第二和第三操作数,条件表达式的类型为第二和第三操作数的提升类型。 / p>
与您执行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
变量的版本与此规则匹配:
如果其中一个操作数是
T
类型,其中T
是byte
,short
或char
,而另一个操作数是int
类型的常量表达式(§15.29),其值可以用类型T
表示,则条件表达式的类型为T
。
最终变量a
的类型为int
,并且是一个常量表达式(因为它是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)共享唯一的实例。
在谈论文字时很容易忽略,但实际上它也适用于常量表达式。
,另一方面是,如果在方法的主体中将变量声明为final,则其行为与作为参数传递的final变量会有不同的行为。
public void testFinalParameters(final String a,final String b) {
System.out.println(a + b == "ab");
}
...
testFinalParameters("a","b"); // Prints false
同时
public void testFinalVariable() {
final String a = "a";
final String b = "b";
System.out.println(a + b == "ab"); // Prints true
}
...
testFinalVariable();
之所以会这样,是因为编译器知道使用final String a = "a"
的{{1}}变量将始终具有a
的值,因此"a"
和a
可以互换而无需问题。
以不同的方式,如果未定义"a"
或a
或定义final
但在运行时分配了它的值(如上例中的final
参数),则编译器在使用之前一无所知。因此,连接发生在运行时,并且不使用内部缓冲池而生成了新的字符串。
基本上,行为是:如果编译器知道变量是常量,则可以将其使用与使用常量相同。
如果未将变量定义为final(或者它是final,但其值在运行时定义),则编译器没有理由将其视为常量,即使其值等于常量且其值从不改变了。