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


346

在Java中,我们看到了很多final可以使用关键字但很少使用的地方。

例如:

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

在上述情况下,str可以,final但是通常不这样做。

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

在任何或所有这些情况下使用final关键字是否真的可以提高性能?如果是这样,那又如何?请解释。如果final对性能的正确使用确实很重要,那么Java程序员应该养成什么习惯来充分利用关键字?


我不这么认为,方法调度(调用站点缓存和...)在动态语言中而不是在静态类型语言中是一个问题
Jahan 2010年

如果我运行用于审阅目的的PMD工具(插件到eclipse),则建议对变量进行更改,以防出现上述情况。但是我不明白它的概念。真的表现如此出色吗?
Abhishek Jain

4
我认为这是一个典型的考试问题。我记得final 确实会影响性能,JRE可以以某种方式优化IIRC final类,因为它们不能被子类化。
卡乌

我实际上已经对此进行了测试。在所有JVM上,我测试了在局部变量上使用final 确实提高了性能(尽管有一点,但是在实用程序方法中可能是一个因素)。源代码在下面的答案中。
rustyx 2014年

在进行性能检查时,最好使用Caliper之类的工具来进行微基准测试。
阿基米德·特拉哈诺2015年

Answers:


285

通常不会。对于虚拟方法,HotSpot会跟踪该方法是否已被实际覆盖,并能够执行优化,例如在未覆盖方法的前提下进行内联-直到它加载了覆盖该方法的类为止。它可以撤消(或部分撤消)那些优化。

(当然,这是假设您使用的是HotSpot-但这是迄今为止最常见的JVM,所以...)

我认为您应该final基于清晰的设计和可读性而不是出于性能原因而使用。如果出于性能原因要进行任何更改,则应在弯曲最清晰的代码之前进行适当的测量-这样,您可以确定所实现的任何额外性能是否值得较差的可读性/设计。(根据我的经验,这几乎是不值得的; YMMV。)

编辑:正如最后提到的领域,值得一提的是,就清晰的设计而言,它们通常还是个好主意。它们还改变了跨线程可见性的保证行为:构造函数完成后,可以保证任何final字段都立即在其他线程中可见。这可能是final我经验中最常见的用法,尽管作为乔什·布洛赫(Josh Bloch)的“继承设计或禁止继承设计”经验法则的支持者,我可能应该final更频繁地在类上使用它。


1
@Abhishek:关于什么特别?最重要的一点是最后一点-您几乎可以肯定不必为此担心。
乔恩·斯基特

9
@Abishek:final通常建议使用它,因为它使代码更易于理解,并有助于发现错误(因为它使程序员的意图明确)。PMD可能final由于这些样式问题而不是出于性能原因而建议使用。
sleske

3
@Abhishek:其中很多可能是特定于JVM的,并且可能依赖于上下文的非常细微的方面。例如,我相信HotSpot 服务器 JVM在一个类中被重写时仍将允许内联虚拟方法,并在适当时进行快速类型检查。但是细节很难确定,发布之间可能会发生变化。
乔恩·斯基特

5
在这里,我引用有效的Java,第2版,第15条,最小化可变性:Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more secure.。此外An immutable object can be in exactly one state, the state in which it was created.VS Mutable objects, on the other hand, can have arbitrarily complex state spaces.。从我的个人经验来看,使用关键字final应该突出开发人员倾向于不变性而不是“优化”代码的意图。我鼓励您阅读本章,非常有趣!
Louis F.

2
其他答案表明,final对变量使用关键字可以减少字节码的数量,这可能会对性能产生影响。
朱利安·克朗格

85

简短的答案:不用担心!

长答案:

在讨论最终局部变量时,请记住,使用关键字final将有助于编译器静态优化代码,最终可能会导致代码更快。例如,以下示例中的最终String 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对性能有任何可衡量的影响。


2
我稍微重写了您的代码以对这两种情况进行100次测试。最终,决赛的平均时间为0毫秒,非决赛的平均时间为9毫秒。将迭代计数增加到10M,将平均值设置为0 ms和75 ms。但是,非决赛的最佳成绩是0毫秒。也许是因为VM检测到的结果只是被扔掉了?我不知道,但是无论如何,final的使用确实有很大的不同。
CasperFærgemand2014年

4
测试有缺陷。较早的测试将预热JVM,并有益于以后的测试调用。重新排序测试,看看会发生什么。您需要在自己的JVM实例中运行每个测试。
史蒂夫郭

16
没有测试没有缺陷,已考虑到预热。第二个测试是较慢,不是更快。如果没有预热,第二个测试将更慢。
rustyx 2014年

6
在testFinal()中,所有时间都从字符串池返回相同的对象,因为最终字符串的字符串和字符串文字连接的重载是在编译时求值的。testNonFinal()每次返回新对象时,即解释了速度差异。
anber 2014年

4
是什么让您认为该方案不现实?String串联操作比添加操作要昂贵得多Integers。静态执行(如果可能)会更有效,这就是测试所显示的。
rustyx 2014年

62

