final 关键字
本教程共 100 篇 · 第 42 篇 · 更新于 2026-08-05 · 约 16 分钟阅读
42. final 关键字
本节目标:搞清楚
final修饰类、方法、变量分别是什么效果,会用它定义常量和不可变对象,并避开「引用不可变 ≠ 内容不可变」这个大坑。
final 的核心含义:一次定终身
final 的字面意思是「最终的」。它出现在哪,就把哪里锁死:
| 位置 | 效果 | 违反时的编译报错关键词 | 标准库例子 |
|---|---|---|---|
| 修饰类 | 这个类不能被继承 | cannot inherit from final | String、Integer |
| 修饰方法 | 这个方法不能被重写 | cannot override ... is final | Object.getClass() |
| 修饰变量 | 这个变量只能赋值一次 | cannot assign a value to final variable | Integer.MAX_VALUE |
三种用法看着不相干,本质是同一件事:声明「到此为止,不许再改」。
三处的检查全在编译期完成,字节码里连尝试修改的指令都生成不出来,不存在「运行时偷偷改掉」的可能。
final 修饰类:封死继承
final class Config {
private final String env;
public Config(String env) { this.env = env; }
public String getEnv() { return env; }
}
// 编译错误:cannot inherit from final Config
// class DevConfig extends Config { }
```bash
标准库里的 `String`、`Integer`、`Long`、`Double` 都是 `final` 类。
为什么要封死?因为 `String` 的不可变性是整个 Java 生态的基石——字符串常量池、`HashMap` 的键、并发安全都建立在「字符串不会变」之上。如果允许继承,任何人都能写一个「可变的伪 String」,把这些假设全部推翻。
> [!TIP]
> 什么时候该给自己的类加 `final`?两种情况:一是这个类的行为不容许被篡改(安全、不变类);二是这个类压根没设计成可继承的(没有 `protected` 扩展点、构造方法里做了完整初始化)。
## final 修饰方法:封死重写
有时候整个类可以继承,只是某几个方法不能动:
```java
class Account {
protected double balance;
// 核心风控逻辑,子类不许改
public final boolean canWithdraw(double amount) {
return amount > 0 && amount <= balance;
}
// 这个可以改
public String describe() {
return "普通账户,余额 " + balance;
}
}
class VipAccount extends Account {
@Override
public String describe() { // OK
return "VIP 账户,余额 " + balance;
}
// @Override
// public boolean canWithdraw(double amount) { return true; } // 编译错误
}
final 方法给继承体系划出「不可协商区」。上一章提到的「不要在构造方法里调用可重写方法」,解法之一就是把那个方法声明为 final。
Note
private方法隐含就是final的——子类看不见它,自然也重写不了。给private方法再加final属于多此一举。
final 修饰变量:只能赋值一次
这是日常用得最多的一种。
局部变量
public void demo() {
final int max = 100;
// max = 200; // 编译错误:cannot assign a value to final variable max
final int min; // 可以先声明后赋值
min = 1; // 第一次赋值,OK
// min = 2; // 第二次赋值,编译错误
}
```bash
关键是「**只能赋值一次**」,不是「必须声明时赋值」。
### 成员字段:三种初始化时机
`final` 实例字段必须在对象构造完成前被赋值,有三个时机可选。
**时机一:声明时直接赋值。**
```java
class Circle {
private final double PI = 3.14159;
}
时机二:在实例代码块中赋值。
class Circle {
private final double pi;
{ pi = 3.14159; }
}
```java
**时机三:在构造方法中赋值(最常用)。**
```java
class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x; // 构造时赋值
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
三个时机只能挑一个,重复赋值就是编译错误。如果一个 final 字段哪个时机都没赋值,编译器同样报错。
Tip时机三是构建不可变对象的标准姿势:字段全部
final,只在构造方法里赋值,只提供 getter 不提供 setter。这样对象一旦创建就冻结了,天然线程安全。
静态常量
static final 组合起来表示「类级别的常量」:
class MathConst {
public static final double PI = 3.141592653589793;
public static final int MAX_RETRY = 3;
public static final String APP_NAME = "MyApp";
}
```bash
命名规范:**全大写,单词间用下划线分隔**。这是 Java 社区的铁律,看到全大写就知道是常量。
`static final` 的基本类型和字符串字面量会被编译器内联到使用处,运行期零开销。
## 最大的坑:final 锁的是引用,不是内容
来看这段让无数人翻车的代码:
```java
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
final List<String> names = new ArrayList<>();
names.add("小明"); // 完全合法!
names.add("小红"); // 完全合法!
System.out.println(names); // [小明, 小红]
// names = new ArrayList<>(); // 这行才会编译错误
}
}
names 是 final 的,为什么还能往里加元素?
因为对引用类型来说,变量存的是对象的地址。final 锁住的是「这个变量永远指向同一个对象」,至于那个对象内部怎么变,final 管不着。
用一句话记:final 引用 = 门牌号不能改,屋里的家具随便搬。
把两种情况的报错时机摆一起,区别更清楚:
final int[] arr = {1, 2, 3};
arr[0] = 99; // 合法,改的是"屋里的家具"
System.out.println(arr[0]); // 99
// arr = new int[]{4, 5, 6}; // 编译报错,改的是"门牌号"
```java
放开最后一行,`javac` 立刻拦下:
```text
error: cannot assign a value to final variable arr
arr = new int[]{4, 5, 6};
^
1 error
而 arr[0] = 99 从头到尾没有任何警告。同一个 final 变量,一个动得了一个动不了,判断标准只有一条:你改的是变量本身,还是变量指向的那个对象的内部。
真想让内容也不可变怎么办
用 List.of() 创建不可变集合(Java 9 引入):
import java.util.List;
public class Main {
public static void main(String[] args) {
final List<String> names = List.of("小明", "小红");
System.out.println(names); // [小明, 小红]
try {
names.add("小刚"); // 运行时抛异常
} catch (UnsupportedOperationException e) {
System.out.println("不可变集合,加不进去");
}
}
}
```bash
`List.of()` 返回的集合任何修改操作都会抛 `UnsupportedOperationException`。这才是真正的「内容不可变」。
> [!WARNING]
> 注意区分:`final` 是**编译期**检查,改引用直接编译不过;不可变集合是**运行期**检查,改内容抛异常。两者不是一回事,最好一起用。
## 完整可运行示例
把三种用法串一遍:
```java
import java.util.List;
public class Main {
// 静态常量
public static final String VERSION = "1.0.0";
public static void main(String[] args) {
System.out.println("版本: " + VERSION);
Point p = new Point(3, 4);
System.out.println("坐标: " + p);
System.out.println("距离原点: " + p.distance());
// final 引用 + 可变对象
final StringBuilder sb = new StringBuilder("Hello");
sb.append(", World"); // 内容可变
System.out.println(sb); // Hello, World
// sb = new StringBuilder(); // 引用不可变,编译错误
// final 引用 + 不可变对象
final List<Integer> nums = List.of(1, 2, 3);
System.out.println("总和: " + nums.stream().mapToInt(Integer::intValue).sum());
}
}
// final 类:不能被继承
final class Point {
// final 字段:构造时赋值,之后不可改
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// final 方法(这里其实多余,因为类已经是 final 的,写出来仅作演示)
public final double distance() {
return Math.sqrt(x * x + y * y);
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
javac Main.java
java Main
```text
输出:
```text
版本: 1.0.0
坐标: Point[x=3, y=4]
距离原点: 5.0
Hello, World
总和: 6
final 与 Lambda:effectively final
Lambda 表达式(Java 8 引入)和匿名内部类捕获局部变量时,要求这个变量是 final 或者「事实上 final」(effectively final)——也就是赋值之后再没被改过。
public void demo() {
int count = 10; // 没写 final,但之后没改 → effectively final
Runnable r = () -> System.out.println(count); // 合法
r.run();
}
public void bad() {
int count = 10;
count = 20; // 改了!不再是 effectively final
// Runnable r = () -> System.out.println(count); // 编译错误
}
```bash
Java 8 之前必须显式写 `final`,Java 8 起编译器能自动推断,写不写都行。但显式写出来可读性更好。
> [!NOTE]
> 为什么有这个限制?因为 Lambda 捕获的是变量的**值拷贝**。如果原变量还能改,Lambda 里读到的就是过期数据,容易产生难以察觉的 bug。Java 干脆从语法上禁止了。
## 小结
- `final` 类不能被继承,`String`、`Integer` 都是这么保护自己的。
- `final` 方法不能被重写,用于锁定继承体系中的核心逻辑。
- `final` 变量只能赋值一次;实例字段可在声明处、实例代码块或构造方法三选一赋值。
- `static final` 表示常量,命名用全大写加下划线。
- **`final` 只锁引用不锁内容**,要真正不可变请配合 `List.of()` 这类不可变对象。
- Lambda 捕获的局部变量必须是 `final` 或 effectively final。
## 常见疑问
**Q:`final` 能加在方法参数上吗?**
能,而且合法。加了之后方法体内就不能给这个参数重新赋值:
```java
public void print(final String msg) {
// msg = "改了"; // 编译错误
System.out.println(msg);
}
好处是防止误改参数造成逻辑混乱,代价是签名变长。团队里统一风格即可,不是硬性要求。
Q:final 会让程序跑得更快吗?
static final 的基本类型和字符串字面量会被编译器内联,这算实打实的收益。至于 final 方法,早年有「便于内联优化」的说法,但现代 JIT 编译器自己就能判断哪些方法可以内联,不需要靠 final 提示。用 final 的理由是表达设计意图,不是压榨性能。
Q:final 和不可变对象是一回事吗?
不是。final 只是「变量不能改指向」,不可变对象是「对象内部没有任何可变状态」。想造一个真正的不可变类,光加 final 不够,还得:字段全 private final、不提供 setter、类本身 final 防止子类开后门、构造方法里对传入的可变对象做拷贝。第 49 章的 record 就是 Java 帮你把这套模板一次性写好的产物。
下一章讲多态——把前面四章的继承、重写、super 全部串成一条威力线。