在Java中使用final关键字是否能提高性能?

412

在Java中,我们可以看到许多地方都可以使用final关键字,但实际上它的使用并不常见。

比如:

String str = "abc";
System.out.println(str);
在上述情况下,`str` 可以是 `final` 的,但通常会省略。当一个方法永远不会被覆盖时,我们可以使用 `final` 关键字。同样地,在不会被继承的类的情况下也可以使用。是否在这些情况下使用 `final` 关键字真正提高了性能?如果是这样,那么它是如何做到的?请解释一下。如果适当使用 `final` 关键字确实对性能有影响,那么 Java 程序员应该养成什么样的习惯来最好地利用这个关键字?

5
我觉得这是一个典型的考试问题。我记得期末成绩确实对表现有影响,如果我没记错的话,final类可以在某种程度上通过JRE进行优化,因为它们不能被子类化。 - Kawu
我实际上已经测试过了。在我测试的所有JVM上,使用final关键字修饰局部变量确实提高了性能(虽然略微,但仍然可能是实用方法中的一个因素)。源代码在我的下面回答中。 - rustyx
在进行性能检查时,最好使用像Caliper这样的工具来进行微基准测试。 - Archimedes Trajano
唯一需要关注使用final的性能收益的情况是:您有一些非常小的方法,例如“long xor(long v1,long v2){return v1 ^ v2;};”,而这个方法占用了相当大的CPU时间。通过将v1和v2标记为final,您可以避免不必要的变量复制(显然,这比操作本身需要更多的时间)。 - Ivan Zaitsau
3
"premature optimization is the root of all evil". 让编译器自己去做它的工作吧。写出易读且有注释的代码,这总是最好的选择! - kaiser
显示剩余3条评论
14个回答

325
通常不需要。对于虚拟方法,HotSpot会跟踪方法是否已经被实际覆盖,并能够执行优化,例如内联,这是在假定方法未被覆盖的情况下进行的,直到加载覆盖该方法的类为止,此时可以撤消(或部分撤消)这些优化。(当然,这是假设您正在使用HotSpot——但它是迄今为止最常见的JVM,因此...)
在我看来,您应该基于清晰的设计和可读性而不是出于性能原因使用final。如果您想出于性能原因更改任何内容,在扭曲最清晰的代码之前应进行适当的测量——这样您就可以决定所实现的额外性能是否值得较差的可读性/设计。(根据我的经验,几乎永远不值得;但每个人的情况都不同。)
编辑:由于已提到final字段,因此值得提出它们通常也是一种清晰设计的好方法。它们还会更改跨线程可见性的保证行为:在构造函数完成后,任何final字段都保证立即在其他线程中可见。这可能是我经验中最常见的使用final方式,尽管作为Josh Bloch“设计用于继承或禁止继承”的支持者,我应该更经常地将final用于类...

1
@Abhishek:关于什么特别的问题?最重要的一点是最后一个——你几乎肯定不应该担心这个。 - Jon Skeet
14
通常建议使用final,因为它使代码更易于理解,并有助于查找错误(因为它明确了程序员的意图)。PMD可能建议使用final是因为这些样式问题,而不是出于性能原因。 - sleske
3
很多东西可能是与 JVM 特定相关的,可能依赖于非常微妙的上下文方面。例如,我相信 HotSpot server JVM 在适当情况下将仍允许内联虚方法,即使在一个类中重写了它们,并进行了快速类型检查。但这些细节很难确定,并且可能会在发布之间发生变化。 - Jon Skeet
14
在这里,我引用《Effective Java》第二版的第15项“最小化可变性”:“不可变类比可变类更容易设计、实现和使用。它们更不容易出错,也更安全。”此外,“一个不可变对象只能处于创建时的状态”,而“可变对象则可以具有任意复杂的状态空间”。从我的个人经验来看,使用关键字final应该强调开发人员倾向于不可变性的意图,而不是为了“优化”代码。我鼓励您阅读这一章节,非常有趣! - Louis F.
2
其他答案显示,在变量上使用final关键字可以减少字节码的数量,这可能会对性能产生影响。 - Julien Kronegg
显示剩余7条评论

113

简短回答:不要担心!

长话短说:

谈及“最终本地变量”时,请记住使用关键字 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");

    }

}

结果是什么?

Method with finals took 5 ms
Method without finals took 273 ms

在Java Hotspot VM 1.7.0_45-b18上进行了测试。

那么实际的性能提升是多少呢?我不敢说。在大多数情况下可能只会有轻微改善(在这个合成测试中,由于完全避免了字符串连接操作,所以只有约270纳秒——这是一个罕见的情况),但在高度优化的工具代码中,它可能是一个因素。无论如何,对最初的问题的回答是是的,它可能会改善性能,但充其量只是轻微的改善

