首页 / Java 入门教程 / final 关键字

Java 入门教程

final 关键字

本教程共 100 篇 · 第 42 篇 · 更新于 2026-08-05 · 约 16 分钟阅读

JavaJava 入门教程final常量不可变继承

42. final 关键字

本节目标:搞清楚 final 修饰类、方法、变量分别是什么效果,会用它定义常量和不可变对象,并避开「引用不可变 ≠ 内容不可变」这个大坑。

final 的核心含义:一次定终身

final 的字面意思是「最终的」。它出现在哪,就把哪里锁死

位置效果违反时的编译报错关键词标准库例子
修饰类这个类不能被继承cannot inherit from finalStringInteger
修饰方法这个方法不能被重写cannot override ... is finalObject.getClass()
修饰变量这个变量只能赋值一次cannot assign a value to final variableInteger.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<>();   // 这行才会编译错误
    }
}

namesfinal 的,为什么还能往里加元素?

因为对引用类型来说,变量存的是对象的地址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 全部串成一条威力线。