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

时间:2010-11-25 17:08:51

标签: java final

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

例如:

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

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

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

在任何或所有这些案例中使用final关键字是否真的能提高性能?如果是这样,那怎么样?请解释。如果正确使用final对性能有用,那么Java程序员应该养成哪些习惯来充分利用关键字?

14 个答案:

答案 0 :(得分:255)

通常不是。对于虚方法,HotSpot会跟踪该方法是否已实际被覆盖,并且能够执行优化,例如内联假设方法尚未被覆盖 - 直到它加载一个覆盖该方法的类,此时它可以撤消(或部分撤销)这些优化。

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

在我看来,您应该基于清晰的设计和可读性而不是出于性能原因而使用final。如果您出于性能原因想要更改任何内容,则应在将最清晰的代码变形之前执行适当的测量 - 这样您就可以决定是否所获得的额外性能值得更差的可读性/设计。 (根据我的经验,它几乎不值得; YMMV。)

编辑:正如最后的字段已被提及,值得提出的是,无论如何,在清晰的设计方面,它们通常都是个好主意。它们还在跨线程可见性方面改变了保证行为:在构造函数完成之后,任何最终字段都保证在其他线程中立即可见。这可能是final在我的经验中最常见的用法,虽然作为Josh Bloch的“继承设计或禁止它的设计”的经验支持者,我应该更频繁地使用final来进行课程学习。 ..

答案 1 :(得分:71)

简短回答:别担心!

答案很长:

在谈论最终局部变量时请记住,使用关键字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会对性能产生任何可测量的影响。

答案 2 :(得分:56)

是的,可以。这是一个最终可以提升绩效的实例:

条件编译是一种技术,其中代码行不会根据特定条件编译到类文件中。这可用于删除生产版本中的大量调试代码。

考虑以下事项:

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

答案 3 :(得分:36)

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

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

答案 4 :(得分:12)

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

  1. final用于本地变量
  2. final for methods / classes
  3. Jon Skeet已经回答了2)。关于1):

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

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

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

    编辑:

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

答案 5 :(得分:10)

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

作为参考,我们针对javac版本8910对此进行了测试。

假设这个方法:

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

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

按原样编译此代码,生成完全相同的字节代码,与final存在时{(1}})相同。

但是这个:

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

代码几乎是不言自明的,如果有编译时常量,它将被直接加载到操作数堆栈上(它不会像前面的例子一样通过{{1}存储到局部变量数组中}) - 这是有道理的,因为没有人可以改变它。

另一方面,为什么在第二种情况下编译器不生成 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个实例变量(必须在每个构造函数中设置);这些非常重要,因为它们可以保证正确发布的参考文献the most beloved,并且也完全由final指定。

答案 6 :(得分:5)

注意:不是java专家

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

答案 7 :(得分:2)

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

优良作法是尽可能使变量为final。我希望Java默认情况下将“变量”定为final,并使用“ Mutable”关键字来允许更改。不可变的类可以产生更好的线程代码,只要浏览每个成员前面都带有“ final”的类,就可以很快证明它是不可变的。

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

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

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

答案 8 :(得分:2)

正如其他地方所提到的,局部变量的“ final”(而成员变量的程度要小一些)则更多是样式问题。

'final'表示您希望变量保持不变(即变量不会改变!)。然后,编译器可以通过抱怨您是否违反自己的约束来帮助您。

我的观点是,如果默认情况下标识符是标识符(对不起,我不能将不变的东西称为“变量”),Java将是一种更好的语言,并要求您明确声明它们是变量。但是,话虽如此,我通常不对已初始化且从未分配过的局部变量使用“ final”。似乎太吵了。

(我在成员变量上使用final)

答案 9 :(得分:1)

我不是专家,但我认为如果不会覆盖{@ 1}}关键字,则应将final关键字添加到该类或方法中,并保留变量。如果有任何方法可以优化这些东西,编译器会为你做这件事。

答案 10 :(得分: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

使用最终关键字:

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

请注意ShaderInput测试的数据。

无论我是将这些领域公之于众还是非公开都没有关系。

顺便提一下,还有一些令人费解的事情。 ShaderInput类优于所有其他变体,即使使用final关键字也是如此。这是非常值得的b / c它基本上是一个包装浮点数组的类,而其他测试直接操纵数组。必须弄清楚这个。可能与ShaderInput的流畅界面有关。

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

答案 11 :(得分:0)

以final声明的成员将在整个计划中可用,因为 与非最终成员不同,如果这些成员尚未在程序中使用,则垃圾收集器仍不会照顾他们。 由于内存管理不善可能会导致性能问题。

答案 12 :(得分:-1)

肯定是的,如果变量转换为常量,

因为我们知道java编译器将这样的最终变量转换为可能的常量

作为常量java编译器的概念,在编译时直接用它的引用替换值

如果是没有任何运行时进程的字符串或基本类型

,则java变量中的

变为常量

否则它只是最终(不可更改)变量,

&安培; constatnt的使用总是比参考更快。

因此,如果可能,请使用任何编程语言中的常量以获得更好的性能

答案 13 :(得分:-3)

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

  1. 课程是最终的
  2. 参考变量是最终的
  3. 本地变量是最终的
  4. 方法是最终的
  5. 一个类是final:一个类是final意味着我们不能被扩展或继承意味着继承是不可能的。

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

    一旦参考变量成为最终变量,就无法将其重新分配给其他对象。但只要字段不是最终的

    ,就可以更改对象的内容