一、final 的核心语义:一句话概括 “不可改变”

final的核心作用是限制修改,但具体行为需根据修饰对象(变量、方法、类)的不同而区分。我们先通过一张表建立整体认知:

修饰对象核心语义关键特性
变量不可重新赋值基本类型值不变;引用类型地址不变(对象内容可改)
方法不可被重写可继承、可调用;JVM 可能优化
不可被继承类中方法隐式 final;常用于不可变类、工具类

二、修饰变量:最复杂的场景,与类加载深度绑定

final修饰变量时,“不可重新赋值” 是表面规则,其底层更关键的是赋值时机—— 不同类型的变量(静态 / 实例)、不同赋值方式(编译期常量 / 运行期常量),会直接影响类加载阶段的行为。

先回顾:类加载的关键阶段

要理解final变量的赋值,必须先明确类加载的核心流程(仅关注与final相关的阶段):

  1. 连接阶段 - 准备阶段:为静态变量分配内存,设置默认初始值(如int默认 0,引用类型默认null);
  2. 初始化阶段:执行类构造器<clinit>()(静态变量赋值、静态代码块)或对象构造器<init>()(实例变量赋值、实例代码块)。

1. 静态 final 变量(类变量):赋值时机分两种

静态final变量属于 “类级别的常量”,其赋值时机取决于是否为编译期可确定的值

情况 1:编译期常量(常量表达式赋值)

如果静态final变量在声明时,用编译期可计算的常量表达式(字面量、简单算术运算、字符串拼接等)赋值,那么赋值会在连接阶段的准备阶段完成,而非等到初始化阶段。

示例代码

java

public class ConstantDemo {
    // 编译期常量:准备阶段直接赋值为100(而非默认0)
    public static final int MAX_COUNT = 100;
    // 字符串拼接:编译期可确定,准备阶段赋值为"HelloJava"
    public static final String GREETING = "Hello" + "Java";
}

底层逻辑

  • 编译器会将这类常量的值存入.class文件的常量池
  • 类加载到准备阶段时,JVM 直接从常量池取出值,赋给静态final变量(跳过默认初始值);
  • 甚至,其他类引用这类常量时,可能无需加载ConstantDemo类(JVM 直接从常量池获取值,优化性能)。
情况 2:运行期常量(非常量表达式赋值)

如果静态final变量的赋值依赖运行期才能确定的逻辑(如方法调用、对象创建、动态计算),那么准备阶段仅会设置默认初始值,最终赋值在初始化阶段(执行<clinit>())完成。

示例代码

java

import java.util.Random;

public class RuntimeFinalDemo {
    // 运行期常量:准备阶段赋默认值0,初始化阶段才赋值为随机数
    public static final int RANDOM_NUM = new Random().nextInt();
    // 方法调用:准备阶段赋默认值null,初始化阶段调用getVersion()赋值
    public static final String VERSION = getVersion();

    private static String getVersion() {
        return "v" + System.currentTimeMillis(); // 运行期动态计算
    }
}

底层逻辑

  • 准备阶段:RANDOM_NUM被设为 0,VERSION被设为null
  • 初始化阶段:JVM 执行<clinit>(),依次调用new Random().nextInt()getVersion(),完成最终赋值;
  • 访问这类变量前,必须触发类的初始化(因为值依赖运行期逻辑)。

2. 实例 final 变量(对象变量):与对象生命周期绑定

实例final变量属于 “对象级别的常量”,其赋值与类加载阶段无关,仅与对象实例化过程绑定,核心规则是:必须在对象构造完成前赋值

赋值时机(二选一,否则编译报错)
  1. 声明时直接赋值:对象创建时,变量随对象初始化直接赋值;
  2. 构造器 / 实例代码块赋值:确保所有构造器都覆盖赋值(避免部分构造器未赋值)。

示例代码

java

public class User {
    // 方式1:声明时赋值
    private final String username = "guest";
    // 方式2:构造器赋值(需覆盖所有构造器)
    private final int userId;

    // 无参构造器
    public User() {
        this.userId = 1000; // 赋值
    }

    // 有参构造器
    public User(int userId) {
        this.userId = userId; // 不同对象可赋不同值,但赋值后不可改
    }
}
关键特性:对象级别的不可变

实例final变量的 “不可变” 是对象维度的:不同对象的final变量可以有不同的值(如new User(1001)new User(1002)userId不同),但一旦对象构造完成,其final变量就无法再修改。

3. 易错点:final 引用类型的 “不可变边界”

很多开发者误以为final修饰引用类型变量时,“对象本身不可变”—— 这是错误的!final仅限制 “引用地址不可变”,对象内部的属性仍可修改。

示例代码

java

import java.util.ArrayList;
import java.util.List;

public class FinalReferenceDemo {
    public static void main(String[] args) {
        // final修饰List引用
        final List<String> list = new ArrayList<>();
        
        list.add("Java"); // 合法:修改对象内部内容
        System.out.println(list); // 输出:[Java]
        
        // list = new LinkedList<>(); // 报错:修改引用地址(不允许)
    }
}

结论final对引用类型仅保证 “浅不可变”,若需 “深不可变”(对象内容也不可改),需额外设计(如private字段 + 无 setter 方法,参考String类)。

三、修饰方法:不可重写,兼顾安全与性能

final修饰方法时,核心语义是 “该方法不能被子类重写(Override) ”,但可被继承和正常调用。其设计目的和底层优化如下:

1. 核心作用:保护父类核心逻辑

父类中被final修饰的方法,通常是核心业务逻辑(如工具类的算法、框架的流程控制),不允许子类篡改。

示例代码

java

// 父类:定义核心计算逻辑
class Calculator {
    // final方法:不允许子类重写
    public final int add(int a, int b) {
        return a + b; // 核心逻辑,不允许修改
    }

