【问题标题】:Effectively final vs final - Different behavior有效地最终与最终 - 不同的行为
【发布时间】:2020-12-23 13:57:14
【问题描述】:

到目前为止,我认为 effectively finalfinal 或多或少是等价的,如果在实际行为中不完全相同,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

所以字符串实习在这里也表现不同(我不想在实际代码中使用这个 sn-p,只是好奇不同的行为)。

【问题讨论】:

  • 非常有趣的问题!我希望 Java 在这两种情况下的行为都相同,但我现在很受启发。我在问自己这是否总是这种行为,或者它在以前的版本中是否有所不同
  • @Lino 下面最佳答案中最后一句话的措辞与Java 6 相同:“如果其中一个操作数的类型为 T其中 Tbyteshortchar,另一个操作数是 int 类型的常量表达式,其值可在类型 T 中表示,则条件表达式的类型为T。" --- 甚至在伯克利找到了一个 Java 1.0 文档。 Same text。 --- 是的,一直都是这样。
  • 你“发现”事物的方式很有趣 :P 不客气 :)

标签: java language-lawyer final jls effectively-final


【解决方案1】:

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

从表面上看,局部变量的finaleffectively final 确实是相同的。但是,JLS 对两者进行了明确的区分,在这种特殊情况下实际上具有广泛的影响。


前提

来自JLS§4.12.4 关于final 变量:

常量变量原始类型字符串类型的final变量,使用常量表达式 (§15.29)。变量是否为常量变量可能会影响与类初始化 (§12.4.1)、二进制兼容性 (§13.1)、可达性 (§14.22) 和明确赋值 (@ 987654327@).

由于int是原始的,变量a就是这样一个常量变量

另外,来自同一章关于effectively final

某些未声明为 final 的变量反而被认为是有效的 final:...

所以从措辞的方式来看,很明显在另一个例子中,a被认为是一个常量变量,因为它不是最终的,但只有有效。


行为

现在我们有了区别,让我们看看发生了什么以及为什么输出不同。

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

条件表达式分为三种,按第二和第三个操作数表达式分类:布尔条件表达式数值条件表达式引用条件表达式.

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

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

这就是两个案例分类不同的部分。

实际上是最终的

effectively final 的版本符合这条规则:

否则,一般numeric Promotion§5.6)应用于第二个和第三个操作数,条件表达式的类型是第二个和第三个操作数的提升类型。

这与您执行5 + 'd' 的行为相同,即int + char,结果为int。见JLS§5.6

数字提升确定数字上下文中所有表达式的提升类型。选择提升类型使得每个表达式都可以转换为提升类型,并且在算术运算的情况下,该操作是为提升类型的值定义的。数值上下文中的表达式顺序对于数值提升并不重要。规则如下:

[...]

接下来,加宽基元转换§5.1.2)和窄基元转换§5.1.3)根据以下规则应用于某些表达式:

在数字选择上下文中,以下规则适用:

如果任何表达式的类型为 int 并且不是常量表达式 (§15.29),则提升的类型为 int,以及其他不属于 @987654356 类型的表达式@经历加宽原语转换int

所以一切都被提升为int,因为a 已经是int。这解释了97 的输出。

最终

带有final 变量的版本符合这条规则:

如果其中一个操作数是T 类型,其中Tbyteshortchar,而另一个操作数是常量表达式 (@987654335 @) 的类型为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)。

很容易忽略,因为它主要是在谈论文字,但它实际上也适用于常量表达式。

【讨论】:

  • 问题在于,无论a变量 还是常量,您都希望... ? a : 'c' 的行为相同。表达方式显然没有任何问题。 --- 相比之下,a + "b" == "ab" 是一个错误的表达式,因为字符串需要使用equals() (How do I compare strings in Java?) 进行比较。当aconstant 时,它“意外地”起作用,这只是字符串文字实习的一个怪癖。
  • @Andreas 是的,但请注意 string interning 是 Java 明确定义的特性。明天或在不同的 JVM 中可能会发生变化,这不是巧合。在任何有效的 Java 实现中,"a" + "b" + "c" == "abc" 必须是 true
  • 没错,这是一个定义明确的怪癖,但a + "b" == "ab" 仍然是一个错误的表达方式。即使您知道 a 是一个常量,也不调用equals() 也太容易出错了。或者也许 fragile 是一个更好的词,即在将来维护代码时太可能分崩离析。
  • 请注意,即使在有效最终变量的主域中,即它们在 lambda 表达式中的使用,差异也可能会改变运行时行为,即它可以在捕获和非捕获 lambda 之间产生差异表达式,后者评估为单例,但前者产生一个新对象。换句话说,当str 是(不是)final 时,(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); 会改变其结果。
【解决方案2】:

另一方面,如果变量在方法体中声明为 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 参数),编译器在它之前不知道任何东西采用。所以连接发生在运行时并生成一个新字符串,而不是使用实习池。


基本上行为是:如果编译器知道变量是常量,则可以像使用常量一样使用它。

如果变量没有定义为 final(或者它是 final 但它的值是在运行时定义的),编译器没有理由将它作为一个常量处理,如果它的值等于一个常量并且它的值永远不会改变了。

【讨论】:

  • 这没什么奇怪的:)
  • 这是问题的另一方面。
  • finel 关键字应用于参数,与final 应用于局部变量等具有不同的语义......
  • 这里使用参数是不必要的混淆。您可以只做final String a; a = "a"; 并获得相同的行为
猜你喜欢
  • 2018-09-15
  • 2014-01-23
  • 2021-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-27
  • 1970-01-01
相关资源
最近更新 更多