在Java中,我们看到许多final
可以使用关键字的地方,但它的使用并不常见.
例如:
String str = "abc";
System.out.println(str);
Run Code Online (Sandbox Code Playgroud)
在上述情况下,str
可以final
但通常不会这样做.
当一个方法永远不会被覆盖时,我们可以使用final关键字.类似地,如果一个类不会被继承.
在任何或所有这些案例中使用final关键字是否真的能提高性能?如果是这样,那怎么样?请解释.如果正确使用final
真正重要的性能,应在Java程序员开发什么习惯,使关键字的最好用?
Jon*_*eet 270
通常不是.对于虚方法,HotSpot会跟踪方法是否实际被覆盖,并且能够在假设方法尚未被覆盖的情况下执行优化,例如内联- 直到它加载一个覆盖方法的类,此时它可以撤消(或部分撤消)这些优化.
(当然,这假设您正在使用HotSpot - 但它是迄今为止最常见的JVM,所以...)
在我看来,你应该final
基于清晰的设计和可读性而不是出于性能原因.如果你想改变出于性能的考虑什么,你应该弯曲清晰的代码走形之前执行适当的测量-这样你就可以决定实现任何额外的性能提升是否值得可读性较差/设计.(根据我的经验,它几乎不值得; YMMV.)
编辑:作为最后的领域已经提到,值得提出的是,无论如何,在清晰的设计方面,它们通常是一个好主意.它们还在跨线程可见性方面改变了保证行为:在构造函数完成之后,任何最终字段都保证在其他线程中立即可见.这可能是final
我经验中最常见的用法,虽然作为Josh Bloch的"继承设计或禁止它的设计"的经验支持者,我应该final
更频繁地使用课程......
rus*_*tyx 81
简答:不要担心!
答案很长:
在谈论最终局部变量时请记住,使用关键字final
将有助于编译器静态优化代码,这最终可能导致更快的代码.例如,a + b
下面示例中的最终字符串是静态连接的(在编译时).
public class FinalTest {
public static final int N_ITERATIONS = 1000000;
public static String testFinal() {
final String a = "a";
final String b = "b";
return a + b;
}
public static String testNonFinal() {
String a = "a";
String b = "b";
return a + b;
}
public static void main(String[] args) {
long tStart, tElapsed;
tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
testFinal();
tElapsed = System.currentTimeMillis() - tStart;
System.out.println("Method with finals took " + tElapsed + " ms");
tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
testNonFinal();
tElapsed = System.currentTimeMillis() - tStart;
System.out.println("Method without finals took " + tElapsed + " ms");
}
}
Run Code Online (Sandbox Code Playgroud)
结果?
Method with finals took 5 ms
Method without finals took 273 ms
Run Code Online (Sandbox Code Playgroud)
在Java Hotspot VM 1.7.0_45-b18上测试.
那么实际的性能提升了多少?我不敢说.在大多数情况下可能是边缘的(在这个综合测试中大约270纳秒,因为完全避免了字符串连接 - 这是一种罕见的情况),但在高度优化的实用程序代码中,它可能是一个因素.在任何情况下,原始问题的答案都是肯定的,它可能会提高性能,但最多只是略微提高.
除了编译时的好处,我找不到任何证据表明关键字的使用final
对性能有任何可衡量的影响.
mel*_*ngs 58
是的,它可以.这是final可以提升性能的实例:
条件编译是一种技术,其中代码行不会根据特定条件编译到类文件中.这可用于删除生产版本中的大量调试代码.
考虑以下:
public class ConditionalCompile {
private final static boolean doSomething= false;
if (doSomething) {
// do first part.
}
if (doSomething) {
// do second part.
}
if (doSomething) {
// do third part.
}
if (doSomething) {
// do finalization part.
}
}
Run Code Online (Sandbox Code Playgroud)
通过将doSomething属性转换为final属性,您告诉编译器每当它看到doSomething时,它应该根据编译时替换规则将其替换为false.编译器的第一遍改变代码的东西是这样的:
public class ConditionalCompile {
private final static boolean doSomething= false;
if (false){
// do first part.
}
if (false){
// do second part.
}
if (false){
// do third part.
}
if (false){
// do finalization part.
}
}
Run Code Online (Sandbox Code Playgroud)
完成此操作后,编译器将再次查看它,并发现代码中存在无法访问的语句.由于您使用的是高质量的编译器,因此它不喜欢所有那些无法访问的字节代码.所以它删除它们,你最终得到这个:
public class ConditionalCompile {
private final static boolean doSomething= false;
public static void someMethodBetter( ) {
// do first part.
// do second part.
// do third part.
// do finalization part.
}
}
Run Code Online (Sandbox Code Playgroud)
从而减少任何过多的代码,或任何不必要的条件检查.
编辑:举个例子,我们来看下面的代码:
public class Test {
public static final void main(String[] args) {
boolean x = false;
if (x) {
System.out.println("x");
}
final boolean y = false;
if (y) {
System.out.println("y");
}
if (false) {
System.out.println("z");
}
}
}
Run Code Online (Sandbox Code Playgroud)
使用Java 8编译此代码并使用反编译时,javap -c Test.class
我们得到:
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static final void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: ifeq 14
6: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #22 // String x
11: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: iconst_0
15: istore_2
16: return
}
Run Code Online (Sandbox Code Playgroud)
我们可以注意到编译的代码只包含非final变量x
.这证明了最终变量对性能的影响,至少对于这个简单的情况.
wmi*_*ell 36
根据IBM的说法 - 它不适用于类或方法.
http://www.ibm.com/developerworks/java/library/j-jtp04223.html
Eug*_*ene 14
令我感到惊讶的是,没有人真正发布了一些经过反编译的真实代码,以证明至少存在一些细微差别.
作为参考,已经针对javac
版本进行了测试8
,9
并且10
.
假设这个方法:
public static int test() {
/* final */ Object left = new Object();
Object right = new Object();
return left.hashCode() + right.hashCode();
}
Run Code Online (Sandbox Code Playgroud)
按原样编译此代码,会产生与存在时完全相同的字节代码final
(final Object left = new Object();
).
但是这一个:
public static int test() {
/* final */ int left = 11;
int right = 12;
return left + right;
}
Run Code Online (Sandbox Code Playgroud)
生产:
0: bipush 11
2: istore_0
3: bipush 12
5: istore_1
6: iload_0
7: iload_1
8: iadd
9: ireturn
Run Code Online (Sandbox Code Playgroud)
离开final
现场产生:
0: bipush 12
2: istore_1
3: bipush 11
5: iload_1
6: iadd
7: ireturn
Run Code Online (Sandbox Code Playgroud)
代码几乎是不言自明的,如果有一个编译时常量,它将被直接加载到操作数堆栈上(它不会像前面的例子一样存储到局部变量数组中bipush 12; istore_0; iload_0
) - 哪种有意义因为没有人可以改变它.
另一方面为什么在第二种情况下编译器不会产生istore_0 ... iload_0
超出我的情况,它不像0
是以任何方式使用该槽(它可能以这种方式缩小变量数组,但可能是我缺少一些内部细节,不能说清楚)
我很惊讶地看到这样的优化,考虑到小的javac
情况.至于我们应该总是使用final
?我甚至不打算写一个JMH
测试(我最初想要的),我确信diff的顺序是ns
(如果可能的话).这可能是一个问题的唯一地方是,当一个方法由于它的大小而无法内联时(并且声明final
会将该大小缩小几个字节).
还有两个final
需要解决的问题.首先是一个方法final
(从一个JIT
角度来看),这种方法是单形的 - 这些是最受欢迎的方法JVM
.
然后是final
实例变量(必须在每个构造函数中设置); 这些是重要的,因为它们将保证正确发布的参考,在这里触及一点,并且也完全由JLS
.
sle*_*ske 13
你真的在问两个(至少)不同的情况:
final
对于局部变量final
方法/类Jon Skeet已经回答了2).关于1):
我不认为它有所作为; 对于局部变量,编译器可以推断出变量是否为final(只需检查它是否被赋值多次).因此,如果编译器想要优化仅分配一次的变量,则无论变量是否实际声明,都可以这样做final
.
final
可能会对 protected/public class字段产生影响; 在那里,编译器很难找出该字段是否被多次设置,因为它可能发生在不同的类中(甚至可能没有加载).但即使这样,JVM也可以使用Jon描述的技术(乐观地优化,如果加载了一个确实会改变字段的类则恢复).
总之,我认为它没有任何理由可以帮助提高性能.所以这种微优化不太可能有所帮助.您可以尝试对其进行基准测试以确保,但我怀疑它会有所作为.
编辑:
实际上,根据TimoWestkämper的回答,在某些情况下final
可以提高课堂领域的表现.我纠正了.
在 Java 中,我们使用关键字使事物变得不可变final
,并且至少有 3 种方法可以使不可变性对代码性能产生真正的影响。这三点的共同点是让编译器或开发人员做出更好的假设:
正如许多其他回复和评论所述,使类不可变会导致代码更清晰、更易于维护,而使对象不可变使它们更易于处理,因为它们可以处于一种状态,因此这意味着更容易并发和优化完成任务所需的时间。
此外,编译器会警告您有关未初始化变量的使用,并且不允许您使用新值重新分配它。
如果我们谈论方法参数,final
如果您不小心对变量使用相同的名称,或重新分配它的值(使参数不再可访问),声明它们会让编译器抱怨。
对生成的字节码进行简单分析,应该可以解决性能问题:使用@rustyx在其回复中发布的代码的最小修改版本,您可以看到当编译器知道对象不会'时生成的字节码是不同的t 改变它们的值。
这就是代码:
public class FinalTest {
private static final int N_ITERATIONS = 1000000;
private static String testFinal() {
final String a = "a";
final String b = "b";
return a + b;
}
private static String testNonFinal() {
String a = "a";
String b = "b";
return a + b;
}
private static String testSomeFinal() {
final String a = "a";
String b = "b";
return a + b;
}
public static void main(String[] args) {
measure("testFinal", FinalTest::testFinal);
measure("testSomeFinal", FinalTest::testSomeFinal);
measure("testNonFinal", FinalTest::testNonFinal);
}
private static void measure(String testName, Runnable singleTest){
final long tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
singleTest.run();
final long tElapsed = System.currentTimeMillis() - tStart;
System.out.printf("Method %s took %d ms%n", testName, tElapsed);
}
}
Run Code Online (Sandbox Code Playgroud)
使用openjdk17编译它:javac FinalTest.java
然后反编译:javap -c -p FinalTest.class
导致这个字节码:
private static java.lang.String testFinal();
Code:
0: ldc #7 // String ab
2: areturn
private static java.lang.String testNonFinal();
Code:
0: ldc #9 // String a
2: astore_0
3: ldc #11 // String b
5: astore_1
6: aload_0
7: aload_1
8: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: areturn
private static java.lang.String testSomeFinal();
Code:
0: ldc #11 // String b
2: astore_0
3: aload_0
4: invokedynamic #17, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
9: areturn
// omitted bytecode for the measure() method, which is not interesting
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,在某些情况下final
关键字会产生影响。
为了完整起见,这些是测量的时间:
方法 testFinal 花了 5 毫秒
方法 testSomeFinal 花了 13 毫秒
方法 testNonFinal 花了 20 毫秒
这些时间似乎无关紧要(考虑到我们完成了 100 万个任务),但我认为,一段时间后,JIT 优化正在发挥其魔力并消除差异,但即使如此,考虑到 4 倍也不是那么可以忽略不计到了testNonFinal
轮到的时候,JVM已经通过前面的测试预热了,公共代码还需要优化。
更少的字节码也意味着更容易和更短的内联,因此可以更好地利用资源和更好的性能。
Java 开发人员可以编写在服务器、台式机和小型或嵌入式设备上运行的代码,因此使代码在编译时更加高效(而不是完全依赖 JVM 优化)可以在所有运行时节省内存、时间和精力,并导致更少的并发问题和错误。
如果对象具有最终或不可变字段,则它们的状态无法更改,并且在创建它们时更容易估计它们所需的内存(因此这会导致更少的重定位)并且需要更少的防御副本:在 getter 中,我可以直接共享不可变的对象对象,而不创建防御性副本。
最后还有一点关于未来的可能性:当 Valhalla 项目将迎来阳光并且“值类”可用时,将不变性应用于对象的字段对于那些想要使用它们的人来说将是一个显着的简化,并利用大量的 JIT - 可能出现的编译器优化。
如果变量、对象的属性和方法的参数在 Java 中默认是不可变的(就像在 Rust 中一样),那么开发人员将被迫编写更干净、性能更好的代码,并显式声明mutable
所有可以改变其值的对象使开发人员更加意识到可能的错误。
我不知道 for final class
es 是否相同,因为mutable class
对我来说听起来没有那么有意义