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

例如:

String str = "abc";
System.out.println(str);

在上面的例子中,str可以是final,但通常不使用。

当一个方法永远不会被覆盖时,我们可以使用final关键字。类似地,类不会被继承。

在任何或所有这些情况下使用final关键字真的能提高性能吗?如果是,那该怎么做?请解释一下。如果final的正确使用确实关系到性能,那么Java程序员应该养成什么样的习惯才能最好地利用这个关键字呢?


当前回答

简单的回答:不用担心!

长一点的回答:

在讨论final局部变量时,请记住使用关键字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对性能有任何可衡量的影响。

其他回答

是的,它可以。下面是一个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。编译器的第一次传递将代码更改为如下内容:

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. 

  }
}

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

编辑: 以下面的代码为例:

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
}

我们可以注意到,编译后的代码只包含非最终变量x。 这证明了最终变量对性能有影响,至少在这个简单的情况下是这样。

简单的回答:不用担心!

长一点的回答:

在讨论final局部变量时,请记住使用关键字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对性能有任何可衡量的影响。

在Java中,我们使用final关键字使东西不可变,至少有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

正如您所看到的,在某些情况下,最后的关键字会产生影响。

为了完整起见,这些是测量的时间:

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

这些时间似乎无关紧要(假设我们完成了100万个任务),但我认为,经过一段时间后,JIT优化正在发挥它的魔力,并消除了差异,但即使这样说,4x也不是可以忽略不计的,因为当涉及到testNonFinal循环时,JVM已经被前面的测试热身了,公共代码还应该优化。

容易内联

更少的字节码也可以转化为更简单、更短的内联,从而更好地利用资源和提高性能。

嵌入式设备

Java开发人员可以编写在服务器、桌面和小型或嵌入式设备上运行的代码,因此在编译时提高代码的效率(并且不完全依赖JVM优化)可以节省所有运行时的内存、时间和精力,并减少并发问题和错误。

更高效的内存分配和垃圾收集

如果对象具有final或immutable字段,那么它们的状态不能改变,并且它们所需要的内存在创建时更容易估计(因此这会导致更少的重定位),并且需要更少的防御性副本:在getter中,我可以直接共享一个不可变对象,而无需创建防御性副本。

最后,还有另一个关于未来可能性的观点:当Valhalla项目看到太阳,“值类”将可用时,将不可变性应用于对象的字段,对于那些想要使用它们的人来说将是一个显著的简化,并利用可能出现的大量jit编译器优化。

一个关于不变性的个人说明

如果变量、对象的属性和方法的参数在Java中默认是不可变的(就像在Rust中一样),开发人员将被迫编写更清晰、性能更好的代码,并且显式地声明所有可以改变其值的对象是可变的,这将使开发人员更加意识到可能出现的错误。

我不知道final类是否也一样,因为可变类听起来对我来说没有什么意义

你实际上在问两种(至少)不同的情况:

局部变量的Final 方法/类的Final

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

我不认为这有什么区别;对于局部变量,编译器可以推断该变量是否是final变量(只需检查它是否被多次赋值)。因此,如果编译器想要优化只赋值一次的变量,无论变量是否声明为final,它都可以这样做。

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

总之,我看不出有任何理由它应该有助于性能。 所以这种微观优化不太可能有帮助。您可以尝试对其进行基准测试以确定,但我怀疑它是否会产生影响。

编辑:

实际上,根据Timo Westkämper的回答,final在某些情况下可以提高类字段的性能。我接受纠正。

令我惊讶的是,居然没有人发布一些经过反编译的真实代码,以证明至少有一些小的差异。

作为参考,已经在javac版本8,9和10上进行了测试。

假设这个方法:

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留给present会产生:

   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时(从JIT的角度来看),这样的方法是单形的——JVM最喜欢这种方法。

然后是最后一个实例变量(必须在每个构造函数中设置);这些是重要的,因为它们将保证一个正确发布的引用,正如这里提到的一点,也由JLS精确指定。


话虽如此,这里还有一件事是每个答案都看不见的:垃圾收集。这将花费大量时间来解释,但是当您读取一个变量时,GC对该读取有一个所谓的障碍。每个loadad和getField都通过这样的屏障“保护”,这里有更多细节。理论上,最终场不需要这样的“保护”(它们可以完全跳过屏障)。因此,如果GC这样做- final将提高性能。