在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类是否也一样,因为可变类听起来对我来说没有什么意义