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

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

例如:

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

在上面的例子中,str可以是final,但这通常被省略。

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

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

111358 次浏览

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

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

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

编辑:前面提到了final字段,值得一提的是,从清晰的设计角度来看,它们通常都是一个好主意。它们还改变了跨线程可见性方面的保证行为:在构造函数完成后,任何final字段都保证立即在其他线程中可见。在我的经验中,这可能是final最常见的用法,尽管作为Josh Bloch的“为继承而设计或禁止它”经验法则的支持者,我可能应该在类中更多地使用final

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

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

注意:不是java专家

如果我没有记错我的java,使用final关键字几乎没有办法提高性能。 我一直知道它的存在是为了“好代码”——设计和可读性。< / p >

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

  1. final用于本地变量
  2. final用于方法/类

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

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

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

总之,我看不出有任何理由它应该有助于性能。 所以这种微观优化不太可能有帮助。你可以试着用基准测试来确定,但我怀疑它会有什么不同

编辑:

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

我不是专家,但我认为你应该在类或方法中添加final关键字,如果它不会被覆盖并且不影响变量的话。如果有任何方法来优化这样的东西,编译器会为你做。

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

  1. 一门课是最终的
  2. 引用变量是最终变量
  3. 局部变量是final
  4. 方法是最终的

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

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

一旦引用变量成为final,它就不能被重新分配给其他对象。但是可以改变对象的内容,只要它的字段不是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.


}
}

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

< p >编辑: 以下面的代码为例:

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将帮助编译器优化代码静态,这可能最终导致更快的代码。例如,下面例子中的最后一个字符串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关键字对性能有任何可衡量的影响。

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

在第三次迭代之后,随着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测试的图。

将字段设置为公共或私有并不重要。

顺便说一句,还有一些更令人困惑的事情。ShaderInput类的性能优于所有其他变量,即使是final关键字。这是值得注意的b/c,它基本上是一个包装浮点数组的类,而其他测试直接操作数组。必须把这个弄清楚。可能与ShaderInput的流畅界面有关。

同时系统。arrayCopy对于小数组显然比在for循环中简单地将元素从一个数组复制到另一个数组要慢一些。使用sun.misc.Unsafe(以及直接使用java.nio。FloatBuffer(这里没有显示)执行得非常糟糕。

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

作为参考,已经针对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测试(这是我最初想写的),我确信diff是在ns的顺序(如果可能的话)。这唯一可能成为问题的地方是,当一个方法因为它的大小而不能内联时(声明final将使该大小缩小几个字节)。

还有两个__abc需要解决。首先是当一个方法是final时(从JIT的角度来看),这样的方法是单型的 -这些是JVM最心爱的人方法。

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


话虽如此,这里还有一个对每个答案都不可见的东西:garbage collection。这将花费大量时间来解释,但当你一个变量时,GC有一个所谓的barrier用于该读取。每一个aloadgetField都是“受保护的”;通过这样一个屏障,这里有更多细节。理论上,final字段不需要这样的“保护”;(他们可以完全跳过障碍)。因此,如果GC这样做- final将提高性能。

Final(至少对于成员变量和参数)更适合人类,而不是机器。

在任何可能的情况下使变量为final是一个很好的实践。我希望Java默认设置“变量”为final,并有一个“可变”关键字来允许更改。不可变类会带来更好的线程代码,只要看一眼每个成员前面都有“final”的类,就会很快显示它是不可变的。

另一种情况——我已经转换了很多代码来使用@NonNull/@Nullable注释(你可以说一个方法参数必须不是null,然后IDE可以警告你每一个地方你传递一个没有@NonNull标记的变量——整个事情蔓延到一个荒谬的程度)。当一个成员变量或形参被标记为final时,证明它不能为空要容易得多,因为你知道它没有在其他任何地方被重新赋值。

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

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

正如在其他地方提到的,“final”用于局部变量,在较小程度上用于成员变量,更多的是一种风格问题。

'final'是一个声明,你希望变量不变(即,变量不会变化!)然后,如果你违反了自己的约束,编译器可以通过报错来帮助你。

我也认为,如果标识符(对不起,我只是不能把一个不变的东西称为“变量”)默认为final,并且要求你显式地说它们是变量,那么Java将是一种更好的语言。但话虽如此,我通常不会在初始化且从未赋值的局部变量上使用'final';好像太吵了。

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

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

在Java中,我们通过final关键字使事物不可变,至少有3种方式可以使不可变对代码性能产生真正的影响。这三点有一个共同的起源,使编译器或开发人员做更好的假设:

  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关键字会产生影响。

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

Method testFinal耗时5 ms
方法testSomeFinal耗时13 ms
方法testNonFinal耗时20 ms

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

容易内联

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

嵌入式设备

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

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

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

最后,关于未来的可能性还有另一个问题:瓦尔哈拉计划什么时候能重见天日?对于那些想要使用对象的人来说,将不可变性应用于对象的字段将是一个显著的简化,并利用可能出现的大量jit编译器优化。

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

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

我不知道for final classes是否也一样,因为mutable class对我来说听起来没有那么有意义