final 关键字作用

面试官问:"final 关键字有什么作用?"

候选人小曹答:"final 修饰的变量不能被修改。"

面试官点点头,又问:"final 修饰的变量,如果是引用类型呢?"

小曹说:"引用不能变。"

面试官追问:"那引用的对象能变吗?"

小曹说:"能。"

面试官又问:"那 String 为什么是不可变的?"

小曹答:"因为 String 用 final 修饰了..."

面试官:"不对,String 底层是一个 char 数组,final 只保证引用不变,不保证数组内容不变。"

小曹彻底懵了。

【面试官心理】 这道题能精准区分"背概念"和"真理解"的候选人。final 修饰引用类型时,只保证引用不变(地址不变),不保证对象内容不变。String 的不可变是多重设计保证的,不是单纯靠 final。

一、final 的三种用法 🔴

修饰目标作用结果
变量赋值后不能改变基本类型值不变,引用类型引用不变
方法不能被重写子类无法修改该方法实现
不能被继承禁止子类化

二、final 修饰变量 🔴

2.1 基本类型 vs 引用类型

// 基本类型:值不可变
final int a = 10;
a = 20; // ❌ 编译错误

// 引用类型:引用不可变
final int[] arr = new int[]{1, 2, 3};
arr[0] = 100; // ✅ 可以,修改的是对象内容
arr = new int[]{4, 5, 6}; // ❌ 编译错误,重新赋值引用

// String 的情况:
final char[] value = new char[]{'h', 'e', 'l', 'l', 'o'};
value[0] = 'H'; // ✅ 可以!数组内容可以修改!
// 但 String 没有提供任何修改 value 的方法
// String 的不可变是 final + 无修改方法 + 私有化 共同保证的
⚠️

final 和不可变是两回事

  • final 修饰数组:引用不变,数组内容可变
  • String 不可变:需要 final(防止引用重定向)+ 无修改方法 + 私有化(防止外部访问数组) :::

2.2 final 变量的初始化

class Example {
    // 1. 声明时初始化
    final int a = 10;

    // 2. 构造器中初始化
    final int b;
    Example(int b) {
        this.b = b;
    }

    // 3. 静态代码块中初始化
    final static double PI;
    static {
        PI = 3.14159;
    }

    // ❌ final 实例变量必须且仅能被赋值一次
    // 如果在构造器和声明处同时赋值,编译错误
    final int c = 1;
    Example() {
        // this.c = 2; // ❌ 重复赋值,编译错误
    }
}

2.3 blank final(空白 final)

class User {
    final int id; // blank final,必须在构造器中初始化

    User(int id) {
        this.id = id; // ✅ 必须赋值
    }

    User() {
        this.id = 0; // ✅ 每个构造器都必须初始化
    }
}

三、final 修饰方法 🔴

3.1 规则

class Parent {
    final void finalMethod() {
        System.out.println("Cannot override");
    }

    void normalMethod() { }
}

class Child extends Parent {
    // ❌ 编译错误:无法重写 final 方法
    // void finalMethod() { }

    // ✅ 可以重写普通方法
    @Override
    void normalMethod() { }
}

3.2 为什么用 final 方法

// 1. 设计意图:有些方法不希望被子类修改
class SecureData {
    final void validate() {
        // 安全性验证,不允许子类修改
    }
}

// 2. 性能优化:JIT 编译器可以对 final 方法做更多内联优化
// private 方法天然不可见,JIT 可以直接内联
// static 方法天然可以内联
// final 方法告诉 JIT"可以安全内联"

:::tip 💡 JIT 编译器对 final 方法可以做更多激进优化。但现代 JVM 的逃逸分析已经非常智能,final 的性能提升已经不明显了。final 更重要的价值是设计意图的表达代码可读性

四、final 修饰类 🔴

4.1 不可继承

final class Immutable {
    // 没有人能继承这个类
}

// ❌ 编译错误
// class SubImmutable extends Immutable { }

// String 就是 final 类
// 为什么 String 要设计为 final?
// 1. 保证字符串常量池的安全性
// 2. 保证 hashCode 的确定性(不可变对象的 hashCode 可以缓存)
// 3. 保证多线程安全性(不可变对象天然线程安全)

4.2 final 类中的方法默认是 final 吗?

final class FinalClass {
    void method1() { } // 不是 final,可以被重写(如果类不是 final)
    final void method2() { } // 显式 final,不能被重写
}

五、final 的最佳实践 🔴

5.1 不可变对象

// 不可变对象 = 所有字段 final + 类 final + 无修改方法 + 防御性拷贝
public final class Money {
    private final long amount;
    private final String currency;

    public Money(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    // ✅ 只读,无 setter
    public long getAmount() { return amount; }
    public String getCurrency() { return currency; }

    // ✅ "修改"操作返回新对象
    public Money add(Money other) {
        if (!this.currency.equals(other.currency))
            throw new IllegalArgumentException();
        return new Money(this.amount + other.amount, this.currency);
    }
}

5.2 局部变量用 final

// JDK 8+ effectively final,可以不显式写 final
void method() {
    int x = 10; // effectively final
    Runnable r = () -> System.out.println(x); // Lambda 中引用 effectively final 变量
    // x = 20; // 如果放开这行,Lambda 编译错误
}

// 显式 final 代码更清晰
void methodWithFinal() {
    final int x = 10;
    Runnable r = () -> System.out.println(x);
}

六、与 static 的组合使用 🔴

组合含义内存位置
static final类常量,编译期常量方法区/元空间
final static同上(顺序无关)方法区/元空间
static类变量,运行时可修改方法区/元空间
final实例常量,构造后不可变堆(随对象)
// static final:类常量(编译期确定)
static final int MAX_SIZE = 100; // 编译时内联
static final Random RANDOM = new Random(); // 运行时初始化

// static final + 基本类型/String:编译期常量
// 编译器直接内联替换所有使用处

七、追问升级

面试官:"static final 和 final static 有区别吗?"

// 没有任何区别,JVM 编译结果完全一样
// 只是代码风格问题
// Sun 的 Java 代码规范推荐:static final(先 static 再 final)

面试官:"为什么 IntegerCache 缓存 -128~127,而不是更大的范围?"

// Integer 的静态内部类 IntegerCache
static final int low = -128;
static final int high = 127;
// high 可以通过 -XX:AutoBoxCacheMax=<size> 设置

// 选择 127 的原因:
// 1. 统计数据显示 -128~127 覆盖了绝大多数日常使用场景
// 2. 更大的范围会增加内存开销
// 3. 这是一个经验值,不是精确计算

【面试官心理】 能说出 IntegerCache 范围选择的候选人是真正研究过 JDK 源码的。这个细节是 P6+ 的加分项。