1. 概述
在 Java 开发圈里,final
关键字是否能带来性能提升一直是一个热门话题。根据使用位置的不同,final
的作用和对性能的影响也会有所不同。
在这篇文章中,我们将深入探讨在代码中使用 final
是否真的能带来性能上的优势。我们会从变量、方法、类三个层面分析其性能影响。
除了性能方面的讨论,我们也会提及 final
在设计层面的意义,并给出推荐的使用场景。
2. 局部变量
当 final
应用于局部变量时,其值只能被赋值一次。
我们可以在声明时赋值,也可以在构造器中赋值。如果后续尝试修改这个变量,编译器会直接报错。
2.1. 性能测试
我们来测试一下,给局部变量加上 final
是否能提升性能。
我们使用 JMH 工具 来测量基准方法的平均执行时间。首先,我们测试不使用 final
的局部变量进行字符串拼接:
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public static String concatNonFinalStrings() {
String x = "x";
String y = "y";
return x + y;
}
接着,我们做同样的操作,但是变量加上了 final
:
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public static String concatFinalStrings() {
final String x = "x";
final String y = "y";
return x + y;
}
JMH 会自动运行预热迭代,让 JIT 编译器 的优化生效。下面是测试结果(单位:纳秒):
Benchmark Mode Cnt Score Error Units
BenchmarkRunner.concatFinalStrings avgt 200 2,976 ± 0,035 ns/op
BenchmarkRunner.concatNonFinalStrings avgt 200 7,375 ± 0,119 ns/op
在这个例子中,使用 final
的局部变量使执行速度提升了 2.5 倍 ✅。
2.2. 静态优化
字符串拼接的例子很好地说明了:**final
可以帮助编译器进行静态优化**。
对于非 final
的局部变量,编译器生成的字节码如下:
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
而加上 final
后,编译器发现这两个字符串是常量,直接优化成了:
LDC "xy"
ARETURN
⚠️ 但需要注意的是,大多数情况下,给局部变量加 final
并不会像这个例子一样带来显著的性能收益。
3. 实例变量和类变量
我们可以将 final
应用于实例变量或类变量(静态变量)。这样可以确保它们的值只能被赋值一次。
实例变量可以在声明时、初始化块中或构造器中赋值。
类变量(静态变量)可以通过添加 static
关键字声明。如果加上 final
,就定义了一个常量。常量可以在声明时或静态初始化块中赋值:
static final boolean doX = false;
static final boolean doY = true;
我们写一个简单的方法,使用这些布尔常量:
Console console = System.console();
if (doX) {
console.writer().println("x");
} else if (doY) {
console.writer().println("y");
}
接下来我们去掉 final
,比较生成的字节码:
- 非 final 类变量:76 行字节码
- final 类变量(常量):39 行字节码
✅ 加上 final
后,编译器会直接将常量替换为其实际值,从而减少字节码体积,实现静态优化。
不过,这种优化在真实项目中很少能带来明显性能提升。
4. 实际上是 final(Effectively Final)
Java 8 引入了 effectively final 这个概念。如果一个变量虽然没有显式声明为 final
,但在初始化后从未被修改,那么它就是 effectively final。
这个概念主要是为了让 lambda 表达式可以访问外部的局部变量。但 ❌ 编译器并不会像对待真正的 final
变量那样,对其进行静态优化。
5. 类和方法
当 final
应用于类或方法时,其目的完全不同:
final
类:不能被继承final
方法:不能被重写
目前没有证据表明在类或方法上使用 final
会带来性能提升。相反,❌ 滥用 final
会限制代码的可扩展性,影响面向对象设计的灵活性。
当然,有些场景下使用 final
是合理的,比如为了实现不可变对象。但 ❌ 性能优化不是在类或方法上使用 final
的好理由。
6. 性能 vs. 清晰设计
除了性能,使用 final
还有其他好处——它可以提高代码的可读性和表达力。下面是几个例子:
final
类:表明该类不允许被继承,有助于实现不可变对象final
方法:防止子类重写导致行为不一致final
方法参数:防止副作用final
变量:表明它是只读的
✅ 因此,我们应该 把 final
当作一种设计意图的表达方式,让其他开发者更容易理解代码的设计逻辑。同时,它也可以作为编译器优化的一个提示。
7. 总结
本文我们分析了 final
关键字的性能影响:
- ✅ 对变量使用
final
可能会带来轻微的性能优化 - ❌ 对类和方法使用
final
并不会提升性能 - ❌ effectively final 变量不会触发编译器的静态优化
- ✅ 使用
final
更多是为了表达设计意图,提升代码可读性和健壮性
源码地址:GitHub 项目地址