final修饰变量只锁住引用不可再赋值,不保证对象内容不可变;用于方法表示不可重写但可重载;用于类表示不可继承;final字段需在构造完成前安全初始化以保障线程可见性。

final修饰变量时,到底锁住了什么
很多人以为final让变量“不可变”,其实它只保证**引用不可再赋值**,不等于对象内容不可变。比如final List之后,你不能写list = new ArrayList();(编译报错),但list.add("a")完全合法。
常见错误现象:
• 用final Map存配置,运行时被意外修改,误以为安全
• 单元测试里反复add()同一个final集合,结果前一个测试污染后一个
- 若需真正不可变容器,用
Collections.unmodifiableList()或ImmutableList.of()(Guava) - 对自定义类,
final字段 + 私有构造 + 不暴露可变内部状态,才是完整不可变设计 - 基本类型(
int、boolean)用final确实等效于常量,JVM可能内联优化
final方法为什么不能被重写,但能被重载
final加在方法上,是告诉编译器“这个行为不允许子类改变”。它不影响重载(overload),只阻止重写(override)。比如父类有final void log(String msg),子类可以新增void log(String msg, int level),但不能写@Override void log(String msg)。
使用场景:
• 模板方法模式中,templateMethod()设为final,强制子类只实现钩子方法
• 工具类中关键校验逻辑(如validateInput()),防止下游绕过安全检查
立即学习“Java免费学习笔记(深入)”;
- 性能影响极小:现代JVM对
final方法可能做去虚化(devirtualization),但别为此刻意加final - 注意IDE警告:如果一个
final方法从未被继承,加final只是增加维护噪音 - 接口里不能用
final方法(Java 8+允许default和static,但final仍非法)
final类的继承限制与真实用途
声明final class StringUtils,意味着它既不能被继承,也不能被代理(除非用字节码工具)。这不是为了“防止别人扩展”,而是明确表达“这个类的设计边界已封闭”。String、Integer、LocalDateTime都是典型例子。
容易踩的坑:
• 为“防误继承”给工具类加final,结果自己后续需要Mock测试,却无法继承覆写方法
• Lombok的@UtilityClass生成的类默认final,但若类里有非静态字段,会编译失败
- 如果类含可变状态(如缓存、连接池),又想防继承,优先考虑私有构造+静态工厂,而非
final - Spring AOP对
final类的方法无法代理(CGLIB失效,JDK Proxy也不行),日志、事务等切面会静默失效 - Android开发中,
final类在ProGuard混淆时更易保留,但过度使用会增大方法数(因无法内联优化)
final字段的初始化时机与线程安全边界
final字段的特殊性在于:只要在构造器结束前完成初始化,JVM就保证其他线程能看到其正确值(无需synchronized或volatile)。这是Java内存模型(JMM)的“final field semantics”保障。
但前提是——必须在构造过程中完成赋值。下面这段代码是危险的:
public class BadExample {
final int value;
public BadExample() {
this.value = compute(); // ❌ compute()可能调用子类被重写的方法
}
int compute() { return 42; }
}
子类若重写compute(),父类构造器中就可能看到未初始化的对象状态。
- 安全做法:所有
final字段在声明时直接初始化,或只在构造器第一行赋值(且不调用可被重写的方法) - 静态
final字段推荐用private static final+ 大写命名,避免反射修改(虽然反射能绕过,但属于破坏契约) - 注意:
final不保证对象图整体不可变。例如final List中,people Person对象本身仍可被修改
不可变设计不是靠堆砌final实现的,而是从字段、方法、构造器到API语义的一致性约束。最容易被忽略的是:把一个类标为final,却忘了把它持有的可变对象也做成不可变,或者没封住toString()返回的内部状态引用。









