S02-05 面向对象-代码块
[TOC]
代码块
在 Java 中,代码块 (Code Block) 指的是用一对花括号 {} 包围起来的代码片段。
根据位置、修饰符的不同,代码块主要分为四种。它们在 Java 面试和实际开发中(尤其是涉及到对象初始化顺序时)非常重要。
基本语法
[修饰符] {
// 逻辑语句
};- 修饰符:
static,可选,按照是否有 static 修饰符分为两类:- 普通代码块:没有 static 修饰
- 静态代码块:有 static 修饰
- 逻辑语句:可以是任意逻辑语句,包括输入、输出、方法调用、循环、判断等。
;:尾部的分号,可以省略。
代码块分类
| 类型 | 关键字 | 位置 | 执行次数 | 执行时机 | 主要用途 |
|---|---|---|---|---|---|
| 普通代码块 | 无 | 方法内 | 随方法调用 | 方法被调用且运行到该位置时 | 限制变量作用域 |
| 构造代码块 | 无 | 类中 | 每次 new | 创建对象时,构造方法之前 | 抽取构造方法共性逻辑 |
| 静态代码块 | static | 类中 | 仅 1 次 | 类加载时 | 初始化静态资源 |
| 同步代码块 | synchronized | 方法内 | 随调用 | 获取锁成功时 | 线程安全控制 |
普通代码块
好的,我们来深入探讨 Java 中的普通代码块(通常也被称为局部代码块)。
虽然在日常开发中,我们最常讨论的是静态代码块或构造代码块,但普通代码块作为最基础的代码组织形式,有着它特定的应用场景和规则。
概述
普通代码块(Local Block,局部代码块) 是指直接在方法内部、构造器内部,或者在控制语句(如 if、for、while)内部使用一对大括号 {} 括起来的代码段。
- 位置:方法体内部。
- 执行时机:遵循代码的顺序执行原则,当程序的控制流从上往下运行到该代码块时,就会执行。
应用场景
普通代码块最核心的作用只有一个:限制局部变量的生命周期和作用域。
A. 控制变量作用域,防止污染:
在
{}内声明的变量,被称为局部变量。一旦代码执行跳出这个{},这些变量就会立刻失效(出栈),不能再被外部访问。这使得我们可以非常精确地控制变量的存活范围。代码示例:
javapublic class LocalBlockDemo { public void processData() { System.out.println("方法开始"); // 1. 普通代码块 { int tempId = 1001; String tempData = "临时数据"; System.out.println("处理临时数据: " + tempId + " - " + tempData); } // 2. tempId 和 tempData 在这里生命周期结束 // System.out.println(tempId); // 3. 这里会编译报错:找不到符号 System.out.println("方法结束"); } }B. 避免同一方法内的变量名冲突:
如果一个方法非常长(虽然不推荐写长方法),你可能需要在不同的阶段使用相同含义的变量名。使用普通代码块可以将它们隔离开来。
代码示例:
javapublic void calculate() { // 阶段 1:计算 A { int result = 10 * 20; System.out.println("阶段1结果:" + result); } // 阶段 2:计算 B (可以毫无顾忌地再次使用 result 作为变量名) { int result = 50 + 60; System.out.println("阶段2结果:" + result); } }C. 内存优化(历史原因):
在早期的 Java 虚拟机(JVM)中,及早结束无用变量的生命周期,可以让这部分内存(主要是栈帧中的局部变量表槽位)尽早被复用,或者让其中引用的堆内存对象尽早符合垃圾回收(GC)的条件。
- 现实情况:现代 JVM 的 JIT(即时编译器)已经非常智能,能够进行极佳的逃逸分析和内存优化。因此,单纯为了“省内存”而在日常业务代码中刻意使用普通代码块的情况已经很少见了。
变量遮蔽
需要注意的“坑”:变量遮蔽(Shadowing)规则:
在 Java 中,普通代码块不能重新定义与其外部作用域中已经存在的同名变量。这与 C/C++ 等语言不同。
错误示例:
public void testShadowing() {
int x = 10;
{
// int x = 20; // 编译报错:已在方法 testShadowing() 中定义了变量 x
int y = 20; // 这是允许的
}
}现代开发中的实际应用建议
现代开发中的实际应用建议:
在现代 Java 开发规范(如《阿里巴巴 Java 开发手册》)和 Clean Code(整洁代码)思想中,不建议在一个方法中大量使用普通的局部代码块。
- 替代方案:如果你发现需要用代码块来隔离一大段逻辑和变量,这通常是一个“代码坏味道”(Code Smell),暗示你的方法太长了。更好的做法是将这个局部代码块提取成一个独立的私有方法(Extract Method)。这样不仅作用域被隔离了,代码的可读性和可重用性也会大幅提升。
普通代码块的逻辑相对直白,它本质上就是对作用域边界的一次声明。
构造代码块@
我们继续深入探讨 Java 中非常有特色的构造代码块(在官方文档中常被称为实例初始化块 )。
相比于普通代码块主要用于限制变量作用域,构造代码块的核心舞台在于对象的初始化阶段。
概述
构造代码块(Instance Initialization Block,实例初始化块) 是直接定义在类内部、和成员方法和属性平级、没有任何修饰符(特别是没有 static 关键字)的一对大括号 {}。
- 位置:类中,方法之外,和成员方法和属性平级。
- 执行时机:每次使用
new关键字创建类的实例(对象)时都会执行。它必定优先于类的构造方法执行。
应用场景
A. 提取公共初始化逻辑(代码复用):
如果你的类有多个重载的构造方法,并且这些构造方法内部有一段完全相同的初始化代码(例如:验证参数、记录日志、初始化一些复杂的非静态成员变量),你可以把这段重复代码提取到构造代码块中,从而避免代码冗余。
代码示例:
javapublic class User { private String name; private int age; // 构造代码块:提取公共逻辑 { System.out.println("[系统日志] 正在初始化新的 User 对象,分配唯一标识..."); // 假设这里有一段复杂的公共校验或初始化逻辑 } public User() { System.out.println("-> 执行无参构造方法"); } public User(String name, int age) { this.name = name; this.age = age; System.out.println("-> 执行有参构造方法"); } }当调用
new User()或new User("Alice", 25)时,控制台都会首先打印[系统日志]...,然后再打印各自构造方法的专属输出。B. 匿名内部类的初始化:
匿名内部类没有类名,因此无法定义显式的构造方法。如果你需要在创建匿名内部类实例时进行初始化操作(比如给集合填充初始数据),构造代码块是你唯一的选择。这也是一种非常经典的用法(常被称为双大括号初始化)。
代码示例:
javaList<String> list = new ArrayList<String>() { // 这是一个匿名内部类中的构造代码块 { add("Apple"); add("Banana"); add("Orange"); } };
底层原理
底层原理:编译器施展的“小魔法”:
为什么构造代码块会优先于构造方法执行?这其实是 Java 编译器(javac)在幕后做的工作。
在编译阶段,编译器会将所有构造代码块中的代码,按顺序原封不动地复制到每一个构造方法的开始处(确切地说,是插入到 super() 调用之后,也就是父类初始化完成后)。
因此,在 JVM 真正运行的时候,构造代码块的内容本质上已经成为了构造方法的一部分,并且排在构造方法自身逻辑的前面。
规则细节
需要注意的细节规则:
- 执行次数:每创建一个对象,就执行一次。创建 100 个对象,它就执行 100 次。这与只在类加载时执行一次的静态代码块形成了鲜明对比。
- 多个代码块的顺序:如果一个类中有多个构造代码块,它们会按照在源文件中从上到下的顺序依次执行。
- 与成员变量的优先级:构造代码块和非静态成员变量的显式赋值是平级的。它们严格按照在代码中出现的先后顺序执行。
顺序示例:
public class OrderDemo {
// 1. 成员变量显式赋值
private int a = 10;
// 2. 构造代码块 (因为写在变量后面,所以后执行)
{
System.out.println("构造代码块执行,此时 a = " + a);
a = 20;
}
// 3. 构造方法 (最后执行)
public OrderDemo() {
System.out.println("构造方法执行,此时 a = " + a);
a = 30;
}
}
// 最终对象的 a 属性值是 30。构造代码块虽然在日常 CRUD(增删改查)业务中不如静态代码块常见,但理解它的底层机制对于阅读优秀的开源框架源码极其重要。
静态代码块@
在 Java 中,静态代码块是使用频率非常高、也是面试中最常被考查的代码块之一。它与类的加载机制息息相关,扮演着“全局初始化”的重要角色。
概述
静态代码块(Static Code Block) 是定义在类中、方法之外,并且由 static 关键字修饰的一段被 {} 包裹的代码。
- 位置:类内部,且必须带有
static关键字。 - 语法:
static { ... }
核心特征与执行时机
核心特征与执行时机:
理解静态代码块的关键在于“类加载”这三个字。
- 执行时机:运行时的类加载阶段执行,当类第一次被 Java 虚拟机(JVM)加载到内存中,并进行初始化(Initialization)阶段时,静态代码块就会被执行。它绝对优先于任何对象的创建(甚至优先于构造代码块和普通方法)。
- 执行次数:在整个 JVM 的生命周期内,一个类通常只会被加载一次,因此静态代码块只执行一次,无论你之后
new了多少个该类的对象。
类加载时机@
主动引用
要理解类什么时候被加载,首先需要明确 JVM 的一个核心机制:懒加载(Lazy Loading)。JVM 不会在启动时把所有相关的 .class 文件一股脑儿全塞进内存,而是“按需加载”。
在 Java 虚拟机规范中,严格规定了只有在发生“主动引用(Active Use)”时,才会触发类的加载和初始化(在这之前,必须完成加载、验证、准备阶段)。
类在什么时候会被初始化:
创建对象实例时:执行
new创建一个对象实例时类会被加载,并且只会加载一次。javaclass Student { static { System.out.println("【时机1】Student 类被加载并初始化了!"); } } public class TriggerDemo1 { public static void main(String[] args) { System.out.println("准备执行 new 操作..."); Student stu = new Student(); // 触发加载 } }创建子类对象实例时,父类也会被加载
javaclass Animal { static { System.out.println("【时机5 - 父类】Animal 类被加载并初始化了!"); } } class Dog extends Animal { static { System.out.println("【时机5 - 子类】Dog 类被加载并初始化了!"); } } public class TriggerDemo5 { public static void main(String[] args) { System.out.println("准备实例化子类..."); Dog dog = new Dog(); // 这里会先触发 Animal 加载,再触发 Dog 加载 } }使用类的非 final 静态成员时
java// 静态属性 class Config { public static int timeout = 5000; static { System.out.println("【时机2】Config 类被加载并初始化了!"); } } public class TriggerDemo2 { public static void main(String[] args) { System.out.println("准备读取静态变量..."); int t = Config.timeout; // 访问静态属性,触发加载 } }java// 静态方法 class MathUtils { static { System.out.println("【时机3】MathUtils 类被加载并初始化了!"); } public static void calculate() { System.out.println("执行计算..."); } } public class TriggerDemo3 { public static void main(String[] args) { System.out.println("准备调用静态方法..."); MathUtils.calculate(); // 访问静态方法,触发加载 } }使用反射机制(
Class.forName())时当使用
java.lang.reflect包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发其初始化。这在加载数据库驱动等场景极其常见。javaclass DatabaseDriver { static { System.out.println("【时机4】DatabaseDriver 类被反射机制加载并初始化了!"); } } public class TriggerDemo4 { public static void main(String[] args) throws ClassNotFoundException { System.out.println("准备使用反射加载类..."); Class<?> clazz = Class.forName("DatabaseDriver"); // 使用反射机制,触发加载 } }JVM 启动时标明的启动类 (包含 main 方法的类)
当虚拟机启动时,用户需要指定一个要执行的主类(包含
public static void main(String[] args)的那个类),虚拟机会先初始化这个主类。注意:在 JDK 8 之后,如果一个接口定义了
default默认方法,当它的实现类被初始化时,该接口也会在实现类之前被初始化。这也是一个较为边缘但真实存在的触发时机。javapublic class MainApp { static { System.out.println("【时机6】包含 main 方法的主类 MainApp 在启动时被最先加载!"); } public static void main(String[] args) { System.out.println("main 方法开始执行..."); } } // 运行该类,控制台会先打印静态代码块的内容,再打印 main 方法的内容。
被动引用
被动引用:在 Java 虚拟机规范中明确规定:所有针对类的引用,如果不是“主动引用”,统统被称为“被动引用”。 被动引用最核心的特征就是:它不会触发类的初始化(即不会执行类的静态代码块和静态变量赋值)。
下面为您详细拆解面试中最常考察的 3 种经典的“被动引用”场景,并附上代码验证:
通过子类引用父类的静态字段:
当通过子类来访问父类中定义的静态字段时,只有真正声明这个字段的类(也就是父类)才会被初始化,而子类不会被初始化。
- 原理解析:对于静态字段,JVM 认为谁定义了它,就初始化谁。子类只是充当了一个“调用通道”的角色。
javaclass SuperClass { static int value = 123; static { System.out.println("【父类】SuperClass 被初始化了!"); // 有被打印 } } class SubClass extends SuperClass { static { System.out.println("【子类】SubClass 被初始化了!"); // 没有被打印 } } public class PassiveRefDemo1 { public static void main(String[] args) { System.out.println("准备通过子类访问父类的静态变量..."); // 这里的调用看似用到了 SubClass,但实际上访问的是 SuperClass 的变量 System.out.println(SubClass.value); } }通过数组定义来引用类:
当你声明一个某个类的数组时,并不会触发该类的初始化。
- 原理解析:当你执行
new SuperClass[10]时,JVM 并没有去实例化 10 个SuperClass对象,而是由 JVM 动态生成了一个专门用来装载这些引用的新类型(类似于[LSuperClass;的数组类),并在内存中开辟了一块连续的空间用来存放 10 个null引用。因此,真正的SuperClass并没有被触碰。
javaclass SuperClass { static { System.out.println("【父类】SuperClass 被初始化了!"); // 没有被打印 } } public class PassiveRefDemo2 { public static void main(String[] args) { System.out.println("准备定义 SuperClass 的数组..."); // 仅仅是分配了数组空间,并没有实例化具体的对象 SuperClass[] arr = new SuperClass[10]; System.out.println("数组定义完毕,长度为: " + arr.length); } }- 原理解析:当你执行
引用类的常量 (
static final):当一个类访问另一个类中被
static final修饰、且在编译期就能确定其值的常量时,不会触发定义该常量的类的初始化。- 原理解析(极其重要,常考考点):这在底层叫做常量传播优化。在 Java 代码编译成
.class字节码文件的阶段,编译器发现这个常量的值是固定不变的(比如字符串"hello"或数字100),就会直接把这个常量的值,复制一份存放到调用类(例子中的PassiveRefDemo3)自己的常量池中。 - 这就意味着,在 JVM 真正运行的时候,调用类根本不需要再去联系那个定义常量的类了,两者在运行期彻底“解绑”。
javaclass ConstClass { // static final 修饰的编译期常量 public static final String HELLO_WORLD = "Hello World!"; static { System.out.println("【常量类】ConstClass 被初始化了!"); // 没有被打印 } } public class PassiveRefDemo3 { public static void main(String[] args) { System.out.println("准备访问 ConstClass 的常量..."); // 实际上这个字符串在编译期就已经放进 PassiveRefDemo3 的常量池了 System.out.println(ConstClass.HELLO_WORLD); } }- 原理解析(极其重要,常考考点):这在底层叫做常量传播优化。在 Java 代码编译成
总结提示:
严格来说,“被动引用”并不代表该类在 JVM 中连“加载(Loading)”阶段都没有经历。在某些虚拟机的具体实现中,被动引用可能会触发类的“加载”,但它绝对不会触发类的“初始化(Initialization)”(也就是不会执行静态代码块和静态赋值操作)。在日常开发排查问题时,我们主要关注的就是它没有执行初始化逻辑。
应用场景
典型应用场景:
静态代码块的核心作用是初始化类的静态成员(静态变量),或者执行一些只需要在系统启动时执行一次的准备工作。
A. 复杂的静态变量初始化:
有时候,给静态变量赋初始值不是一句简单的等式就能完成的,可能需要逻辑判断、异常处理或者多步计算。静态代码块提供了执行这些复杂逻辑的空间。
代码示例(初始化静态的 Map 集合):
javaimport java.util.HashMap; import java.util.Map; public class ConfigManager { public static Map<String, String> settings; // 静态代码块:用于执行复杂的静态初始化逻辑 static { System.out.println("-> 正在加载系统全局配置..."); settings = new HashMap<>(); settings.put("timeout", "3000"); settings.put("max_retries", "3"); // 假设这里还包含从配置文件读取数据的逻辑,可能会抛出异常 } }B. 加载底层驱动或本地库:
在很多经典的 Java 框架中(如 JDBC),通常会在静态代码块中注册驱动,或者使用
System.loadLibrary()加载 C/C++ 编写的底层动态链接库。代码示例(模拟 JDBC 驱动加载):
javapublic class MyDatabaseDriver { // 静态代码块:类加载时自动将自己注册到驱动管理器中 static { System.out.println("-> 注册数据库驱动..."); // DriverManager.registerDriver(new MyDatabaseDriver()); } }提示:当你执行
Class.forName("MyDatabaseDriver")时,即使没有创建对象,也会触发类的加载,从而执行这段静态代码块。
关键访问限制@
因为静态代码块在类加载时就执行了,而那时候对象根本还没创建出来,所以它有一些严格的限制(静态的限制):
只能访问静态成员:静态代码块内部只能直接访问该类的其他静态变量和静态方法。
绝对不能访问实例成员:不能访问非静态的变量和非静态的方法。
不能使用
this和super关键字:因为这两个关键字都指向具体的对象实例,而类加载时实例还不存在。
错误示例:
public class ErrorDemo {
// 属性
private static int staticVar = 20;
private int instanceVar = 10;
// 方法
private static void staticMethod() { System.out.println("静态方法") }
private void instanceMethod() { System.out.println("普通方法") }
// 静态代码块
static {
System.out.println(staticVar); // ✅ 正确:访问静态属性
System.out.println(staticMethod); // ✅ 正确:访问静态方法
// System.out.println(instanceVar); // ❌ 编译报错:无法从静态上下文中引用非静态变量
// System.out.println(instanceMethod); // ❌ 编译报错:无法从静态上下文中引用非静态方法
// System.out.println(this.staticVar); // ❌ 编译报错:无法从静态上下文中引用 'this'
}
}执行顺序
执行顺序(与静态变量):
如果一个类中有多个静态变量显式赋值和多个静态代码块,它们是平级的,JVM 会严格按照它们在源代码中从上到下的顺序依次执行。
public class StaticOrderDemo {
// 1. 静态变量显式赋值
public static int value = 1;
// 2. 静态代码块
static {
System.out.println("执行静态代码块,此时 value = " + value);
value = 2; // 修改静态变量的值
}
public static void main(String[] args) {
System.out.println("main方法执行,最终 value = " + value);
}
}
// 输出:
// 执行静态代码块,此时 value = 1
// main方法执行,最终 value = 2同步代码块
我们终于来到了 Java 代码块中最具挑战性、也是并发编程中最核心的部分:同步代码块(Synchronized Block)。
如果说前面三种代码块(局部、构造、静态)是为了组织代码和初始化数据,那么同步代码块完全是为了解决“多线程安全问题”而生的。
概述
在多线程环境下,当多个线程同时访问并修改同一个共享资源(比如同一个变量、同一个文件)时,极易引发数据混乱(竞态条件)。为了保证数据的正确性,Java 提供了 synchronized 关键字。
同步代码块(Synchronized Block) 就是被 synchronized 关键字和一对大括号 {} 包裹起来的代码段。
- 位置:定义在方法内部。
- 核心思想:排队机制。它确保在同一时刻,最多只能有一个线程能够进入这部分代码执行。其他试图进入的线程必须在外面阻塞等待,直到当前线程执行完毕并释放“锁”。
基本语法与锁对象
基本语法:
同步代码块的语法比其他代码块多了一个必须要提供的参数:锁对象。
synchronized (锁对象) {
// 需要被同步的代码(临界区)
// 通常是操作共享数据的代码
}锁对象(Monitor):
在 Java 中,任何一个对象都可以作为这把“锁”(底层称为 Monitor 对象)。当线程执行到 synchronized 时,它会去检查这个对象有没有被别的线程“锁住”。
- 如果没有,它就拿走锁,进入代码块执行。
- 如果有,它就在代码块外面排队等待(阻塞状态)。
常用锁对象
常用的三种“锁”对象:
根据保护的资源不同,我们通常会选择不同的锁对象:
this
A. 使用 :this 作为锁(当前对象锁)
如果多个线程共享的是同一个实例对象,最常见的就是用 this。
public class BankAccount {
private int balance = 1000;
public void withdraw(int amount) {
// ... 一些不需要同步的准备工作 ...
synchronized (this) { // 锁定当前 BankAccount 实例
if (balance >= amount) {
balance -= amount;
System.out.println(Thread.currentThread().getName() + " 取款成功,余额: " + balance);
} else {
System.out.println("余额不足");
}
}
}
}自定义对象(推荐)
B. 使用自定义对象作为锁(细粒度锁):
这是最推荐的做法。如果一个类里有两段完全不相干的同步逻辑(比如一个是修改账户 A,一个是修改账户 B),用 this 会导致它们互相阻塞。这时应该专门创建一个对象充当锁,强烈建议使用 final 修饰,防止锁被中途替换。
public class TicketSystem {
private int tickets = 100;
// 1. 专门创建一个对象充当锁,强烈建议使用 final 修饰,防止锁被中途替换!
private final Object lock = new Object();
public void sellTicket() {
// 2. 只锁定特定的业务逻辑
synchronized (lock) {
if (tickets > 0) {
tickets--;
System.out.println("卖出一张票,剩余: " + tickets);
}
}
}
}类名.class
C. 使用 :类名.class 作为锁(全局/类锁)
如果我们要保护的共享资源是静态变量(属于类的,被所有对象共享),那么用 this 或普通的实例对象是锁不住的,必须使用该类的 Class 对象。
public class GlobalCounter {
// 1. 共享资源是静态变量
private static int globalCount = 0;
public static void increment() {
// 2. 锁定整个类
synchronized (GlobalCounter.class) {
globalCount++;
}
}
}对比同步方法
为什么要用同步代码块(对比同步方法):
除了同步代码块,Java 还可以直接在方法签名上加 synchronized(即同步方法)。为什么还要用同步代码块呢?
- 更细的控制粒度(性能更好):如果一个方法有 100 行代码,但只有其中 5 行涉及修改共享数据。如果使用同步方法,整个 100 行代码都会被锁住,其他线程等待时间过长;如果使用同步代码块,只锁那 5 行,剩下的 95 行代码各个线程依然可以并发执行,大大提升了系统吞吐量。
注意事项
注意事项:
锁对象不能为
null:如果传入synchronized (null),运行时会直接抛出NullPointerException。避免使用
String常量和基本类型包装类作为锁:因为由于 JVM 字符串常量池和包装类缓存(如 Integer 的 -128 到 127)的存在,你以为不同的锁,可能在底层是同一个对象,从而引发莫名其妙的死锁或阻塞。永远优先使用new Object()。
代码块执行顺序
这是一个非常经典且高频的 Java 面试题!理解了这个执行顺序,就等于彻底打通了 Java 类加载机制和对象生命周期的“任督二脉”。
当存在父子类继承关系时,代码块和构造方法的执行顺序遵循一个核心原则:先静态后实例,先父类后子类。
我们可以把整个过程分为两个阶段:类加载阶段(只发生一次)和对象创建阶段(每次 new 都会发生)。
核心执行顺序
核心执行顺序(黄金法则):
当你第一次创建子类对象(例如执行 new Child())时,完整的执行顺序如下:
父类静态代码块和静态属性(父类加载时执行。优先级一样时,按定义顺序执行)
子类静态代码块和静态属性(子类加载时执行。优先级一样时,按定义顺序执行)
父类构造代码块和非静态属性(准备初始化父类空间。优先级一样时,按定义顺序执行)
父类构造方法(完成父类初始化)
子类构造代码块和非静态属性(准备初始化子类空间。优先级一样时,按定义顺序执行)
子类构造方法(完成子类初始化)
重点提示:如果你紧接着创建第二个子类对象,步骤 1 和 2(静态代码块)将不再执行,只会依次执行步骤 3、4、5、6。
经典代码验证:
空谈不如看代码,我们用一段完整的代码来印证这个法则:
// 父类
class Parent {
static { System.out.println("1. 父类 - 静态代码块"); }
{ System.out.println("3. 父类 - 构造代码块"); }
public Parent() {
System.out.println("4. 父类 - 无参构造方法");
}
}
// 子类继承父类
class Child extends Parent {
static { System.out.println("2. 子类 - 静态代码块"); }
{ System.out.println("5. 子类 - 构造代码块"); }
public Child() {
// 这里隐藏了一个 super();
System.out.println("6. 子类 - 无参构造方法");
}
}
// 测试类
public class InitializationOrderDemo {
public static void main(String[] args) {
System.out.println("--- 第一次实例化子类 ---");
new Child();
System.out.println("\n--- 第二次实例化子类 ---");
new Child();
}
}控制台输出结果:
--- 第一次实例化子类 ---
1. 父类 - 静态代码块
2. 子类 - 静态代码块
3. 父类 - 构造代码块
4. 父类 - 无参构造方法
5. 子类 - 构造代码块
6. 子类 - 无参构造方法
--- 第二次实例化子类 ---
3. 父类 - 构造代码块
4. 父类 - 无参构造方法
5. 子类 - 构造代码块
6. 子类 - 无参构造方法底层原理解析
为什么是这个顺序(底层原理解析):
要彻底记住这个顺序,死记硬背是不够的,理解其背后的底层逻辑会让你茅塞顿开:
为什么静态最先执行?且父类优先子类?
JVM 在遇到
new Child()时,会先去方法区找Child类的元数据。如果没加载,就会触发类加载。因为Child继承自Parent,JVM 规定加载子类前必须先加载其父类。类加载阶段会执行static代码块,所以父类静态块先于子类静态块执行。加载完成后,静态部分就“固化”在内存中了,以后再也不用管了。为什么父类的构造代码块/构造方法,先于子类执行?
子类的构造方法中,第一行永远隐藏着一句隐式的
super();(除非你显式调用了带参的super(...))。这保证了在初始化子类特有属性之前,父类的状态必须先被完全初始化。为什么构造代码块先于构造方法?
正如我们之前聊过的,Java 编译器在编译时,会把“构造代码块”里的代码全部复制,悄悄塞进所有构造方法的最前面(紧跟在
super()之后)。所以它自然就在构造方法的主体逻辑之前执行了。
class Father {}
class Child {
{ // 构造代码块 }
public Child() {
// super(); // 1. 构造器最开始隐式包含 super(),指向父类引用
// 2. 编译时,会将构造代码块中的代码复制到此处
// 3. 其他代码逻辑
}
}