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


104

到目前为止,我认为有效的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

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


2
非常有趣的问题!我希望Java在这两种情况下的行为都一样,但是现在我得到了启发。我问自己这是否始终是这种行为,或者在以前的版本中是否有所不同
Lino

8
@Lino在下面的伟大回答最后一个报价的写法是一样的所有回方式的Java 6:“如果一个操作数的类型是牛逼,其中牛逼byteshort或者char,另一个操作数是一个常量表达式类型,int其值可以在类型T中表示,那么条件表达式的类型是T。 ” ---甚至在伯克利找到了Java 1.0文档。相同的文字。---是的,一直都是这样。
安德里亚斯

1
您“发现”事情的方式很有趣:P不客气:)
phant0m

Answers:


66

首先,我们只在谈论局部变量有效的final不适用于字段。这一点很重要,因为final字段的语义非常不同,并且需要进行大量的编译器优化和内存模型保证,有关最终字段的语义,请参见$ 17.5.1

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


前提

JLS§4.12.4关于final变量:

常量变量是一个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节)应用于第二和第三操作数,条件表达式的类型为第二和第三操作数的提升类型。

这与您将要执行的行为相同5 + 'd',即int + char导致int。参见JLS§5.6

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

[...]

接下来,根据以下规则,将扩展原语转换(第5.1.2节)和缩小原语转换(第5.1.3节)应用于某些表达式:

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

如果任何表达式的类型的int并且是不是一个常量表达式§15.29),则提升的类型是int,那类型不是其他表达式int离岗加宽原语转换int

所以一切都被提升到intaint已。这就解释了的输出97

最后

带有final变量的版本通过以下规则匹配:

如果操作数之一的类型为T,其中Tbyteshort,或char,另一操作数是常量表达式§15.29型的)int,它的值是在式表示的T,则该条件表达式的类型是T

最终变量a的类型int和一个常量表达式(因为是final)。它可以表示为char,因此结果是类型char。到此结束输出a


字符串示例

具有字符串相等性的示例基于相同的核心差异,final变量被视为常量表达式/变量,effectively final事实并非如此。

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

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

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

参见JLS§3.10.5

此外,字符串文字总是引用类String的相同实例。这是因为使用方法(第12.5节)对字符串文字(或更一般而言作为常量表达式值的字符串(第15.29节))进行了插入,以便共享唯一的实例。String.intern

由于它主要涉及文字,因此很容易忽略,但实际上它也适用于常量表达式。


8
问题是,你会期望... ? a : 'c'有同样的表现是否a是一个变量常量。该表达式显然没有错。---相比之下,这a + "b" == "ab"是一个错误的表达式,因为需要使用来比较字符串equals()如何比较Java中的字符串?)。它“偶然地”在when时起作用的事实a是一个常量,这只是字符串文字中间的一个怪癖。
安德烈亚斯(Andreas)

5
@Andreas是的,但是请注意,字符串实习是Java明确定义的功能。这不是巧合,明天或在其他JVM中可能都会改变。"a" + "b" + "c" == "abc"必须true在任何有效的Java实现中。
Zabuzard

10
是的,这是一个定义明确的怪癖,但a + "b" == "ab"仍然是错误的表达。即使您知道a是一个常量,也很容易出错,无法调用equals()。或者说脆弱是一个更好的词,即将来维护代码时很可能会崩溃。
安德里亚斯

2
请注意,即使在有效的最终变量的主域中(即它们在lambda表达式中的使用),该差异也可能会更改运行时行为,即,它可以使捕获的lambda表达式与非捕获的lambda表达式之间产生差异,后者将评估为单例,但前者产生一个新对象。换句话说,(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);str为(not)时更改其结果final
Holger

7

另一个方面是,如果在方法的主体中将变量声明为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,但其值在运行时定义),则即使编译器的值等于常量且其值从不更改,编译器也没有理由将其作为常量处理。


4
没什么奇怪的:)
dbl

2
这是问题的另一方面。
Davide Lorenzo MARINO

5
finel应用于参数的关键字具有与final应用于局部变量不同的语义,等等……
dbl

6
在这里使用参数是不必要的混淆。您可以做得到final String a; a = "a";相同的行为
yawkat
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.