编译期仅做语法和类型检查,不执行代码;运行期才加载类、分配内存、触发初始化;编译期常量需满足final+字面量+基本类型或String;反射和动态代理绕过编译检查。

编译期只做语法和类型检查,不执行任何代码
Java源文件(.java)被javac处理时,编译器只验证语法规则、符号存在性、泛型擦除后类型兼容性等。它不会调用main方法,也不会初始化静态变量或执行static块——哪怕里面写了System.out.println("hello"),编译期也完全无视。
常见误判场景:
- 把
NullPointerException当成编译错误(实际是运行期才抛) - 以为
final int x = getValue();中getValue()会在编译期求值(不会,除非是常量表达式) - 对
String s = "a" + "b";能优化成"ab"有印象,但误以为所有字符串拼接都这样(仅限编译期可知的字面量)
运行期才真正加载类、分配内存、触发初始化
当java命令启动JVM后,类加载器按需读取.class字节码,链接(验证、准备、解析),再执行(静态初始化块和静态字段赋值)。此时new对象、调用方法、访问数组下标越界等行为才会真实发生。
关键差异点:
立即学习“Java免费学习笔记(深入)”;
-
Class.forName("X")会触发初始化;ClassLoader.loadClass("X")默认不触发 -
int[] arr = new int[10];在运行期才向堆申请内存,编译期只检查语法合法 - 泛型信息在运行期已擦除,
List和List在JVM里都是List
编译期常量和运行期常量容易混淆
只有满足“编译期可知+基本类型或String+用final修饰+直接赋字面量值”的变量,才是编译期常量。例如:
final int A = 10; // ✅ 编译期常量 final String B = "hello"; // ✅ 编译期常量 final int C = System.currentTimeMillis(); // ❌ 运行期才确定 final int D = getConst(); // ❌ 方法调用无法在编译期求值
这种区别直接影响:
- 是否参与
switch语句的case分支(必须是编译期常量) - 是否被内联到调用处(如
public static final int MAX = 100;会被替换成100) - 是否允许在注解中作为属性值(同样要求编译期常量)
反射和动态代理彻底打破编译期约束
编译期检查不到的类型关系,在运行期可能通过反射绕过。比如:
Object obj = "hello";
Method m = obj.getClass().getMethod("length");
int len = (int) m.invoke(obj); // 编译期不知道obj有length(),但运行期可以调
这类操作代价高,且绕过了编译器保护:
- 方法名写错 → 运行时报
NoSuchMethodException - 参数类型不匹配 → 运行时报
IllegalArgumentException - 私有成员访问未设
setAccessible(true)→ 运行时报IllegalAccessException
动态代理生成的类(如Proxy.newProxyInstance)也是运行期才生成字节码,编译期根本不存在对应类文件。
真正容易被忽略的是:编译期检查只是第一道防线,而JVM规范定义的“运行期行为”(如内存模型、指令重排、类初始化顺序)才是决定程序是否正确的底层依据。写对了语法,不代表跑起来就对。