是的,它可以。这是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属性转换为最终属性,您已经告诉编译器,只要编译器看到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。这证明,至少对于这种简单情况,最终变量会影响性能。


1
@ŁukaszLech我从一本Oreilly的书中学习到了这一点:Hardcore Java,在有关final关键字的章节中。
mel3kings

15
这是关于在编译时进行优化的意思,这意味着开发人员在编译时知道最终布尔变量的VALUE,如果在这种情况下不需要IF-CONDITIONS而不进行任何意义?在我看来,即使这可以提高性能,但它首先是错误的代码,可以由开发人员自己进行优化,而不是将责任交给编译器,而这个问题主要是想问一下使用final的常规代码的性能改进。具有程序意义。
Bhavesh Agarwal

7
这样做的目的是按照mel3kings的说明添加调试语句。您可以在生产构建之前翻转变量(或在构建脚本中对其进行配置),并在创建发行版时自动删除所有该代码。
亚当


16

令我惊讶的是,没有人实际发布过一些经过反编译的真实代码,以证明至少存在一些细微的差异。

对于参考这已经对测试javac版本8910

假设此方法:

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

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

按原样编译此代码,将产生与出现时完全相同的字节代码finalfinal 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

该代码几乎是不言自明的,如果有一个编译时间常数,它将直接加载到操作数堆栈上(它不会像前面的示例一样通过via存入本地变量数组中bipush 12; istore_0; iload_0)-这很有意义因为没有人可以改变它。

另一方面,为什么在第二种情况下编译器没有产生istore_0 ... iload_0超出我的范围,这并不是像0以任何方式使用该插槽(它可能以这种方式缩小变量数组,但可能是我缺少了一些内部细节,无法肯定地说)

考虑到这种优化很少,我很惊讶javac。至于我们应该经常使用final吗?我什至不打算编写一个JMH测试(我最初想写一个测试),我确信差异在ns(如果可能的话)的顺序中。唯一可能出现问题的地方是,由于方法的大小而无法内联一个方法(并且声明final会将其缩小几个字节)。

还有两个final需要解决的问题。首先是方法final(从JIT角度来看)时,这种方法是单态的 -并且它们是上最受欢迎的方法JVM

然后是final实例变量(必须在每个构造函数中设置);这些非常重要,因为它们将保证正确发布的参考文献(在此处有一点涉及,也由精确指定)JLS


我使用带有调试选项(javac -g FinalTest.java)的Java 8(JDK 1.8.0_162)编译了代码,使用进行了反编译javap -c FinalTest.class,但没有获得相同的结果(final int left=12,我得到了bipush 11; istore_0; bipush 12; istore_1; bipush 11; iload_1; iadd; ireturn)。因此,是的,生成的字节码取决于许多因素,很难说是否final会对性能产生影响。但是由于字节码不同,因此可能存在一些性能差异。
朱利安·克朗格


13

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

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

乔恩·斯凯特(Jon Skeet)已回答2)。大约1):

我认为这没有什么不同;对于局部变量,编译器可以推断出该变量是否为最终变量(只需通过检查其是否分配了多次即可)。因此,如果编译器想要优化仅分配一次的变量,则无论是否实际声明变量,它都可以这样做final

final 可能对受保护/公共类字段有所不同;对于编译器来说,很难发现该字段是否被设置了一次以上,因为它可能来自不同的类(甚至可能没有被加载)。但是即使那样,JVM仍可以使用Jon所描述的技术(乐观地进行优化,如果加载了确实改变了字段的类,则进行还原)。

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

编辑:

实际上,根据TimoWestkämper的回答,在某些情况下final 可以提高类字段的性能。我站得住了。


我认为编译器无法正确检查分配局部变量的次数:如果有大量分配的if-then-else结构如何?
gyorgyabraham 2013年

1
@gyabraham:如果您将局部变量声明为final,则编译器已经检查了这些情况,以确保您不会将其分配两次。据我所知,可以(并且可能)使用相同的检查逻辑来检查变量是否可以是final
sleske

局部变量的最终形式未在字节码中表示,因此JVM甚至都不知道它是最终的
Steve Kuo 2014年

1
@SteveKuo:即使未在字节码中表示,也可能有助于javac更好地进行优化。但这只是猜测。
sleske 2014年

1
编译器可以发现局部变量是否只被分配了一次,但是实际上,它不是(除了错误检查之外)。另一方面,如果final变量是原始类型或类型,String并且像问题示例中一样立即被分配了编译时常量,则编译器必须内联它,因为该变量是每个规范的编译时常量。但是对于大多数使用情况,代码看起来可能有所不同,但是从性能角度来看,常量是内联还是从局部变量读取仍然没有区别。
Holger

6

注意:不是Java专家

如果我没有记错我的Java,那么使用final关键字将几乎没有提高性能的方法。我一直都知道它存在于“好的代码”中-设计和可读性。


1

我不是专家,但是我想您应该final在类或方法中添加关键字(如果不会被覆盖),并且不要理会变量。如果有任何方法可以优化此类事情,编译器将为您完成。


1

实际上,在测试一些与OpenGL相关的代码时,我发现在私有字段上使用final修饰符会降低性能。这是我测试的课程的开始:

public class ShaderInput {

    private /* final */ float[] input;
    private /* final */ int[] strides;


    public ShaderInput()
    {
        this.input = new float[10];
        this.strides = new int[] { 0, 4, 8 };
    }


    public ShaderInput x(int stride, float val)
    {
        input[strides[stride] + 0] = val;
        return this;
    }

    // more stuff ...

这是我用来测试各种替代方法的性能的方法,其中包括ShaderInput类:

public static void test4()
{
    int arraySize = 10;
    float[] fb = new float[arraySize];
    for (int i = 0; i < arraySize; i++) {
        fb[i] = random.nextFloat();
    }
    int times = 1000000000;
    for (int i = 0; i < 10; ++i) {
        floatVectorTest(times, fb);
        arrayCopyTest(times, fb);
        shaderInputTest(times, fb);
        directFloatArrayTest(times, fb);
        System.out.println();
        System.gc();
    }
}

在第3次迭代后,随着VM的预热,我始终获得这些数字而没有最后一个关键词:

Simple array copy took   : 02.64
System.arrayCopy took    : 03.20
ShaderInput took         : 00.77
Unsafe float array took  : 05.47

使用 final关键字:

Simple array copy took   : 02.66
System.arrayCopy took    : 03.20
ShaderInput took         : 02.59
Unsafe float array took  : 06.24

注意ShaderInput测试的数字。

我是否公开字段还是私有字段都没有关系。

顺便说一句,还有更多令人困惑的事情。即使使用final关键字,ShaderInput类也胜过所有其他变体。这很了不起,因为它基本上是一个包装float数组的类,而其他测试则直接操作该数组。必须弄清楚这一点。可能与ShaderInput的流畅接口有关。

而且,对于小型数组,System.arrayCopy实际上显然比在for循环中将元素从一个数组复制到另一个数组要慢一些。并且使用sun.misc.Unsafe(以及直接的java.nio.FloatBuffer,在此未显示)执行abysmally。


1
您忘记将参数设置为最终值。<pre> <code> public ShaderInput x(final int stride,final float val){input [strides [stride] + 0] = val; 返回这个 } </ code> </ pre>根据我的经验,进行任何变量或字段最终运算确实可以提高性能。
Anticro

1
哦,还要将其他代码也设为final:<pre> <code> final int arraySize = 10; final float [] fb =新的float [arraySize]; for(int i = 0; i <arraySize; i ++){fb [i] = random.nextFloat(); }最终的int时间= 1000000000;for(int i = 0; i <10; ++ i){floatVectorTest(times,fb); arrayCopyTest(times,fb); shaderInputTest(times,fb); directFloatArrayTest(times,fb); System.out.println(); System.gc(); } </ code> </ pre>
Anticro

1

最终(至少对于成员变量和参数而言)对人而言要多于对机器而言。

最好尽可能使变量最终化。我希望Java在默认情况下将“变量”定为final,并使用“ Mutable”关键字来允许更改。不可变的类会导致更好的线程代码,只要浏览每个成员前面都带有“ final”的类,就会很快显示出它是不可变的。

另一种情况-我一直在转换很多代码以使用@ NonNull / @ Nullable注释(您可以说方法参数不能为null,然后IDE会在您传递未标记@的变量时向您发出警告@ NonNull-整个事情蔓延到一个荒谬的程度。当成员变量或参数被标记为final时,证明成员变量或参数不能为null会容易得多,因为您知道它不会在其他任何地方重新分配。

我的建议是养成默认情况下为成员和参数应用final的习惯,它只是几个字符,但如果没有其他问题,将带您朝着改进编码风格的方向发展。

方法或类的final是另一个概念,因为它不允许非常有效的重用形式,并且并没有真正告诉读者很多信息。最好的用法可能是他们使String和其他内部类型最终化的方式,因此您可以在任何地方都依赖一致的行为-这样可以避免很多错误(尽管有时我喜欢扩展字符串....哦,可能性)


-3

用final声明的成员将在整个程序中可用,因为与非final成员不同,如果未在程序中使用这些成员,则Garbage Collector仍将不小心使用它们,因此由于内存管理不当而可能导致性能问题。


2
这是什么废话?您有任何消息来源为何您认为是这种情况吗?
J. Doe

-4

final 关键字可以在Java中以五种方式使用。

  1. 一堂课是最后的
  2. 参考变量是最终的
  3. 局部变量是最终的
  4. 方法是最终的

一个类是最终的:一个类是最终的,这意味着我们不能扩展,或者继承意味着不可能的继承。

同样-对象是最终对象:有时我们不修改对象的内部状态,因此在这种情况下我们可以指定对象为最终对象。对象最终意味着既不是变量也不是最终对象。

一旦将参考变量设置为最终变量,就无法将其重新分配给其他对象。但是可以更改对象的内容,只要它的字段不是最终的


2
“对象是最终的”是指“对象是不可变的”。
gyorgyabraham 2013年

6
您说的没错,但您没有回答这个问题。OP没有询问什么final意思,而是使用是否会final影响性能。
Honza Zidek '16
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.