ChatGPT解决这个技术问题 Extra ChatGPT

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

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

例如:

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

在上述情况下,str 可以是 final,但这通常被省略。

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

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

我不这么认为,方法调度(调用站点缓存和...)是动态语言中的问题,而不是静态类型语言中的问题
如果我运行用于审查目的的 PMD 工具(eclipse 插件),它建议对变量进行更改,以防万一,如上所示。但我不明白它的概念。表演真的这么火吗??
我以为这是一道典型的考试题。我记得 final 确实对性能有影响,IIRC final 类可以通过 JRE 以某种方式优化,因为它们不能被子类化。
我实际上已经对此进行了测试。在我测试的所有 JVM 上,在局部变量上使用 final 确实提高了性能(略微,但仍然可以成为实用方法的一个因素)。源代码在我下面的答案中。
“过早的优化是万恶之源”。让编译器完成它的工作。编写可读且注释良好的代码。这永远是最好的选择!

J
Jon Skeet

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

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

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

编辑:正如最后提到的那样,值得提出的是,无论如何,就清晰的设计而言,它们通常是一个好主意。它们还改变了跨线程可见性方面的保证行为:在构造函数完成后,保证任何最终字段立即在其他线程中可见。根据我的经验,这可能是 final 最常见的用法,尽管作为 Josh Bloch 的“设计继承或禁止它”经验法则的支持者,我可能应该更频繁地将 final 用于类......


@Abhishek:特别是关于什么?最重要的一点是最后一点——你几乎肯定不应该担心这一点。
@Abishek:通常建议使用 final,因为它使代码更易于理解,并有助于发现错误(因为它使程序员的意图明确)。由于这些样式问题,PMD 可能建议使用 final,而不是出于性能原因。
@Abhishek:其中很多可能是特定于 JVM 的,并且可能依赖于上下文的非常微妙的方面。例如,我相信 HotSpot 服务器 JVM 在一个类中重写时仍允许内联虚拟方法,并在适当的情况下进行快速类型检查。但是细节很难确定,并且在发布之间很可能会发生变化。
在这里,我将引用 Effective 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.Mutable objects, on the other hand, can have arbitrarily complex state spaces.。根据我的个人经验,使用关键字 final 应该突出开发人员倾向于不变性而不是“优化”代码的意图。我鼓励你阅读这一章,引人入胜!
其他答案表明,对变量使用 final 关键字可以减少字节码的数量,这可能会对性能产生影响。
r
rustyx

简短的回答:别担心!

长答案:

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


我稍微重写了你的代码来测试这两种情况 100 次。最终决赛的平均时间为 0 毫秒,非决赛的平均时间为 9 毫秒。将迭代计数增加到 10M 将平均值设置为 0 ms 和 75 ms。然而,非决赛的最佳运行时间是 0 毫秒。也许是VM检测结果被丢弃或其他什么?我不知道,但无论如何,final 的使用确实会产生重大影响。
有缺陷的测试。较早的测试将预热 JVM 并使以后的测试调用受益。重新排序你的测试,看看会发生什么。您需要在其自己的 JVM 实例中运行每个测试。
不,测试没有缺陷,热身被考虑在内。第二个测试是更慢,而不是更快。如果没有预热,第二次测试会更慢。
在 testFinal() 中,所有时间都从字符串池返回相同的对象,因为最终字符串的恢复和字符串文字连接在编译时进行评估。 testNonFinal() 每次返回新对象,这就是速度差异的解释。
是什么让你觉得这个场景不切实际? String 连接比添加 Integers 的操作成本更高。静态执行(如果可能)更有效,这就是测试显示的内容。
m
mel3kings

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


@ŁukaszLech 我从 Oreilly 的一本书中了解到这一点:Hardcore Java,在他们关于 final 关键字的章节中。
这谈到了编译时的优化,这意味着开发人员在编译时知道最终布尔变量的值,如果块放在首位,那么对于这种情况,如果不需要 IF-CONDITIONS 并且不制作,那么编写的全部意义是什么任何意义?在我看来,即使这提高了性能,这首先是错误的代码,可以由开发人员自己进行优化,而不是将责任交给编译器,这个问题主要是想问一下使用 final 的通常代码的性能改进具有编程意义。
这样做的目的是添加 mel3kings 所述的调试语句。您可以在生产构建之前翻转变量(或在构建脚本中配置它)并在创建分发时自动删除所有该代码。
w
wmitchell

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

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


...根据 IBM 的说法,它适用于字段ibm.com/developerworks/java/library/j-jtp1029/… - 并且还被宣传为最佳实践。
文章“04223”是2003年的,现在快十七岁了。那是……Java 1.4?
E
Eugene

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

作为参考,这已针对 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();) 时相同的 exact 字节码。

但是这个:

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 存储到局部变量数组中) - 排序of 是有道理的,因为没有人可以改变它。

另一方面,为什么在第二种情况下编译器不产生 istore_0 ... iload_0 超出了我的理解,它不像插槽 0 以任何方式使用(它可以以这种方式缩小变量数组,但可能我错过了一些内部细节,不能确定)

我很惊讶看到这样的优化,考虑到小 javac 所做的事情。至于我们应该总是使用final吗?我什至不打算编写 JMH 测试(我最初想这样做),我确信差异是 ns 的顺序(如果可能被捕获的话)。这可能是一个问题的唯一地方,是当一个方法因为它的大小而不能被内联时(并且声明 final 会将该大小缩小几个字节)。

还有两个final需要解决。首先是当一个方法是 final(从 JIT 的角度来看)时,这种方法是 单态 - 这些是 JVMthe most beloved

