在Java中,我们使用
final
关键字使事物变得不可变,并且至少有三种方法可以使不可变性在代码性能方面产生实际差异。这三个点都源于编译器或开发人员做出更好的假设:
- 更可靠的代码
- 更高效的代码
- 更有效的内存分配和垃圾回收
更可靠的代码
正如其他回复和评论所述,使类不可变会导致更清洁、更易维护的代码,并使对象不可变使它们更容易处理,因为它们可以处于完全一种状态,这意味着更容易进行并发操作并优化完成任务所需的时间。
此外,编译器会警告您有关未初始化变量的使用,并且不会让您重新分配新值。
如果我们谈论方法参数,则将它们声明为
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
2: areturn
private static java.lang.String testNonFinal();
Code:
0: ldc #9
2: astore_0
3: ldc #11
5: astore_1
6: aload_0
7: aload_1
8: invokedynamic #13, 0
13: areturn
private static java.lang.String testSomeFinal();
Code:
0: ldc #11
2: astore_0
3: aload_0
4: invokedynamic #17, 0
9: areturn
如您所见,有时候使用
final
关键字确实会有影响。
为了完整起见,下面是测试时间:
方法 testFinal 耗时 5 毫秒
方法 testSomeFinal 耗时 13 毫秒
方法 testNonFinal 耗时 20 毫秒
尽管这些时间看起来微不足道(因为我们执行了一百万个任务),但我认为经过一段时间后,JIT 优化会发挥它的作用,平滑掉差异。但即使如此,在考虑到对于
testNonFinal
这个测试场景,JVM 已被前面的测试热身并且共同的代码应该已经被优化的情况下,4 倍的性能提升也已经不容忽视。
更易于内联
较少的字节码也就意味着更易于短小的内联,进而更好地利用资源并获得更好的性能。
嵌入式设备
Java 开发者可以潜在地编写可运行于服务器、桌面端和小型或嵌入式设备上的代码,因此,将代码在编译时更加高效(而不是完全依赖于 JVM 的优化)可以在
所有 运行时节省内存、时间和能量,同时减少并发问题和错误。
更有效的内存分配和垃圾回收
如果对象有不可变或 final 字段,则它们的状态无法更改,它们在创建时所需的内存更容易估计(因此这会导致较少的重新定位)并且需要更少的防御性拷贝:在 getter 中可以直接共享一个不可变对象,而无需创建防御性拷贝。
最后,关于未来可能性还有另一个要点:当项目 Valhalla 推出并且“值类”可用时,将对象的字段应用不可变性将成为希望使用它们并利用许多 JIT 编译器优化的人的重要简化。
关于不可变性的个人看法
如果 Java 中的变量、对象属性和方法参数默认情况下是不可变的(像 Rust 中一样),那么开发者将被迫编写更清晰、更高效的代码,并明确声明所有可能改变其值的对象,这将使得开发者更加注意潜在错误。
我不知道对于
final class
是否也是如此,因为
mutable class
对我来说并没有太多意义。