final 关键字
本节定位:
final是 Java 中最常用的修饰符之一。它可以在类、方法、变量三个层面施加"不可变"约束。深入理解final的语义——特别是引用类型的final——对于编写安全、可维护的代码至关重要。本节还将讨论final与String不可变性的关系,以及空白 final 在构造器注入中的应用。
final 的三重约束
语法速查表
| 修饰目标 | 语法 | 效果 |
|---|---|---|
| 类 | final class 类名 {} | 该类不能被继承,所有方法隐式 final |
| 方法 | final 返回值 方法名() {} | 该方法不能被子类重写 |
| 局部变量 | final 类型 变量名 = 值; | 变量只能赋值一次 |
| 成员变量 | final 类型 变量名; | 必须显式初始化(构造方法或声明处) |
| 静态变量 | static final 类型 变量名 | 常量,命名通常全大写+下划线 |
| 参数 | 方法(final 类型 参数名) | 方法内不能修改参数值 |
一、final 修饰类:不可继承
// final 类:任何类都不能继承它
public final class StringUtils {
private StringUtils() {} // 工具类:private 构造防止实例化
public static boolean isEmpty(String s) {
return s == null || s.isEmpty();
}
}
// ❌ 编译错误
// public class EnhancedStringUtils extends StringUtils { }
// 错误: 无法从final StringUtils进行继承
JDK 中的 final 类
| 类 | 为什么是 final |
|---|---|
java.lang.String | 安全性——字符串不可变是 Java 安全模型的基础 |
java.lang.Integer 等包装类 | 不可变性保证缓存和比较的正确性 |
java.lang.Math | 工具类,无继承意义 |
java.lang.System | 系统类,防止篡改 |
飞翔科技场景:安全的配置类
架构师白歌要求系统配置类不可被继承:
/**
* 飞翔科技全局系统配置
* final 确保没有子类篡改配置行为
*/
public final class SystemConfig {
public static final String COMPANY_NAME = "飞翔科技";
public static final String HR_EMAIL = "hr@feixiang.net";
public static final int MAX_LEAVE_DAYS = 15;
private SystemConfig() {} // 工具类禁止实例化
}
二、final 修饰方法:不可重写
基础用法
public class Employee {
private String name;
private String id;
/**
* 获取员工唯一标识——所有子类必须使用此逻辑,不可篡改
*/
public final String getId() {
return id;
}
/**
* 模板方法:打印工资单——子类不可重写整体流程
* 但可以通过重写 computeMonthlySalary() 影响结果(多态)
*/
public final void printPaySlip() {
System.out.println("===== 工资单 =====");
System.out.println("姓名: " + getName());
System.out.println("实发: " + computeMonthlySalary()); // 多态
System.out.println("=================");
}
public String getName() { return name; }
public double computeMonthlySalary() { return 0; } // 子类可重写
}
public class Manager extends Employee {
private double bonus;
// ❌ 编译错误:getId() 是 final,不能重写
// @Override
// public String getId() { return "MGR-" + super.getId(); }
@Override
public double computeMonthlySalary() {
return getBaseSalary() + bonus; // ✅ computeMonthlySalary 不是 final
}
}
模板方法模式中的 final
printPaySlip() 是典型的模板方法:定义算法骨架(final),具体步骤留给子类实现(可重写方法)。
三、final 修饰变量:值不可变
基本类型 final:值不可变
public class FinalPrimitiveTest {
public static void main(String[] args) {
final int MAX_VALUE = 100;
// MAX_VALUE = 200; // ❌ 编译错误:无法为 final 变量分配值
System.out.println(MAX_VALUE); // 100
}
}
引用类型 final:地址不可变,内容可变
这是最常见的误区。实习生小崔就曾在这里踩坑:
public class FinalReferenceTest {
public static void main(String[] args) {
final StringBuilder builder = new StringBuilder("飞翔科技");
// builder = new StringBuilder("新公司"); // ❌ 编译错误:不能改引用
builder.append("技术部"); // ✅ 可以修改对象内容!
System.out.println(builder); // 输出:飞翔科技技术部
}
}
核心理解:
final锁定的是栈中引用变量的地址值,而非堆中对象的内容。这和 C++ 的const语义不同。
完整对比示例
import java.util.*;
public class FinalVariableFullTest {
// 1. final 成员变量:必须显式初始化
private final String employeeId; // 构造方法中初始化
private final Date hireDate = new Date(); // 声明时初始化
public static final String COMPANY = "飞翔科技"; // 静态常量
public FinalVariableFullTest(String employeeId) {
this.employeeId = employeeId; // 空白 final 初始化
}
public static void main(String[] args) {
// 2. final 局部变量
final double pi = 3.14159;
System.out.println("pi = " + pi);
// 3. final 数组引用
final int[] numbers = {1, 2, 3, 4, 5};
numbers[0] = 99; // ✅ 可以修改数组元素
// numbers = new int[]{6, 7, 8}; // ❌ 不能修改引用
System.out.println("numbers[0] = " + numbers[0]); // 99
// 4. final 集合引用
final List<String> employeeNames = new ArrayList<>();
employeeNames.add("大翔"); // ✅ 可以修改集合内容
employeeNames.add("白歌");
// employeeNames = new LinkedList<>(); // ❌ 不能修改引用
System.out.println(employeeNames); // [大翔, 白歌]
}
}
运行输出:
pi = 3.14159
numbers[0] = 99
[大翔, 白歌]
四、final 修饰参数
public class FinalParameterTest {
/**
* final 参数确保方法内部不会意外修改参数
*/
public void updateSalary(final Employee emp, final double newSalary) {
// emp = new Employee(...); // ❌ 编译错误
// newSalary = newSalary * 2; // ❌ 编译错误
emp.setSalary(newSalary); // ✅ 可以修改对象内部状态
}
}
| 参数类型 | final 效果 |
|---|---|
final 基本类型 | 方法内不能修改参数值 |
final 引用类型 | 方法内不能修改引用指向,但可以修改对象内容 |
无 final | 方法内可以任意修改(不推荐,容易产生 bug) |
五、空白 final(Blank Final)
空白 final 指声明时不初始化、在构造方法中赋值的 final 成员变量。
public class Manager extends Employee {
private final String managerId; // 空白 final
private final LocalDate promotedDate; // 空白 final
public Manager(String name, String id, double salary, String managerId, LocalDate promotedDate) {
super(name, id, salary);
this.managerId = managerId; // 在构造方法中初始化
this.promotedDate = promotedDate;
}
// ❌ 编译错误:final 字段不能有 setter
// public void setManagerId(String managerId) { this.managerId = managerId; }
}
空白 final 的价值
空白 final 让每个对象可以拥有不同但不可变的属性值——结合了"不可变性"与"灵活性"。
白歌点评:空白 final 是实现"不可变对象"模式的关键技术。配合构造器注入,你可以创建出"一次赋值、永不改变"的对象——这正是 Spring 推荐构造器注入的底层原因之一。
易错场景与避坑指南
反例一:误以为 final 引用 = 不可变对象
// ❌ 危险:final 引用指向可变对象
public class PayrollConfig {
public static final List<String> DEPARTMENTS = new ArrayList<>();
static {
DEPARTMENTS.add("技术部");
DEPARTMENTS.add("市场部");
}
}
// 其他地方可以随意修改!
public class SomeOtherClass {
public void mischief() {
PayrollConfig.DEPARTMENTS.add("黑客部"); // ✅ 编译通过!
PayrollConfig.DEPARTMENTS.clear(); // ✅ 编译通过!
}
}
纠正:使用不可变集合包装:
// ✅ 安全:不可变集合
public class PayrollConfig {
public static final List<String> DEPARTMENTS;
static {
List<String> depts = new ArrayList<>();
depts.add("技术部");
depts.add("市场部");
DEPARTMENTS = Collections.unmodifiableList(depts); // 不可变视图
}
}
反例二:构造方法中未初始化空白 final
public class Employee {
private final String id;
public Employee() {
// ❌ 编译错误:可能尚未初始化变量 id
}
}
纠正:每个构造方法都必须初始化所有空白 final 字段。
反例三:final 方法与 private 方法的混淆
public class Employee {
private void internalLogic() {
System.out.println("Employee internal");
}
}
public class Manager extends Employee {
// 这不是重写!private 方法对子类不可见
private void internalLogic() { // 这是 Manager 的独立方法
System.out.println("Manager internal");
}
}
关键点:
private方法隐式为final,但子类可以定义同名方法——这不是重写,是碰巧同名。加上@Override注解可以让编译器帮你发现这个误区。
final 与性能
final 方法可能被 JIT 编译器**内联(inline)**优化:将方法体直接插入调用处,消除方法调用开销。
public final int getValue() { return 42; }
// JIT 内联后,调用处:
// int x = obj.getValue();
// 变为:
// int x = 42;
白歌提醒:不要把 final 当性能优化手段随意使用。现代 JVM 非常智能,即使不加 final,它也会通过"类层次分析"判断方法是否被重写,自动内联。final 的首要价值是语义约束而非性能。
面试考点
Q1:final、finally、finalize() 有什么区别?
| 项目 | 含义 | 用途 |
|---|---|---|
final | 修饰符关键字 | 类不可继承、方法不可重写、变量不可变 |
finally | 异常处理关键字 | try-catch-finally 中,无论是否异常都执行 |
finalize() | Object 方法 | GC 回收前回调(JDK 9 已过时,不推荐使用) |
Q2:final 修饰的引用类型变量,对象内容可以改吗?
可以。
final锁定的是栈中变量存储的引用地址,而非堆中对象的内容。例如final List<String> list = new ArrayList<>()不能将 list 指向另一个 List,但可以调用list.add()修改集合内容。
Q3:String 为什么被设计为 final?
主要出于三点考量:① 安全性——String 被 JVM 的类加载器、安全管理器等底层机制大量使用,不可变保证其不被恶意篡改;② 字符串常量池——不可变性使字符串字面量可以安全共享,节省内存;③ Hash 缓存——不可变性允许 String 缓存
hashCode()的值,提升 HashMap 等数据结构的性能。
Q4:什么是空白 final?有什么应用场景?
空白 final 指声明时不赋值、在构造方法中初始化的 final 实例变量。典型应用场景是实现"不可变对象"——每个对象依赖初始化后不可修改,但不同对象可以有不同的值。Spring 推荐构造器注入的核心优势之一就是可以配合
final字段声明,保证依赖不可变。