static、类加载顺序与多态是 Java 面向对象与运行时行为的核心知识。掌握它们有助于理解程序启动时发生了什么、为什么某些初始化行为会出现意外输出、以及如何写出安全且易维护的类设计。
1. 基础回顾:数据类型与内存概念
-
基本数据类型:
byte, short, int, long, float, double, boolean, char—— 存储在栈或对象内部(按上下文),值语义。 -
引用数据类型:类、接口、数组 —— 引用指向堆上的对象实例。
-
JVM 相关区(理解
static很重要):-
堆(Heap):对象实例存放处(运行时分配)。
-
栈(Stack):方法调用帧、局部变量引用、基本类型的局部拷贝(按情况)。
-
方法区 / 元空间(Method Area / Metaspace):类的元信息、静态变量、常量池等(
static成员通常归类到此处或相应区域)。 -
运行时常量池:字符串常量等可驻留于此。
-
2. static 的本质与作用
-
归属层级:
static修饰的字段/方法/代码块属于类(class-level),而不是某个实例(instance-level)。 -
共享特性:同一个类的所有实例共享同一份静态变量(所有实例引用同一内存位置)。
-
访问方式:推荐使用
类名.静态成员的形式访问静态内容,而不是通过对象实例调用(可读性与设计意图清晰)。 -
静态方法的限制:
-
不能使用
this,因为无“当前实例”语义。 -
不能直接访问非静态成员(因为在类被加载但未实例化时,这些实例成员可能不存在)。
-
-
静态代码块(static{}):
-
在类初始化阶段执行(类被首次主动使用时),且先于任何实例构造。
-
可用于复杂静态资源初始化(比如初始化静态集合、读取配置等)。
-
3. static 与 final 的差别与联用要点
-
static:表示“属于类”。 -
final:表示不可变
修饰常量
规则:被 final 修饰的常量必须赋值,且只能赋值一次,后续无法修改。示例:final int MAX_AGE = 120;(一旦赋值,MAX_AGE 的值就固定不变)。
修饰方法
规则:被 final 修饰的方法不能被子类重写。
修饰类
规则:被 final 修饰的类不能被继承。
典型案例:String 类就是 final 修饰的类(如代码示例中 public final class String),这意味着任何类都无法继承 String 来扩展其功能。
作用:用于锁定方法逻辑,确保方法行为在继承体系中保持稳定。
-
若是编译时常量(例如static final(常量):static final int MAX = 10;或static final String S = "X";),编译器可能把它内联到使用处(调用方的字节码含值),这会导致常量值改变后依赖方未重新编译仍旧使用旧值的情况。
不可变对象也推荐使用 static final(如 static final List<String> LIST = Collections.unmodifiableList(...)),但注意不可让该引用指向可变内部状态被外部修改。
4. 类加载与初始化顺序(必须掌握的执行流程)
-
类加载(Loading):JVM 找到类二进制数据并放入方法区(或元空间)。
-
连接(Linking):
-
验证、准备(Preparation)(为静态变量分配默认值)、解析(Resolution)(符号引用变为直接引用)。
-
-
初始化(Initialization):
-
执行静态变量的显式赋值与
static代码块,按源代码中出现的顺序执行。 -
这是执行类显式静态初始化的阶段(发生在类的首次主动使用)。
-
-
实例创建时:
-
分配对象内存(堆),设置默认值;
-
执行实例字段的显式赋值与实例初始化块(按顺序);
-
执行构造函数体。
-
继承关系中的顺序:
-
加载/初始化父类 → 父类的静态初始化完成 → 子类的静态初始化 → 创建对象时先父类的实例初始化与构造,再子类的实例初始化与构造。
换言之:static(类级)先于实例构造,且父类static先于子类static。
5. 典型示例(演示输出顺序)
class Animal {
static {
System.out.println("动物会跑");
}
void eat() {
System.out.println("动物在吃饭");
}
}
class Dog extends Animal {
static {
System.out.println("狗会跑");
}
void bark() {
System.out.println("狗在叫");
}
}
public class Demo {
public static void main(String[] args) {
System.out.println("程序开始执行");
// 创建Dog对象(触发类加载 + 构造过程)
Dog d = new Dog();
// 调用方法
d.eat();
d.bark();
System.out.println("程序结束执行");
}
}
输出顺序:
动物会跑
狗会跑
程序开始执行
动物在吃饭
狗在叫
程序结束执行
说明:
先加载类(static先执行)
-
程序运行时,JVM 会先加载
Animal类 → 输出 “动物会跑”。 -
接着加载
Dog类 → 输出 “狗会跑”。
(静态代码块在类加载时执行,只执行一次)
执行 main 方法
-
输出 “程序开始执行”。
创建对象并调用方法
-
创建
Dog对象(此时构造方法无输出)。 -
调用
eat()→ 输出 “动物在吃饭”。 -
调用
bark()→ 输出 “狗在叫”。
程序结束
-
输出 “程序结束执行”
6. 多态
-
定义:父类引用可以指向子类对象(
Parent p = new Child();),方法调用根据对象的实际类型(运行时)进行动态绑定(即调用子类重写的方法)。 -
限制:通过父类引用只能调用父类声明的方法;若要访问子类特有方法需向下转型。
-
内存视角:堆上的对象含有子类与父类的字段(对象只有一份),方法区保存类方法信息,运行时根据对象实际类型查找方法表进行调用。
7. 常见坑与最佳实践
-
避免可变的
static状态:全局可变静态变量会带来隐藏依赖、并发问题与测试困难。 -
初始化顺序陷阱:静态变量依赖其他静态变量(尤其跨类)可能因加载顺序产生
null或默认值问题。尽量避免复杂的静态互相依赖。 -
static final字符串/基本类型常量的内联:变更常量值后,记得重新编译所有使用该常量的模块。
16

被折叠的 条评论
为什么被折叠?



