关键字深度解析:final、static、transient
final 关键字
final 的含义是"最终的,不可改变的"。它可以修饰类、方法和变量,分别有不同的语义。
修饰变量:值不可变
final int MAX = 100;
MAX = 200; // ❌ 编译错误
对于基本类型,final 意味着值不能改变。
对于引用类型,final 意味着引用不能重新指向别的对象,但对象内部的状态可以改变:
final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 对象内部状态可以改变
list = new ArrayList<>(); // ❌ 引用不能重新赋值
这就像把钥匙焊死在锁上——你不能换一把钥匙(引用不可变),但门里面的东西你随便改(对象可变)。
修饰方法:不可重写
public class Parent {
public final void doSomething() { ... }
}
public class Child extends Parent {
@Override
public void doSomething() { ... } // ❌ 编译错误
}
final 方法的意图是"锁定行为"——子类可以继承但不能改变这个方法的实现。private 方法隐式地就是 final 的(子类看不到,也就无法重写)。
修饰类:不可继承
public final class String { ... }
Java 标准库中的 String、Integer、Math 等都是 final 类。final 类不能有子类,这保证了它们的行为不会被继承链中的子类所破坏。
final 与线程安全(JMM 语义)
final 在 JMM(Java Memory Model)中有特殊的语义保证:
构造器中对
final字段的写入,与随后将该对象引用赋给其他变量,这两个操作之间存在 happens-before 关系。
通俗地说:当其他线程看到这个对象的引用时,final 字段一定已经正确初始化了。这是 String 等不可变对象天然线程安全的基础。
public class SafePublication {
final int x;
public SafePublication() {
x = 42; // 写入 final 字段
}
// 当另一个线程拿到 SafePublication 的引用时,
// x 一定是 42,不会看到 0 或其他中间值
}
没有 final 修饰,在没有同步的情况下,另一个线程可能看到 x = 0(对象已创建但字段还未初始化),这是指令重排导致的。
final 实例变量的初始化位置
编译器只要求 final 实例变量在每个构造器返回之前一定已被赋值,但赋值的位置有三种选择:
| 方式 | 示例 |
|---|---|
| 声明时直接赋值 | final int x = 42; |
| 实例初始化块 | { x = 42; } |
| 构造器中赋值 | public Foo() { x = 42; } |
上面的 SafePublication 示例选择在构造器中赋值,是为了直观演示 JMM 的 happens-before 语义,并非要求必须在构造器里初始化。
有多个构造器时,每一个构造器都必须保证 final 字段被赋值,否则编译报错:
public class Foo {
final int x;
public Foo(int val) { x = val; } // ✅
public Foo() { /* x 未赋值 */ } // ❌ 编译错误
}
static 关键字
static 的含义是"属于类,而非属于实例"。
静态变量(类变量)
public class Counter {
static int count = 0; // 所有实例共享
int id; // 每个实例独有
public Counter() {
id = ++count;
}
}
静态变量在类加载时就在方法区(Java 8 后是元空间)分配内存,且全局只有一份。所有实例共享同一个 count。
静态方法
public static void main(String[] args) { ... }
静态方法没有 this 引用,因此:
- ✅ 可以访问静态变量和静态方法
- ❌ 不能直接访问实例变量和实例方法
- ❌ 不能使用
this和super
main 方法必须是 static 的,因为 JVM 启动时还没有创建任何对象实例,只能通过类名直接调用。
静态代码块
public class Config {
static Map<String, String> settings;
static {
// 类加载时执行,且只执行一次
settings = loadFromFile("config.properties");
}
}
静态代码块按书写顺序执行,常用于复杂的类级别初始化。
静态内部类
public class Outer {
private int x = 10;
static class StaticInner {
// ❌ 不能访问 x(因为没有外部类实例的引用)
// ✅ 可以独立于 Outer 实例存在
}
class Inner {
// ✅ 可以访问 x(隐式持有 Outer.this)
// ⚠️ 但也意味着 Inner 实例不被回收,Outer 也不会被回收
}
}
静态内部类不持有外部类实例的引用,避免了内存泄漏的风险,是推荐的内部类形式。
初始化顺序
类的初始化顺序是底层运行期的核心考点:
父类静态变量 + 父类静态代码块(按书写顺序)
↓
子类静态变量 + 子类静态代码块(按书写顺序)
↓
父类实例变量 + 父类实例代码块(按书写顺序)
↓
父类构造器
↓
子类实例变量 + 子类实例代码块(按书写顺序)
↓
子类构造器
用一个例子来验证:
class Parent {
static int pStaticVar = initVar("① 父类静态变量");
static { System.out.println("② 父类静态代码块"); }
int pInstanceVar = initVar("⑤ 父类实例变量");
{ System.out.println("⑥ 父类实例代码块"); }
Parent() { System.out.println("⑦ 父类构造器"); }
static int initVar(String msg) {
System.out.println(msg);
return 0;
}
}
class Child extends Parent {
static int cStaticVar = initVar("③ 子类静态变量");
static { System.out.println("④ 子类静态代码块"); }
int cInstanceVar = initVar("⑧ 子类实例变量");
{ System.out.println("⑨ 子类实例代码块"); }
Child() { System.out.println("⑩ 子类构造器"); }
}
public class Main {
public static void main(String[] args) {
System.out.println("===== 第一次 new =====");
new Child();
System.out.println("\n===== 第二次 new =====");
new Child();
}
}
输出:
===== 第一次 new =====
① 父类静态变量
② 父类静态代码块
③ 子类静态变量
④ 子类静态代码块
⑤ 父类实例变量
⑥ 父类实例代码块
⑦ 父类构造器
⑧ 子类实例变量
⑨ 子类实例代码块
⑩ 子类构造器
===== 第二次 new =====
⑤ 父类实例变量
⑥ 父类实例代码块
⑦ 父类构造器
⑧ 子类实例变量
⑨ 子类实例代码块
⑩ 子类构造器
第二次 new 时,①②③④ 不再出现——静态部分只在类首次加载时执行一次,实例部分每次 new 都会执行。
transient 关键字
transient 的作用很单一:在 Java 序列化时,标记某个字段不参与序列化。
public class User implements Serializable {
private String username;
private transient String password; // 不会被序列化
}
原理
Java 的默认序列化机制(ObjectOutputStream)会遍历对象的所有非 static、非 transient 字段,将它们转换为字节流。遇到 transient 标记的字段,直接跳过。反序列化时,transient 字段会被设为类型的默认值(对象类型为 null,数值类型为 0,布尔类型为 false)。
使用场景
| 场景 | 示例 |
|---|---|
| 敏感数据 | 密码、密钥、令牌 |
| 可推导字段 | 缓存的计算结果,可以从其他字段重新算出 |
| 不可序列化的字段 | 数据库连接、线程引用、Socket |
| 性能优化 | 大体积的临时数据,序列化时不需要保留 |
transient vs static
static 变量属于类而非实例,不参与对象的序列化(序列化是对象级别的操作)。所以 static transient 是多余的——static 已经保证了不被序列化。
Externalizable 的特殊情况
如果类实现的是 Externalizable 接口(而非 Serializable),序列化过程由开发者在 writeExternal() 和 readExternal() 方法中手动控制,transient 标记会失效——是否写入完全取决于你的代码。
static final:常量
static final 组合是 Java 中定义常量的标准方式:
public static final double PI = 3.141592653589793;
public static final String DEFAULT_CHARSET = "UTF-8";
static:属于类,所有实例共享final:值不可变
编译器会对 static final 的编译期常量(基本类型和 String 字面量)进行内联优化——直接把常量值替换到使用的地方,而不是通过引用去读取。这意味着:
// 编译后,其他类中的 MAX_SIZE 会被替换为字面量 1024
// 即使重新编译定义类改了值,使用方不重新编译也看不到新值
public static final int MAX_SIZE = 1024;
这是"常量折叠"的一个表现。如果常量的值是运行时才确定的(如 static final Date CREATED = new Date()),则不会被内联。
小结
| 关键字 | 核心语义 | 修饰对象 |
|---|---|---|
final |
不可变 | 变量(值不变)、方法(不可重写)、类(不可继承) |
static |
属于类 | 变量(共享)、方法(类调用)、代码块(类加载时执行)、内部类 |
transient |
不序列化 | 变量(跳过序列化) |
static final |
常量 | 编译期常量可被内联优化 |