掌握反射创建对象的初始化细节关键在于观察代码执行顺序:类加载、连接、初始化可分离;class.forname触发初始化,classloader.loadclass仅加载不初始化,initdemo.class不触发任何初始化。

要真正掌握反射创建对象时的初始化细节,关键不是背流程,而是用可观察的代码块执行顺序来验证每一步发生了什么。类加载、连接、初始化三个阶段在反射中不是自动连贯触发的,而是可以被拆开、延迟甚至跳过——这正是容易出错的地方。
看清楚:静态代码块和静态变量什么时候执行
写一个带明显副作用的测试类:
public class InitDemo {
static {
System.out.println("【静态代码块】执行");
}
static int staticField = initStatic();
static int initStatic() {
System.out.println("【静态变量赋值】执行");
return 42;
}
public InitDemo() {
System.out.println("【构造函数】执行");
}
}
然后分别用两种方式触发:
- new InitDemo() → 立即输出三行:静态代码块、静态变量赋值、构造函数
- Class.forName("InitDemo") → 输出前两行(类加载+初始化完成),但没调用构造函数
- ClassLoader.getSystemClassLoader().loadClass("InitDemo") → 无任何输出(仅加载,不初始化)
反射创建对象时,初始化时机由谁决定
Class.newInstance() 和 Constructor.newInstance() 的行为差异直接影响初始化是否发生:
- Class.newInstance():要求类已初始化,若未初始化则强制触发(等价于 Class.forName() + newInstance)
- Constructor.newInstance():只负责实例化;如果类尚未初始化,它会先触发初始化(JVM规范保证)
- getDeclaredConstructor().setAccessible(true).newInstance():即使构造函数是 private,只要类已初始化,就能绕过访问检查创建实例
注意:Java 9+ 中 Class.newInstance() 已弃用,必须用 Constructor 方式,且需显式处理 IllegalAccessException(通过 setAccessible(true))。
实战对比:三种获取 Class 对象对初始化的影响
同一类 InitDemo,以下三行代码的输出完全不同:
- InitDemo.class → 不触发任何初始化(编译期常量,类可能根本没进 JVM)
- new InitDemo().getClass() → 必然已初始化(因为 new 已触发)
- Class.forName("InitDemo") → 明确触发初始化(这是它的设计语义)
所以,如果你只想加载类而不初始化(比如插件热加载、条件加载),只能用 ClassLoader.loadClass();若后续要 newInstance,则初始化会在第一次 newInstance 调用时补上。
破坏单例的关键点就在初始化控制上
一个典型懒汉式单例:
public class Singleton {
private static Singleton instance;
static {
System.out.println("Singleton 初始化");
}
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
用反射强行调用私有构造函数时:
- 如果之前从未触发过 Singleton.class 或 Class.forName("Singleton"),那么第一次 Constructor.newInstance() 会先初始化类(执行静态块),再调用私有构造函数 → 单例被破坏
- 但如果提前用 ClassLoader.loadClass("Singleton") 加载过,再反射创建,初始化仍只发生一次(JVM 保证类初始化仅一次),但构造函数可被多次调用 → 单例仍被破坏
真正防御反射攻击,得在私有构造函数里加校验(如用 static flag 标记是否已初始化过实例)。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











