英文:
Effectively final vs final - Different behavior
问题
到目前为止,我认为 effectively final 和 final 在很大程度上是等价的,而且如果不是在实际行为上几乎相同,JLS(Java 语言规范)应该会对它们进行类似的处理。然后我发现了这个人为构造的场景:
final int a = 97;
System.out.println(true ? a : 'c'); // 输出 a
// 相比之下
int a = 97;
System.out.println(true ? a : 'c'); // 输出 97
显然,JLS 在这两种情况下对这两者之间做出了重要的区分,我不确定为什么。
我阅读了其他类似的讨论,比如
但它们没有详细探讨这个问题。从更广泛的角度来看,它们似乎基本上是等价的。但是,深入挖掘后,它们显然是不同的。
是什么导致了这种行为,有人能够提供一些能够解释这一点的 JLS 定义吗?
编辑:我发现了另一种相关的情况:
final String a = "a";
System.out.println(a + "b" == "ab"); // 输出 true
// 相比之下
String a = "a";
System.out.println(a + "b" == "ab"); // 输出 false
因此,在这里字符串的内部处理也表现出不同的行为(我不想在真实代码中使用这个片段,只是对不同的行为感到好奇)。
英文:
So far I thought that effectively final and final are more or less equivalent and that the JLS would treat them similar if not identical in the actual behavior. Then I found this contrived scenario:
final int a = 97;
System.out.println(true ? a : 'c'); // outputs a
// versus
int a = 97;
System.out.println(true ? a : 'c'); // outputs 97
Apparently, the JLS makes an important difference between the two here and I am not sure why.
I read other threads like
- Difference between final and effectively final
- Effectively final variable vs final variable
- What does a variable being “effectively final” mean?
but they do not go into such detail. After all, on a broader level they appear to be pretty much equivalent. But digging deeper, they apparently differ.
What is causing this behavior, can anyone provide some JLS definitions that explain this?
Edit: I found another related scenario:
final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true
// versus
String a = "a";
System.out.println(a + "b" == "ab"); // outputs false
So the string interning also behaves differently here (I dont want to use this snippet in real code, just curious about the different behavior).
答案1
得分: 65
首先,我们只讨论局部变量。_有效地终态_不适用于字段。这很重要,因为final
字段的语义非常不同,并且受到严格的编译器优化和内存模型承诺的影响,参见$17.5.1关于final字段的语义。
在表面上,对于局部变量来说,final
和有效地终态
是相同的。然而,Java语言规范(JLS)在这两者之间做出了明确的区分,实际上在特殊情况下会产生广泛的影响,就像这个例子一样。
前提
来自于JLS§4.12.4关于final
变量:
> 一个常量变量是一个用常量表达式(§15.29)初始化的基本类型或String类型的final
变量。变量是否为常量变量可能会影响类初始化(§12.4.1)、二进制兼容性(§13.1)、可达性(§14.22)和明确赋值(§16.1.1)。
由于int
是基本类型,变量a
是这样一个常量变量。
此外,从同一章节关于有效地终态
:
> 某些未声明为final
的变量被视为有效地终态:...
因此,从这种措辞方式可以清楚地看出,在另一个例子中,a
不被视为常量变量,因为它不是final,而仅仅是有效地终态。
行为
既然我们有了这个区分,让我们看看发生了什么,以及为什么输出不同。
在这里,您使用了条件运算符? :
,因此我们必须检查其定义。来自JLS§15.25:
> 有三种条件表达式,根据第二个和第三个操作数表达式进行分类:布尔条件表达式、数值条件表达式和引用条件表达式。
在这种情况下,我们正在讨论数值条件表达式,来自JLS§15.25.2:
> 数值条件表达式的类型如下确定:
这就是区分这两种情况的部分。
有效地终态
与有效地终态
相匹配的版本是通过以下规则匹配的:
> 否则,对第二个和第三个操作数应用一般的数值提升(§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
使用final
变量的版本与以下规则相匹配:
> 如果操作数之一是类型为T
(其中T
为byte
、short
或char
),并且另一个操作数是类型为int
的常量表达式(§15.29),其值在类型T
中可表示,则条件表达式的类型为T
。
最终变量a
的类型是int
,并且是常量表达式(因为它是final
)。它可以表示为char
,因此结果的类型为char
。这解释了输出为a
。
字符串示例
字符串相等性的示例基于相同的核心差异,final
变量被视为常量表达式/变量,而有效地终态
则不是。
在Java中,**
英文:
First of all, we are talking about local variables only. Effectively final does not apply to fields. This is important, since the semantics for final
fields are very distinct and are subject to heavy compiler optimizations and memory model promises, see $17.5.1 on the semantics of final fields.
On a surface level final
and effectively final
for local variables are indeed identical. However, the JLS makes a clear distinction between the two which actually has a wide range of effects in special situations like this.
Premise
From JLS§4.12.4 about final
variables:
> A constant variable is a final
variable of primitive type or type String that is initialized with a constant expression (§15.29). Whether a variable is a constant variable or not may have implications with respect to class initialization (§12.4.1), binary compatibility (§13.1), reachability (§14.22), and definite assignment (§16.1.1).
Since int
is primitive, the variable a
is such a constant variable.
Further, from the same chapter about effectively final
:
> Certain variables that are not declared final are instead considered effectively final: ...
So from the way this is worded, it is clear that in the other example, a
is not considered a constant variable, as it is not final, but only effectively final.
Behavior
Now that we have the distinction, lets lookup what is going on and why the output is different.
You are using the conditional operator ? :
here, so we have to check its definition. From JLS§15.25:
> There are three kinds of conditional expressions, classified according to the second and third operand expressions: boolean conditional expressions, numeric conditional expressions, and reference conditional expressions.
In this case, we are talking about a numeric conditional expressions, from JLS§15.25.2:
> The type of a numeric conditional expression is determined as follows:
And that is the part where the two cases get classified differently.
effectively final
The version that is effectively final
is matched by this rule:
> Otherwise, general numeric promotion (§5.6) is applied to the second and third operands, and the type of the conditional expression is the promoted type of the second and third operands.
Which is the same behavior as if you would do 5 + 'd'
, i.e. int + char
, which results in int
. See JLS§5.6
> Numeric promotion determines the promoted type of all the expressions in a numeric context. The promoted type is chosen such that each expression can be converted to the promoted type, and, in the case of an arithmetic operation, the operation is defined for values of the promoted type. The order of expressions in a numeric context is not significant for numeric promotion. The rules are as follows:
>
> [...]
>
> Next, widening primitive conversion (§5.1.2) and narrowing primitive conversion (§5.1.3) are applied to some expressions, according to the following rules:
>
> In a numeric choice context, the following rules apply:
>
> If any expression is of type int
and is not a constant expression (§15.29), then the promoted type is int
, and other expressions that are not of type int
undergo widening primitive conversion to int
.
So everything is promoted to int
as a
is an int
already. That explains the output of 97
.
final
The version with the final
variable is matched by this rule:
> If one of the operands is of type T
where T
is byte
, short
, or char
, and the other operand is a constant expression (§15.29) of type int
whose value is representable in type T
, then the type of the conditional expression is T
.
The final variable a
is of type int
and a constant expression (because it is final
). It is representable as char
, hence the outcome is of type char
. That concludes the output a
.
String example
The example with the string equality is based on the same core difference, final
variables are treated as constant expression/variable, and effectively final
is not.
In Java, string interning is based on constant expressions, hence
"a" + "b" + "c" == "abc"
is true
as well (dont use this construct in real code).
See JLS§3.10.5:
> Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.29) - are "interned" so as to share unique instances, using the method String.intern
(§12.5).
Easy to overlook as it is primarily talking about literals, but it actually applies to constant expressions as well.
答案2
得分: 7
另一个方面是,如果变量在方法体中被声明为 final,它与作为参数传递的 final 变量的行为不同。
public void testFinalParameters(final String a, final String b) {
System.out.println(a + b == "ab");
}
...
testFinalParameters("a", "b"); // 输出 false
而在以下情况下:
public void testFinalVariable() {
final String a = "a";
final String b = "b";
System.out.println(a + b == "ab"); // 输出 true
}
...
testFinalVariable();
之所以会发生这种情况,是因为编译器知道使用 final String a = "a"
时,变量 a
将始终具有值 "a"
,因此 a
和 "a"
可以互换而无问题。不同的是,如果 a
没有被定义为 final
,或者它被定义为 final
但其值是在运行时分配的(如上面的示例中 a
是参数的情况),编译器在使用之前不知道任何信息。因此,连接操作发生在运行时,并且会生成一个新的字符串,而不会使用内部池(intern pool)。
基本上行为是这样的:如果编译器知道一个变量是常量,就可以将其与使用常量的方式一样使用。
如果变量没有被定义为 final(或者它是 final 的,但其值是在运行时定义的),那么编译器没有理由将其处理为常量,即使其值等于常量,并且其值从不改变。
英文:
Another aspect is that if the variable is declared final in the body of the method it has a different behaviour from a final variable passed as parameter.
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();
it happens because the compiler knows that using final String a = "a"
the a
variable will always have the "a"
value so that a
and "a"
can be interchanged without problems.
Differently, if a
is not defined final
or it is defined final
but its value is assigned at runtime (as in the example above where final is the a
parameter) the compiler doesn't know anything before its use. So the concatenation happens at runtime and a new string is generated, not using the intern pool.
Basically the behaviour is: if the compiler knows that a variable is a constant can use it the same as using the constant.
If the variable is not defined final (or it is final but its value is defined at runtime) there is no reason for the compiler to handle it as a constant also if its value is equal to a constant and its value is never changed.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论