    // 非final方法:允许子类扩展
    public int multiply(int a, int b) {
        return a * b;
    }
}

// 子类:尝试重写final方法(编译报错)
class AdvancedCalculator extends Calculator {
    // @Override
    // public int add(int a, int b) { // 报错:Cannot override the final method from Calculator
    //     return a + b + 1;
    // }

    // 合法:重写非final方法
    @Override
    public int multiply(int a, int b) {
        System.out.println("执行乘法");
        return a * b;
    }
}

2. 底层优化:方法内联

JVM 对final方法有一个重要优化 ——方法内联:由于final方法不可重写,编译器或 JIT(即时编译)可以直接将方法体 “嵌入” 到调用处,避免频繁的方法栈帧创建 / 销毁,提升性能。

例如,调用calculator.add(1,2)时,JVM 可直接替换为1+2,而非通过方法调用流程执行。

3. 与 private 方法的隐藏关系

private方法隐式被 final 修饰(无需显式声明):由于private方法仅在当前类可见,子类无法访问,自然无法重写。

示例代码

java

class Parent {
    // private方法:隐式final
    private void print() {
        System.out.println("Parent print");
    }
}

class Child extends Parent {
    // 注意:这是新方法,不是重写(父类print()不可见)
    public void print() {
        System.out.println("Child print");
    }
}

public class PrivateFinalDemo {
    public static void main(String[] args) {
        Parent parent = new Child();
        // parent.print(); // 报错:print()在Parent中是private,无法调用
    }
}

四、修饰类:不可继承,保障不可变性与安全性

final修饰类时,核心语义是 “该类不能被继承”(无子类),常用于设计不可变类或工具类,避免子类破坏原有设计。

1. 典型应用场景

场景 1:不可变类(如 String、Integer)

java.lang.String是典型的final类,其设计目的是 “实例内容不可变”:若允许继承String,子类可能重写substring()concat()等方法,返回可变对象,破坏String的不可变性。

示例代码(模拟不可变类)

java

// final类:不可继承
public final class ImmutableUser {
    // private final字段:保证外部无法修改
    private final String name;
    private final int age;

    // 构造器:初始化所有字段
    public ImmutableUser(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 仅提供getter,无setter
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
场景 2:工具类(如 Math、Collections)

java.lang.Mathjava.util.Collections等工具类,仅提供静态方法,无需继承。用final修饰可防止无意义的继承,同时避免子类篡改工具方法。

2. 关键特性:类中方法隐式 final

final类中的所有方法(无论是否显式声明final)都隐式为 final—— 因为类不可继承,方法自然无法被重写。但字段可以是非 final 的(如final类中可定义普通实例变量)。

示例代码

java

// final类
final class FinalClass {
    // 普通字段(非final)
    private int count;

    // 方法:隐式final(无需显式声明)
    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

// class SubClass extends FinalClass { // 报错:Cannot extend final class 'FinalClass'
// }

五、final 与线程安全:不可变性的天然优势

在多线程环境中,final变量是保障线程安全的 “轻量级工具”,其核心依赖Java 内存模型(JMM)的初始化安全性

问题背景:非 final 变量的 “半初始化” 风险

非 final 变量可能因 JVM 指令重排序,导致其他线程看到 “半初始化” 的对象。例如:

java

class UnsafeUser {
    private String name; // 非final

    public UnsafeUser() {
        name = "Alice"; // 步骤1:赋值
    }

    public String getName() {
        return name;
    }
}

// 线程A:创建对象
UnsafeUser user = new UnsafeUser();
// 线程B:获取name(可能看到null,因为指令重排序导致步骤1未执行)
String name = user.getName();

final 的解决方案:禁止重排序

JMM 规定:final字段的赋值操作与对象构造完成的操作之间,禁止重排序。这意味着:

  • 只要对象的构造器正常结束,其他线程看到的final字段一定是 “完全初始化后的值”,不会看到默认初始值(如null、0)。

示例代码(线程安全的对象)

java

class SafeUser {
    private final String name; // final字段

    public SafeUser() {
        name = "Alice"; // final赋值,禁止与构造完成重排序
    }

    public String getName() {
        return name; // 其他线程一定看到"Alice",而非null
    }
}

结论final字段无需额外加锁,即可保证多线程下的初始化安全,是实现线程安全的 “低成本方案”。

六、常见误区与最佳实践

误区 1:final 引用类型 = 对象不可变

错。final仅保证引用地址不变,对象内部属性可修改(如final Listadd元素)。若需对象不可变,需结合private字段 + 无 setter 方法。

误区 2:所有变量都加 final 更安全

过度使用final会降低代码灵活性(如后续需求变更需修改变量时,需删除final)。仅对 “确实不需要修改” 的变量加final

最佳实践

  1. 全局常量用public static final:如接口中的常量(public static final String CONFIG_KEY = "app.config";),明确标识 “全局唯一、不可变”。
  2. 方法参数加 final:若方法内无需修改参数,加final可增强可读性(如public void print(final String msg))。
  3. 核心逻辑方法加 final:父类中不允许子类篡改的方法(如工具类的核心算法),用final保护。
  4. 不可变类 / 工具类加 final:如自定义的不可变模型类、工具类,用final防止继承破坏设计。
  5. 多线程共享对象的关键字段加 final:如线程安全的 DTO 类,其关键字段用final保证初始化安全。

七、总结

final关键字看似简单,实则是 Java 设计思想的集中体现 —— 通过 “限制修改”,平衡代码的安全性、性能与可读性

  • 修饰变量:控制赋值时机,结合类加载优化,保障线程安全;
  • 修饰方法:保护核心逻辑,支持 JVM 内联优化;
  • 修饰类:保障不可变性,防止子类篡改。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