除了编译时的好处外,我找不到使用关键字final对性能有任何可衡量影响的证据。


5
我稍微修改了你的代码,进行了100次测试,涵盖了两种情况。最终的平均时间对于非final版本为0毫秒和9毫秒。将迭代次数增加到10M,平均值为0毫秒和75毫秒。然而,非final版本的最佳运行时间是0毫秒。也许是虚拟机检测到结果被丢弃的原因之一吗?我不知道,但不管怎样,使用final确实会产生显著差异。 - Casper Færgemand
8
有缺陷的测试。早期的测试会预热 JVM 并使后面的测试调用受益。重新排序你的测试并观察结果。你需要在单独的 JVM 实例中运行每个测试。 - Steve Kuo
23
不,这项测试并非有缺陷,热身已被考虑在内。第二个测试更慢,而非更快。如果没有热身,第二个测试甚至会更慢。 - rustyx
7
在testFinal()中,所有时间返回的都是来自字符串池的同一个对象,因为最终字符串和字符串字面量的连接结果在编译时被评估。testNonFinal()每次都返回一个新对象,这就解释了速度上的差异。 - anber
4
您认为这种情况不现实的原因是什么?使用字符串连接比添加整数更加耗费资源。如果可能的话,进行静态连接会更有效率,这就是测试结果显示的内容。 - rustyx
显示剩余6条评论

87

是的,它可以。以下是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. 
    }
}

将doSomething属性转换为final属性后,您告诉编译器无论何时看到doSomething,都应根据编译时替换规则将其替换为false。 编译器的第一次通过将代码更改为像something这样:

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. 

    }
}

完成后,编译器会再次检查代码并发现存在无法到达的语句。由于你正在使用高质量的编译器,它不喜欢所有那些无法到达的字节码。因此,它会将其删除,最终你得到:

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. 

  }
}

从而减少任何过多的代码或不必要的条件检查。

Edit: 例如,让我们看下面的代码:

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");
        }
    }
}

使用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
}

我们可以注意到编译后的代码仅包括非final变量x。这证明了final变量对性能有影响,至少对于这种简单情况是如此。


2
@ŁukaszLech 我从Oreilly的一本书《Hardcore Java》中学到了这个:在他们关于final关键字的章节中。 - mel3kings
23
这段谈论的是编译时的优化,即开发人员在编译时就知道最终布尔变量的值,那么为什么一开始要编写if块呢?对于这种情况,IF条件没有必要且毫无意义。在我看来,即使这样可以提高性能,在第一次编写代码时也是错误的,开发人员自己可以进行优化,而不是将责任交给编译器。该问题主要意在询问final通常被使用的情况下有哪些性能改进,这在编程上是有意义的。 - Bhavesh Agarwal
11
这样做的目的是像mel3kings所说,添加调试语句。你可以在生产版本之前(或在构建脚本中配置)翻转该变量,并在创建分发时自动删除所有的代码。 - Adam

38

3
根据IBM的说法,这种方法对于领域非常有效:https://www.ibm.com/developerworks/java/library/j-jtp1029/index.html#heading6 - 并且也被推广为最佳实践。 - Philzen
7
“04223”这篇文章来源于2003年,现在已经过去了近17年。那时候...是Java 1.4吗? - dmatej

27

我很惊讶没有人发布一些真正的反编译代码来证明至少存在一些微小的差异。

参考文献:此测试已针对javac版本8910进行。

假设有这个方法:

public static int test() {
    /* final */ Object left = new Object();
    Object right = new Object();

    return left.hashCode() + right.hashCode();
}

编译这段代码时,生成的字节码与使用final关键字的代码完全相同(final Object left = new Object();)。
但是这个:
public static int test() {
    /* final */ int left = 11;
    int right = 12;
    return left + right;
}

生成:

   0: bipush        11
   2: istore_0
   3: bipush        12
   5: istore_1
   6: iload_0
   7: iload_1
   8: iadd
   9: ireturn

留下 final 存在会产生:

   0: bipush        12
   2: istore_1
   3: bipush        11
   5: iload_1
   6: iadd
   7: ireturn

代码本身非常易懂,如果有编译时常量,它将直接加载到操作数栈上(不会像前面的示例一样存储到本地变量数组中通过 bipush 12; istore_0; iload_0),这在某种程度上是有道理的,因为没有人能够更改它。
另一方面,为什么在第二种情况下编译器不会产生 istore_0 ... iload_0,我不知道,毕竟那个位置 0 没有被以任何方式使用(这样可以缩小变量数组,但可能我错过了一些内部细节,无法确定)。
看到这样的优化,我感到惊讶,考虑到 javac 的优化很少。至于我们是否应该总是使用 final?我甚至不会写一个 JMH 测试(最初我想这样做),我确信差异在 ns 的数量级上(如果可能被捕获的话)。唯一可能出现问题的地方是当一个方法由于其大小而不能被内联(并且声明 final 将使其大小缩小几个字节)时。