然后有 final 个实例变量(必须在每个构造函数中设置);这些很重要,因为它们将保证正确发布的参考,as touched a bit here 并且也由 JLS 准确指定。

话虽这么说:这里的每个答案还有一件事是看不见的:garbage collection。这将花费大量时间来解释,但是当您读取一个变量时,GC 有一个所谓的 barrier 用于该读取。每个 aloadgetField 都通过这样的屏障 a lot more details here “保护”。理论上,final 字段不需要这样的“保护”(它们可以完全跳过障碍)。因此,如果 GC 这样做 - final 将提高性能。


我使用带有调试选项 (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 会对性能产生影响。但由于字节码不同,可能存在一些性能差异。
尽管字节码的样子并不十分有趣。实际的汇编代码才是最重要的。我很怀疑在这种情况下,程序集最终看起来是一样的。
s
sleske

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

final 用于局部变量 final 用于方法/类

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

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

final可能对受保护/公共类字段产生影响;编译器很难确定该字段是否被多次设置,因为它可能来自不同的类(甚至可能尚未加载)。但即便如此,JVM 也可以使用 Jon 描述的技术(乐观地优化,如果加载了确实改变了字段的类,则恢复)。

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

编辑:

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


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

注意:不是java专家

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


B
Bill K

Final(至少对于成员变量和参数)对于人类而言比对机器而言更多。

尽可能使变量成为最终变量是一种很好的做法。我希望 Java 在默认情况下将“变量”设为 final,并有一个“可变”关键字来允许更改。不可变的类会带来更好的线程代码,只要看一眼每个成员前面都有“final”的类就会很快表明它是不可变的。

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

我的建议是养成在默认情况下为成员和参数应用 final 的习惯,它只是几个字符,但如果没有别的,它会推动你改进你的编码风格。

方法或类的 final 是另一个概念,因为它不允许一种非常有效的重用形式,并且并没有真正告诉读者太多。最好的用途可能是他们将 String 和其他内在类型设为 final 的方式,这样你就可以在任何地方依赖一致的行为——这可以防止很多错误(尽管有时我会喜欢扩展字符串......哦可能性)


在代码中添加额外的注释和 final 会使代码变得更大……但它真的会变得更好吗?
绝对的,而且好多了。编码不是输入东西来使某些东西起作用,这基本上是脚本编写(这是一种完全有效的做法,但我不建议使用 java)。最终保存,所以可能会出现问题(并且不会真正使您的程序更大 - 除非出于某种可怕的原因,您的意思是最小化源代码大小,这是一个可怕的概念 - 为什么不只使用 APL 或其中一种代码高尔夫语言编写代码如果那是您的目标吗?在遇到问题之前使用您的代码来预防问题非常有用。
当然......但是......我已经看到了在所有地方都有根深蒂固的问题的代码,并且我已经看到了没有它的漂亮简单的代码。 final 如何以其他实践没有的方式特别提供帮助?例如,如果您只编写非常短的函数,并且很少使用临时变量,那么 final 不会像 SHOUTING 那样添加那么多! ;)
u
user13752845

正如在别处提到的,局部变量的“final”,以及在较小程度上的成员变量,更多的是风格问题。

'final' 是您希望变量不改变的声明(即,变量不会改变!)。如果您违反了自己的约束,编译器可以通过抱怨来帮助您。

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

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


C
Crozin

我不是专家,但我想你应该在类或方法中添加 final 关键字,如果它不会被覆盖并且不理会变量。如果有任何方法可以优化这些事情,编译器会为你做这件事。


u
user3663845

实际上,在测试一些与 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();
    }
}

在第三次迭代之后,随着虚拟机的预热,我一直得到这些数字,但没有最后的关键词:

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 测试的数字。

我将这些字段设为公开还是私有都没有关系。

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

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


您忘记将参数设置为最终参数。
 public ShaderInput x(final int stride, final float val) { input[strides[stride] + 0] = val;返回这个; } 
以我的经验,做任何变量或字段 final 确实可以提高性能。
哦,也让其他人也成为最终的:
 final int arraySize = 10;最终浮点[] fb = 新浮点[arraySize]; for (int i = 0; i < arraySize; i++) { fb[i] = random.nextFloat(); } 最终整数倍 = 1000000000; for (int i = 0; i < 10; ++i) { floatVectorTest(times, fb); arrayCopyTest(次, fb); shaderInputTest(次,FB); directFloatArrayTest(次, fb); System.out.println(); System.gc(); } 
S
Samanja Cartagena

我不能说这是不常见的,因为据我所知,这是在 java 中声明常量的唯一方法。作为一名 javascript 开发人员,我知道关键字常量的重要性。如果您在生产级别工作,并且您的值永远不会被其他编码人员意外更改,例如 SSN 编号甚至名称之类的值。然后你必须使用 final 关键字来声明变量。如果某些类可以被继承,有时会很麻烦。因为如果很多人在一个团队中工作,那么有人可以继承一个类,扩展它,甚至更改父类的变量和方法。这可以用关键字 final 停止,因为除非使用 final 关键字,否则即使是静态变量也可以更改。就您的问题而言,我认为 final 关键字不会影响代码的性能,但它绝对可以通过确保其他团队成员不会意外修改任何需要保持不变的内容来防止人为错误。


H
Harish Gowda

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

类是最终的 引用变量是最终的 局部变量是最终的 方法是最终的

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

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

一旦引用变量成为最终变量,就不能将其重新分配给其他对象。但是可以改变对象的内容,只要它的字段不是最终的


“对象是最终的”是指“对象是不可变的”。
你是对的,但你没有回答这个问题。 OP 没有问 final 是什么意思,而是使用 final 是否会影响性能。