Effectively final vs final - 不同的行为
Effectively final vs final - Different behavior
到目前为止,我认为 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 在这里使两者之间存在重要差异,我不确定为什么。
我读了其他帖子,例如
- 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
字段的语义非常不同,并且受大量编译器优化和内存模型承诺的影响,请参阅 .5.1 final 字段的语义。
从表面上看,局部变量的 final
和 effectively final
确实是相同的。然而,JLS 对两者进行了明确的区分,在这种特殊情况下实际上具有广泛的影响。
前提
来自 JLS§4.12.4 关于 final
个变量:
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).
由于int
是原始变量,变量a
就是这样一个常量变量.
此外,来自关于effectively final
的同一章节:
Certain variables that are not declared final are instead considered effectively final: ...
所以从这个措辞来看,很明显在另一个例子中,a
是 而不是 被认为是常量变量,因为它是 不是最终的,但只有有效的最终。
行为
现在我们有了区别,让我们看看发生了什么以及输出不同的原因。
您在这里使用了条件运算符 ? :
,因此我们必须检查其定义。来自 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.
在这种情况下,我们正在谈论一个数值条件表达式,来自JLS§15.25.2:
The type of a numeric conditional expression is determined as follows:
这就是两个案例分类不同的部分。
最终有效
effectively final
版本与此规则相匹配:
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.
这与您执行 5 + 'd'
相同的行为,即 int + char
,结果是 int
。参见 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
.
因此所有内容都被提升为 int
,因为 a
已经是 int
。这解释了 97
.
的输出
决赛
具有final
变量的版本与此规则匹配:
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
.
最后的变量a
是int
类型的常量表达式(因为是final
)。它可以表示为 char
,因此结果的类型是 char
。结束输出 a
.
字符串示例
字符串相等的例子是基于相同的核心区别,final
变量被视为常量expression/variable,而effectively final
不是。
在Java中,string interning基于常量表达式,因此
"a" + "b" + "c" == "abc"
也是true
(不要在实际代码中使用这个结构)。
参见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).
很容易被忽视,因为它主要是在谈论文字,但它实际上也适用于常量表达式。
另一个方面是,如果变量在方法主体中声明为 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"
a
变量将始终具有 "a"
值,因此 a
和 "a"
可以是互换没有问题。
不同的是,如果 a
未定义 final
或已定义 final
但其值是在运行时分配的(如上例所示,final 是 a
参数)编译器在使用之前什么都不知道。因此连接发生在运行时并生成一个新字符串,而不是使用内部池。
基本上行为是:如果编译器知道变量是常量,则可以像使用常量一样使用它。
如果变量不是最终定义的(或者它是最终的但它的值是在运行时定义的)编译器没有理由将它作为常量处理,即使它的值等于常量并且它的值永远不会改变。
到目前为止,我认为 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 在这里使两者之间存在重要差异,我不确定为什么。
我读了其他帖子,例如
- 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
字段的语义非常不同,并且受大量编译器优化和内存模型承诺的影响,请参阅 .5.1 final 字段的语义。
从表面上看,局部变量的 final
和 effectively final
确实是相同的。然而,JLS 对两者进行了明确的区分,在这种特殊情况下实际上具有广泛的影响。
前提
来自 JLS§4.12.4 关于 final
个变量:
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).
由于int
是原始变量,变量a
就是这样一个常量变量.
此外,来自关于effectively final
的同一章节:
Certain variables that are not declared final are instead considered effectively final: ...
所以从这个措辞来看,很明显在另一个例子中,a
是 而不是 被认为是常量变量,因为它是 不是最终的,但只有有效的最终。
行为
现在我们有了区别,让我们看看发生了什么以及输出不同的原因。
您在这里使用了条件运算符 ? :
,因此我们必须检查其定义。来自 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.
在这种情况下,我们正在谈论一个数值条件表达式,来自JLS§15.25.2:
The type of a numeric conditional expression is determined as follows:
这就是两个案例分类不同的部分。
最终有效
effectively final
版本与此规则相匹配:
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.
这与您执行 5 + 'd'
相同的行为,即 int + char
,结果是 int
。参见 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 isint
, and other expressions that are not of typeint
undergo widening primitive conversion toint
.
因此所有内容都被提升为 int
,因为 a
已经是 int
。这解释了 97
.
决赛
具有final
变量的版本与此规则匹配:
If one of the operands is of type
T
whereT
isbyte
,short
, orchar
, and the other operand is a constant expression (§15.29) of typeint
whose value is representable in typeT
, then the type of the conditional expression isT
.
最后的变量a
是int
类型的常量表达式(因为是final
)。它可以表示为 char
,因此结果的类型是 char
。结束输出 a
.
字符串示例
字符串相等的例子是基于相同的核心区别,final
变量被视为常量expression/variable,而effectively final
不是。
在Java中,string interning基于常量表达式,因此
"a" + "b" + "c" == "abc"
也是true
(不要在实际代码中使用这个结构)。
参见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).
很容易被忽视,因为它主要是在谈论文字,但它实际上也适用于常量表达式。
另一个方面是,如果变量在方法主体中声明为 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"
a
变量将始终具有 "a"
值,因此 a
和 "a"
可以是互换没有问题。
不同的是,如果 a
未定义 final
或已定义 final
但其值是在运行时分配的(如上例所示,final 是 a
参数)编译器在使用之前什么都不知道。因此连接发生在运行时并生成一个新字符串,而不是使用内部池。
基本上行为是:如果编译器知道变量是常量,则可以像使用常量一样使用它。
如果变量不是最终定义的(或者它是最终的但它的值是在运行时定义的)编译器没有理由将它作为常量处理,即使它的值等于常量并且它的值永远不会改变。