还有两个需要解决的final问题。首先是当方法被认为是final时(从JIT的角度来看),这样的方法是单态的——这些方法是JVM最受喜爱的方法。

然后是final实例变量(必须在每个构造函数中设置);这些变量非常重要,因为它们将保证正确发布引用,稍微提到了一下并且也由JLS准确指定。


话虽如此:这里还有一件事情是对每一个回答都不可见的:垃圾回收。需要花费很多时间来解释,但是当你读取一个变量时,GC会为该读取设置所谓的屏障。每个aloadgetField都通过这样一个屏障进行“保护”,这里有更多细节。理论上,final字段不需要这样的“保护”(它们可以完全跳过屏障)。因此,如果GC这样做了-final将提高性能。


我使用Java 8 (JDK 1.8.0_162)编译了代码并启用了调试选项(javac -g FinalTest.java),使用javap -c FinalTest.class反编译了代码,但没有获得相同的结果(在final int left=12的情况下,我得到了bipush 11; istore_0; bipush 12; istore_1; bipush 11; iload_1; iadd; ireturn)。因此,生成的字节码取决于许多因素,很难说final是否对性能产生影响。但是由于字节码不同,可能存在一些性能差异。 - Julien Kronegg
2
这不是反编译的真实代码吗? - user202729
1
虽然字节码的样子并不是非常有趣,但实际汇编代码才是最重要的。我很怀疑在这种情况下汇编代码最终会看起来一样。 - Stefan Reich

13
您实际上询问了至少两种不同情况:
1. 本地变量的 final 2. 方法/类的 final
Jon Skeet 已经回答了第二个问题。关于第一个问题:
我认为这并没有什么区别;对于本地变量,编译器可以推断出变量是 final 还是非 final 的(只需检查它是否被赋值超过一次即可)。因此,如果编译器想要优化仅分配一次的变量,它可以这样做,无论变量是否实际声明为 final。
final 在某些情况下可能会对受保护/公共类字段产生影响。那里对于编译器来说很难找出该字段是否被设置了多次,因为它可能来自不同的类(甚至可能未加载)。但是,即使在这种情况下,JVM 也可以使用 Jon 描述的技术(进行乐观优化,在更改字段的类被加载时还原)。
总之,我没有看到它为什么会提高性能的任何理由。因此,这种微小的优化不太可能有帮助。您可以尝试进行基准测试以确保它不会有任何差异,但我怀疑它不会有任何影响。
编辑:根据 Timo Westkämper 的答案,实际上 final 在某些情况下可以改善类字段的性能。我纠正了我的错误。

我认为编译器无法正确检查局部变量被赋值的次数:那么对于有大量赋值的if-then-else结构呢? - gyorgyabraham
1
@gyabraham:如果您将本地变量声明为“final”,编译器已经检查了这些情况,以确保您不会分配两次。就我所看到的,相同的检查逻辑可以(并且可能已经)用于检查变量是否可以是“final”。 - sleske
局部变量的最终性在字节码中没有表达,因此JVM甚至不知道它是final的。 - Steve Kuo
1
@SteveKuo:即使它没有在字节码中表达,也可能有助于javac更好地进行优化。但这只是猜测。 - sleske
1
编译器可以找出一个本地变量是否被赋值了一次,但在实践中,它并不会(除了错误检查)。另一方面,如果一个 final 变量是原始类型或类型为 String 并且立即被分配为编译时常量,就像问题的示例一样,编译器 必须 将其内联,因为该变量是规范上的编译时常量。但对于大多数用例,代码可能看起来不同,但从性能上讲,无论常量是内联还是从本地变量读取,都没有区别。 - Holger

7

注意:不是java专家

如果我记得正确的话,使用final关键字很难提高性能。我一直知道它存在于“好代码”-设计和可读性。


6
在Java中,我们使用final关键字使事物变得不可变,并且至少有三种方法可以使不可变性在代码性能方面产生实际差异。这三个点都源于编译器或开发人员做出更好的假设:
  1. 更可靠的代码
  2. 更高效的代码
  3. 更有效的内存分配和垃圾回收

更可靠的代码

正如其他回复和评论所述,使类不可变会导致更清洁、更易维护的代码,并使对象不可变使它们更容易处理,因为它们可以处于完全一种状态,这意味着更容易进行并发操作并优化完成任务所需的时间。
此外,编译器会警告您有关未初始化变量的使用,并且不会让您重新分配新值。
如果我们谈论方法参数,则将它们声明为final,如果您意外地使用相同的名称用于变量,或重新分配它的值(使参数不再可访问),则编译器会发出警告。

更高效的代码

