吃透 Java final 关键字:用法、原理与源码应用场景
·
在 Java 开发中,final是一个高频使用但容易被浅尝辄止的关键字。它的核心语义是 “不可变、最终的”,但在修饰类、方法、变量时的表现差异,以及在 JDK 源码中的巧妙应用,往往是面试和实际开发的关键考点。本文将从基础用法到源码实践,全方位拆解final关键字,帮你彻底掌握它的本质。
一、final 关键字核心定义
final(最终的)是 Java 的非访问修饰符,用于限制目标的可修改性,核心作用是约束变化、保证不可变性,可修饰:
- 变量(局部变量、成员变量、静态变量)
- 方法
- 类
设计初衷:
- 保证数据安全性(如常量不被篡改);
- 提升代码可读性(明确标识 “不可变” 语义);
- 辅助 JVM 优化(编译期确定常量值、避免方法重写带来的动态分派开销)。
二、final 的基础用法详解
2.1 修饰变量:不可变的 “常量”
final修饰变量时,变量成为 “只读常量”,核心规则是:只能赋值一次,赋值后不可修改。
2.1.1 不同类型变量的赋值规则
| 变量类型 | 赋值要求 |
|---|---|
| 局部变量 | 声明后必须赋值(直接赋值 / 先声明后赋值二选一,仅能赋值一次) |
| 成员变量(实例) | 必须在「声明时、构造方法、初始化块」三者中选其一赋值 |
| 静态变量 | 必须在「声明时、静态初始化块」中赋值(不能在构造方法赋值,因为静态变量初始化更早) |
2.1.2 基础示例
public class FinalVariableDemo {
// 1. 静态常量(推荐命名:全大写+下划线分隔)
public static final double PI = 3.1415926;
// 2. 实例常量:声明时直接赋值
final String DEFAULT_NAME = "未知用户";
// 3. 实例常量:初始化块赋值
final int DEFAULT_AGE;
{
DEFAULT_AGE = 18;
}
// 4. 实例常量:构造方法赋值(支持动态赋值)
final String USER_ID;
public FinalVariableDemo(String userId) {
this.USER_ID = userId; // 仅能赋值一次
}
public static void main(String[] args) {
// 局部常量:先声明后赋值
final int MAX_COUNT;
MAX_COUNT = 100;
// MAX_COUNT = 200; // 编译报错:无法为最终变量分配值
// 关键易错点:引用类型常量
final User user = new User("张三", 20);
// user = new User("李四", 21); // 编译报错:引用地址不可变
user.setAge(21); // 合法:对象内容可修改
System.out.println(user.getAge()); // 输出 21
}
}
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// get/set方法
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
2.1.3 关键注意点
- 基本类型:值不可变;
- 引用类型:引用地址不可变,但对象 / 数组的内容可修改(如
final int[] arr = {1,2}; arr[0] = 3;合法); - 静态常量:推荐
public static final组合使用,成为全局常量,编译期即可确定值(编译时常量)。
2.2 修饰方法:禁止重写
final修饰方法时,子类无法重写该方法(但可继承使用),核心作用是固定方法逻辑,防止子类篡改。
2.2.1 核心规则
- 可继承:子类能正常调用父类的 final 方法;
- 不可重写:子类不能定义与父类 final 方法签名一致的方法;
- 可重载:子类可定义同名但参数不同的方法(重载不受 final 限制)。
2.2.2 示例代码
public class FinalMethodDemo {
// final方法:禁止子类重写
public final void printInfo() {
System.out.println("父类固定逻辑");
}
// 普通方法:允许重写
public void normalMethod() {
System.out.println("父类普通方法");
}
}
class SubDemo extends FinalMethodDemo {
// 编译报错:无法重写最终方法 FinalMethodDemo.printInfo()
// @Override
// public void printInfo() {
// System.out.println("子类修改逻辑");
// }
// 合法:重载final方法(参数不同)
public final void printInfo(String msg) {
System.out.println("子类重载方法:" + msg);
}
// 合法:重写普通方法
@Override
public void normalMethod() {
System.out.println("子类修改普通方法逻辑");
}
public static void main(String[] args) {
SubDemo demo = new SubDemo();
demo.printInfo(); // 调用父类final方法,输出:父类固定逻辑
demo.printInfo("测试"); // 调用子类重载方法,输出:子类重载方法:测试
demo.normalMethod(); // 调用子类重写方法,输出:子类修改普通方法逻辑
}
}
2.3 修饰类:禁止继承
final修饰类时,该类成为 “最终类”,无法被任何类继承,核心作用是保证类的功能完整性和安全性。
2.3.1 适用场景
- 核心工具类 / 基础类:防止子类篡改核心逻辑(如 JDK 的
String、Integer); - 安全敏感类:避免通过继承注入恶意代码;
- 性能优化:JVM 无需为 final 类做方法重写的动态分派优化,提升调用效率。
2.3.2 示例代码
// final类:禁止继承
final class FinalClassDemo {
public void show() {
System.out.println("final类的方法");
}
}
// 编译报错:无法继承最终类 FinalClassDemo
// class SubClass extends FinalClassDemo {
// }
public class TestFinalClass {
public static void main(String[] args) {
// 合法:实例化并使用final类
FinalClassDemo demo = new FinalClassDemo();
demo.show(); // 输出:final类的方法
}
}
三、final 在 JDK 源码中的典型应用
理解final的最佳方式是看 JDK 源码的设计思路,以下是高频应用场景:
3.1 修饰核心类:保证类不可继承
JDK 中大量基础类被声明为final,避免子类修改核心逻辑:
java.lang.String:字符串是不可变对象的核心,若允许继承,子类可篡改equals()、hashCode()等关键方法,导致整个 Java 字符串体系崩溃;java.lang.Integer/Long等包装类:保证数值类型的不可变性,避免多线程环境下的安全问题;java.util.UUID:唯一标识生成类,禁止继承以保证 ID 生成规则的唯一性。
String 类源码片段:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 字符数组:final修饰,保证引用不可变
private final char value[];
// 构造方法
public String() {
this.value = "".value;
}
// ... 其他方法
}
3.2 修饰常量:定义全局不可变值
JDK 源码中大量使用public static final定义常量,作为行业标准值:
java.lang.Math:定义数学常量,如PI、E;java.nio.charset.StandardCharsets:定义字符集常量(UTF_8、GBK等);java.net.HttpURLConnection:定义 HTTP 状态码常量(HTTP_OK=200、HTTP_NOT_FOUND=404)。
Math 类源码片段:
public final class Math {
// 私有化构造方法:禁止实例化
private Math() {}
// 圆周率常量
public static final double PI = 3.14159265358979323846;
// 自然常数
public static final double E = 2.7182818284590452354;
// ... 数学方法
}
3.3 修饰方法:固定核心逻辑
JDK 中关键方法被声明为final,防止子类重写破坏逻辑:
java.lang.Object:getClass()方法是 final 的,保证返回的 Class 对象是当前对象的真实类型,避免子类伪造类型;java.util.ArrayList:size()方法是 final 的,保证返回的集合大小是真实值,防止子类篡改;java.lang.String:equals()、hashCode()、intern()等核心方法是 final 的,保证字符串比较、哈希计算的逻辑统一。
Object 类源码片段:
public class Object {
// final方法:返回对象的运行时类
public final native Class<?> getClass();
// 普通方法:允许重写(如toString()、equals())
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
}
3.4 修饰参数:保证方法参数不可变
JDK 源码中部分方法的参数被声明为 final,防止方法内部修改参数引用:
java.util.Collection:addAll(Collection<? extends E> c)方法的参数 c 常被 final 修饰(不同 JDK 版本略有差异);java.lang.String:concat(String str)方法的参数 str 是 final 的,保证拼接的源字符串不被篡改。
示例(模拟 JDK 风格):
public final class String {
// 拼接字符串方法:参数str为final
public String concat(final String str) {
if (str.isEmpty()) {
return this;
}
int len = value.length;
int otherLen = str.value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
}
四、final 的常见面试题 & 易错点
4.1 面试高频问题
- final 修饰的引用变量,指向的对象内容能改吗?能。final 仅限制引用地址不可变,对象内部属性可正常修改(如
final List list = new ArrayList(); list.add(1);合法)。 - final、finally、finalize 的区别?
- final:关键字,限制不可变;
- finally:异常处理关键字,保证代码块必执行;
- finalize:Object 类的方法,垃圾回收前执行(已被标记为过时)。
- static final 和 final 的区别?
- static final:静态常量,属于类,类加载时初始化,全局唯一;
- final:实例常量,属于对象,每个对象可有不同值(如构造方法赋值的 final 变量)。
4.2 开发易错点
- final 成员变量未赋值:编译报错,需确保在声明 / 构造方法 / 初始化块中赋值;
- 试图继承 final 类:如自定义类继承 String,编译直接报错;
- final 局部变量重复赋值:即使值相同,也会编译报错(如
final int a = 1; a = 1;)。
五、总结
final的核心是限制修改:修饰变量时值 / 引用不可变,修饰方法时不可重写,修饰类时不可继承;- JDK 源码中,
final多用于保护核心类(如 String)、定义全局常量(如 Math.PI)、固定关键方法逻辑(如 Object.getClass ()); - 实际开发中,合理使用 final 可提升代码安全性和可读性,尤其在多线程、常量定义场景下。
更多推荐

所有评论(0)