有效的最终与最终 - 不同的行为

到目前为止,我认为有效的最终最终或多或少是等效的,并且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

因此,字符串实习在这里的行为也有所不同(我不想在实际代码中使用此片段,只是对不同的行为感到好奇)。


答案 1

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

在表面层面上和局部变量确实是相同的。但是,JLS在两者之间做了明确的区分,这在这样的特殊情况下实际上具有广泛的影响。finaleffectively final


前提

来自 JLS§4.12.4 关于变量:final

常量变量是使用常量表达式§15.29) 初始化的基元类型或 String 类型的变量。变量是否为常量变量可能会对类初始化 (§12.4.1)、二进制兼容性 (§13.1)、可访问性 (§14.22) 和确定赋值 (§16.1.1) 产生影响。final

由于 是 基元的,所以变量就是这样一个常量变量inta

此外,从同一章关于:effectively final

某些未声明为最终状态的变量将被视为有效的最终变量:...

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


行为

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

您在此处使用的是条件运算符,因此我们必须检查其定义。根据 JLS§15.25:? :

有三种条件表达式,根据第二和第三操作数表达式进行分类:布尔条件表达式数值条件表达式引用条件表达式

在本例中,我们谈论的是 JLS§15.25.2 中的数字条件表达式

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

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

有效最终

与此规则匹配的版本:effectively final

否则,常规数值提升§5.6) 将应用于第二和第三操作数,条件表达式的类型是第二和第三操作数的升级类型。

这与您执行的行为相同,即,这会导致 。参见 JLS§5.65 + 'd'int + charint

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

[...]

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

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

如果任何表达式属于类型且不是常量表达式§15.29),则提升的类型为 ,而其他不属于类型的表达式将进行加宽基元转换为intintintint

因此,一切都被提升到已经的样子。这解释了 的输出。intaint97

最后

包含该变量的版本与此规则匹配:final

如果其中一个操作数的类型为 , 、 或 ,而另一个操作数是其值可在类型中表示的常量表达式§15.29),则条件表达式的类型为 。TTbyteshortcharintTT

最后一个变量是类型和常量表达式(因为它是 )。它可以表示为 ,因此结果是类型。输出到此结束。aintfinalcharchara


字符串示例

字符串相等的示例基于相同的核心差异,变量被视为常量表达式/变量,而不是常量表达式/变量。finaleffectively final

在Java中,字符串暂存基于常量表达式,因此

"a" + "b" + "c" == "abc"

也是(不要在实际代码中使用此构造)。true

参见 JLS§3.10.5

此外,字符串文本始终引用类 String 的同一实例。这是因为字符串文本 (或者更一般地说,作为常量表达式值的字符串 (§15.29) ) 被“暂存”,以便使用该方法 (§12.5) 共享唯一实例。String.intern

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


答案 2

另一个方面是,如果变量在方法主体中被声明为 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 是参数),则编译器在使用之前什么都不知道。因此,串联在运行时发生,并生成一个新字符串,而不是使用实习生池。final String a = "a"a"a"a"a"afinalfinala


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

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


推荐