一、基础篇:final 的三种用法(像给变量上锁)

1. final 变量(一次性密码锁)

// 声明时赋值(像设置初始密码)
final int MAX_RETRY = 3;

// 构造函数赋值(像首次使用必须修改密码)
class User {
    final String id;

    public User(String id) {
        this.id = id; // 这里必须赋值!
    }
}

2. final 方法(禁止子类修改)

class Parent {
    // 像贴上"禁止覆盖"的标签
    final void importantMethod() {
        // 关键逻辑...
    }
}

class Child extends Parent {
    // 这里会编译报错!无法修改父类的重要方法
    // void importantMethod() { ... }
}

3. final 类(终极防护罩)

// 像用防弹玻璃保护的类
final class SecurityUtils {
    // 这个类不能被继承
}

// 编译错误!无法继承final类
// class HackerUtils extends SecurityUtils { ... }

二、进阶篇:final 在并发中的魔法

1. 构造函数的"安全结界"(可视化原理)

class SafeBox {
    final String secret;

    public SafeBox(String password) {
        this.secret = password; // ① 写final域
        // 编译器在此插入"结界"(内存屏障)
    } // ② 对象引用赋值
}

2. 多线程下的安全读取

// 主线程
final SharedData data = new SharedData(); // 正确构造的final对象

// 线程A
new Thread(() -> {
    System.out.println(data.value); // 保证看到正确值
}).start();

3. 典型错误案例:提前暴露的保险箱

class LeakExample {
    final int secret;
    static LeakExample leaked;

    public LeakExample() {
        secret = 123;       // 设置密码
        leaked = this;      // 危险!保险箱还没锁好就暴露位置
        // 其他初始化操作...
    }
}

三、高手进阶:JVM 如何实现 final

1. 类文件中的 final 标记

class Demo {
    final int number = 42;
}

对应的 class 文件结构:

字段表:
  access_flags: ACC_FINAL (0x0010)
  name: "number"
  descriptor: "I"

2. 内存屏障的真相(快递打包比喻)

  • StoreStore 屏障:像快递员打包时先放重要物品(final 变量),再贴快递单(对象引用)
  • LoadLoad 屏障:像收件人必须先检查快递单(对象引用),再拆封重要物品(final 变量)

四、实际应用场景

1. Spring 中的 final 依赖注入

@Service
public class OrderService {
    private final PaymentService paymentService;

    // 构造函数注入(线程安全)
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

2. 多线程配置类

public class AppConfig {
    // 全局配置项(安全发布)
    public static final ExecutorService executor =
        Executors.newFixedThreadPool(4);
}

五、常见问题解答

Q:为什么匿名类访问外部变量要加 final?

void demo() {
    int count = 0;
    // 需要改为:final int count = 0;
    new Thread(() -> {
        System.out.println(count); // 编译错误!
    }).start();
}

原因:匿名类可能在其他线程执行,final 保证值的稳定性

Q:反射能修改 final 字段吗?

Field field = MyClass.class.getDeclaredField("FINAL_FIELD");
field.setAccessible(true);
field.set(obj, newValue); // 抛出IllegalAccessException!

解决方法:通过反射修改 Field 的 modifiers 字段(危险操作!)

六、面试专题:final 高频 10 问(P7 级考点)

Q1:为什么 lambda 表达式/匿名内部类访问外部变量必须是 final?

void demo() {
    int count = 0;
    // 实际编译时会自动加final
    Runnable r = () -> System.out.println(count);
}

答案

  1. 生命周期不一致:外部方法栈帧销毁后仍需访问变量
  2. 值一致性:保证所有线程看到相同值
  3. JVM 实现:自动生成合成字段(synthetic field)

Q2:final 字段在类加载哪个阶段初始化?

答案

类加载流程:
加载 → 验证 → 准备(默认值)→ 解析 → 初始化(执行<clinit>)
               ↓
final static基本类型:准备阶段直接赋值

Q3:如何实现线程安全的延迟初始化?

Spring 最佳实践

@Service
public class LazyService {
    private final Object lock = new Object();
    private volatile Resource resource;

    public Resource getResource() {
        if (resource == null) {
            synchronized (lock) {
                if (resource == null) {
                    resource = new Resource();
                }
            }
        }
        return resource;
    }
}

Q4:final 字段与序列化的冲突如何解决?

public class User implements Serializable {
    private final String uid;

    // 自定义序列化逻辑
    private void readObject(ObjectInputStream ois)
        throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        // 通过反射重新初始化final字段
        Field uidField = User.class.getDeclaredField("uid");
        uidField.setAccessible(true);
        uidField.set(this, (String) ois.readObject());
    }
}

Q5:final 引用逃逸问题如何检测?

案例分析

public class EscapeExample {
    final int x;
    static EscapeExample instance;

    public EscapeExample() {
        x = 42;
        instance = this; // 危险!
    }
}

检测工具

  • FindBugs:RC_REF_COMPARISON_BAD
  • IDEA Inspection:“this” escape in constructor

Q6:final vs volatile 对比

特性 final volatile
可见性 构造函数完全初始化后可见 写操作后立即可见
原子性 只保证引用可见性 不保证复合操作原子性
使用场景 不可变对象 状态标志

Q7:反射修改 final 字段的黑魔法

Field field = User.class.getDeclaredField("uid");
field.setAccessible(true);

// 移除final修饰符
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

field.set(user, "new_id"); // 危险操作!

⚠️ 导致问题:

  1. 破坏编译器优化
  2. 可能引发 SecurityManager 告警
  3. 多线程环境下出现可见性问题

Q8:为什么内部类持有外部类引用必须是 final?

JVM 层解释

// 源代码
class Outer {
    void foo() {
        final int local = 42;
        new Inner() {
            void bar() { System.out.println(local); }
        };
    }
}

// 编译后生成:
class Outer$1 extends Inner {
    private final int val$local;
    Outer$1(int local) { this.val$local = local; }
    void bar() { System.out.println(val$local); }
}

Q9:String 类的 final 设计哲学

public final class String {
    private final char value[];
    // 所有修改操作都返回新对象
    public String concat(String str) { /*...*/ }
}

设计优势

  1. 安全性:防止篡改(如网络请求参数)
  2. 性能:缓存 hashCode、线程安全
  3. 内存:字符串常量池优化

Q10:final 方法继承的边界情况

class Parent {
    public final void method() {}
}

class Child extends Parent {
    // 编译错误:Cannot override
    // public void method() {}

    // 合法:隐藏父类方法(非重写)
    public static void method() {}
}

JLS 规范

  • final 方法不能被子类重写
  • 允许静态方法隐藏父类实例方法(不推荐)