对生成的字节码进行简单分析应该可以解决性能问题:使用@rustyx在回复中发布的代码的最小修改版本,您可以看到当编译器知道对象不会改变其值时,生成的字节码是不同的。
这就是代码:
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);
    }
    
}

使用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

如您所见,有时候使用 final 关键字确实会有影响。
为了完整起见,下面是测试时间:

方法 testFinal 耗时 5 毫秒
方法 testSomeFinal 耗时 13 毫秒
方法 testNonFinal 耗时 20 毫秒

尽管这些时间看起来微不足道(因为我们执行了一百万个任务),但我认为经过一段时间后,JIT 优化会发挥它的作用,平滑掉差异。但即使如此,在考虑到对于 testNonFinal 这个测试场景,JVM 已被前面的测试热身并且共同的代码应该已经被优化的情况下,4 倍的性能提升也已经不容忽视。

更易于内联

较少的字节码也就意味着更易于短小的内联,进而更好地利用资源并获得更好的性能。

嵌入式设备

Java 开发者可以潜在地编写可运行于服务器、桌面端和小型或嵌入式设备上的代码,因此,将代码在编译时更加高效(而不是完全依赖于 JVM 的优化)可以在 所有 运行时节省内存、时间和能量,同时减少并发问题和错误。

更有效的内存分配和垃圾回收

如果对象有不可变或 final 字段,则它们的状态无法更改,它们在创建时所需的内存更容易估计(因此这会导致较少的重新定位)并且需要更少的防御性拷贝:在 getter 中可以直接共享一个不可变对象,而无需创建防御性拷贝。
最后,关于未来可能性还有另一个要点:当项目 Valhalla 推出并且“值类”可用时,将对象的字段应用不可变性将成为希望使用它们并利用许多 JIT 编译器优化的人的重要简化。

关于不可变性的个人看法

如果 Java 中的变量、对象属性和方法参数默认情况下是不可变的(像 Rust 中一样),那么开发者将被迫编写更清晰、更高效的代码,并明确声明所有可能改变其值的对象,这将使得开发者更加注意潜在错误。
我不知道对于 final class 是否也是如此,因为 mutable class 对我来说并没有太多意义。

3

如其他地方所提到的,对于局部变量和稍微次要的成员变量而言,'final' 更多的是一种风格问题。

'final' 是表明你想要这个变量不可改变(即该变量不会变动!)。编译器则可以通过报错来帮助你避免违反自己的约束。

我认为如果Java默认情况下将标识符(抱歉,我只能叫一个不可变的东西为“变量”)设为final,并要求你明确表示它们是变量,那么它将成为一种更好的语言。但话虽如此,对于已初始化并且从未分配过的局部变量,我通常不使用 'final';因为这样会显得太繁琐。

(对于成员变量,我会使用 'final' )


2

对于成员变量和参数来说,Final(至少是)更多地为人类服务,而不是机器。

在可能的情况下,将变量设置为final是一个好习惯。我希望Java默认把“变量”设置为final,并使用“Mutable”关键字来允许更改。不可变类会导致更好的线程安全代码,只需看一眼带有“final”前缀的类的每个成员变量,就可以快速确定其是否是不可变的。

另一种情况是:我一直在将很多代码转换为使用@NonNull/@Nullable注释(您可以说方法参数不能为空,然后IDE可以警告您未标记@NonNull的变量的所有位置-整个过程扩散到荒谬的程度)。当成员变量或参数被标记为final时,可以更容易地证明它们不可能为空,因为您知道它们没有被重新分配给其他地方。

我的建议是:默认情况下将成员和参数应用final,这只是几个字符,但即使没有其他任何东西,也会促使您改进编码风格。

对于方法或类来说,Final是另一个概念,因为它禁止了一种非常有效的重用形式,并且并不能真正告诉读者太多信息。最好的用法可能是它们将String和其他内部类型设置为final,以便您可以在任何地方依赖一致的行为-这防止了很多错误(尽管有时我真的很想扩展字符串....哦,那些可能性)。


在代码中添加额外的注释和final会使其变得更大...但这真的会让它变得更好吗? - Ashley Frieze
当然,而且好得多。编程不是为了让某些东西工作而打字,那基本上就是写脚本(这是一种完全有效的实践,但我不建议使用Java)。最终保存会有很多问题(并不会真正使您的程序变大——除非出于某种可怕的原因,您想要最小化源代码大小,这是一个可怕的概念——如果这是您的目标,为什么不在APL或其中一种代码高尔夫语言中编码?在遇到问题之前使用您的代码来防止问题是非常有用的。 - Bill K
当然,但是...我见过一些无处不在的final代码存在深层次的问题,而且我也见过很简单的没有使用final修饰符的代码。final相比其他做法具体如何有所帮助呢?例如,如果你只编写非常短的函数,很少使用临时变量,那么final并没有多大的帮助,看起来只像是喊话!;) - Ashley Frieze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接