在Java中使用final关键字可以提高性能吗?

Abh*_*ain 332 java final

在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更频繁地使用课程......

  • @Abishek:通常建议使用`final`,因为它使代码更易于理解,并有助于发现错误(因为它使程序员的意图明确).由于这些样式问题,PMD可能建议使用`final`,而不是出于性能原因. (9认同)
  • 在这里,我将引用Effective Java,第2版,第15项,最小化可变性:`不可变类比可变类更容易设计,实现和使用.它们不容易出错并且更安全.此外,`一个不可变对象可以处于一个状态,即创建它的状态.vs`另一方面,可变对象可以具有任意复杂的状态空间.根据我的个人经验,使用关键字"final"应该突出开发人员倾向于不可变性的意图,而不是"优化"代码.我鼓励你阅读本章,引人入胜! (4认同)
  • @Abhishek:很多可能是特定于JVM的,并且可能依赖于非常微妙的上下文方面.例如,我相信HotSpot*服务器*JVM仍然允许在一个类中重写时内联虚拟方法,并在适当的情况下进行快速类型检查.但细节很难确定,并且可能会在发布之间发生变化. (3认同)
  • 其他答案表明,在变量上使用`final`关键字可以减少字节码的数量,这可能会对性能产生影响. (2认同)

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对性能有任何可衡量的影响.

  • 没有测试没有缺陷,考虑了热身.第二个测试是SLOWER,而不是更快.如果没有预热,第二次测试就会失败. (15认同)
  • 在testFinal()所有的时间返回相同的对象,从串池,因为最终的字符串和字符串字面串联的resust在编译时进行评估.testNonFinal()每次返回新对象,这就解释了速度的差异. (6认同)
  • 有缺陷的测试.早期的测试将对JVM进行预热并使以后的测试调用受益.重新排序测试,看看会发生什么.您需要在自己的JVM实例中运行每个测试. (4认同)
  • 是什么让你觉得这个场景不切实际?`String`连接比添加`Integers'要昂贵得多.静态地(如果可能的话)更有效率,这是测试显示的. (4认同)
  • 我重写了一下你的代码,以便对这两种情况进行100次测试.最终,最终的平均时间为0毫秒,非最终时间为9毫秒.将迭代计数增加到10M会将平均值设置为0 ms和75 ms.然而,非决赛的最佳表现是0毫秒.也许是VM的检测结果只是被扔掉了什么?我不知道,但无论如何,使用final都会产生重大影响. (2认同)

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.这证明了最终变量对性能的影响,至少对于这个简单的情况.

  • 这谈到了编译时的优化,意味着开发人员在编译时知道最终布尔变量的VALUE,如果首先是块,那么写入的整个点是什么,然后这个场景中不需要IF条件而不是任何意义?在我看来,即使这提高了性能,这首先是错误的代码,并且可以由开发人员自己优化,而不是将责任转交给编译器.问题主要是要询问使用final的常规代码中的性能改进.具有程序感. (13认同)
  • 这一点的意思是添加调试语句,如mel3kings所述.您可以在生成构建之前翻转变量(或在构建脚本中配置它),并在创建分发时自动删除所有代码. (4认同)
  • @ŁukaszLech 我从 Oreilly 的一本书中学到了这一点:Hardcore Java,在他们关于 Final 关键字的章节中。 (2认同)

wmi*_*ell 36

根据IBM的说法 - 它不适用于类或方法.

http://www.ibm.com/developerworks/java/library/j-jtp04223.html

  • 文章“04223”是2003年的。现在已经快十七岁了。那是……Java 1.4? (5认同)
  • ...根据 IBM 的说法,**它适用于字段**:https://www.ibm.com/developerworks/java/library/j-jtp1029/index.html#heading6 - 并且也被提升为最佳实践。 (2认同)

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.

  • [这不是反编译后的真实代码吗...](/sf/answers/1213465081/) (2认同)

sle*_*ske 13

你真的在问两个(至少)不同的情况:

  1. final 对于局部变量
  2. final 方法/类

Jon Skeet已经回答了2).关于1):

我不认为它有所作为; 对于局部变量,编译器可以推断出变量是否为final(只需检查它是否被赋值多次).因此,如果编译器想要优化仅分配一次的变量,则无论变量是否实际声明,都可以这样做final.

final 可能会对 protected/public class字段产生影响; 在那里,编译器很难找出该字段是否被多次设置,因为它可能发生在不同的类中(甚至可能没有加载).但即使这样,JVM也可以使用Jon描述的技术(乐观地优化,如果加载了一个确实会改变字段的类则恢复).

总之,我认为它没有任何理由可以帮助提高性能.所以这种微优化不太可能有所帮助.您可以尝试对其进行基准测试以确保,但我怀疑它会有所作为.

编辑:

实际上,根据TimoWestkämper的回答,在某些情况下final 可以提高课堂领域的表现.我纠正了.


Neo*_*ard 6

注意:不是java专家

如果我正确记住了我的java,使用final关键字提高性能的方法很少.我一直都知道它存在于"好代码" - 设计和可读性.


Mar*_*ggi 6

在 Java 中,我们使用关键字使事物变得不可变final,并且至少有 3 种方法可以使不可变性对代码性能产生真正的影响。这三点的共同点是让编译器或开发人员做出更好的假设:

  1. 更可靠的代码
  2. 更高性能的代码
  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 classes 是否相同,因为mutable class对我来说听起来没有那么有意义