代码块
本教程共 100 篇 · 第 38 篇 · 更新于 2026-08-05 · 约 13 分钟阅读
38. 代码块
本节目标:认识静态代码块、实例代码块、局部代码块三种代码块,弄清它们的执行时机和先后顺序,避免把初始化逻辑放错地方。
代码块就是一对花括号 {} 包起来的一段代码。按出现的位置,分三种:局部代码块、实例代码块、静态代码块,执行时机各不相同。
搞不清顺序会出什么事?变量还没初始化就被用了,程序抛空指针,你却盯着代码看半天想不通——明明赋值语句写在那儿。
局部代码块:限定变量范围
写在方法里的普通 {},用来缩小变量作用域。
public class Local {
public static void main(String[] args) {
{
int temp = 10; // 只在这个块里有效
System.out.println(temp);
}
// System.out.println(temp); // 放开这行会编译报错
int temp = 20; // 名字可以重用,因为上面那个已经失效
System.out.println(temp);
}
}
```java
放开注释那行,编译器会说:
```text
Local.java:8: error: cannot find symbol
System.out.println(temp);
^
symbol: variable temp
location: class Local
如今 JVM 会自己优化,局部代码块的「省内存」意义不大。主要价值是让变量名早点失效,避免长方法里命名冲突。
不过更好的办法是把长方法拆短。需要靠局部代码块隔离变量,通常说明这个方法该拆了。
实例代码块:每次 new 都跑
写在类里、方法外的 {},叫实例代码块(也叫构造代码块)。每次创建对象时,它都在构造方法体之前执行。
public class Demo {
{
System.out.println("实例代码块"); // 每次 new 都先跑
}
Demo() {
System.out.println("无参构造");
}
Demo(int x) {
System.out.println("带参构造");
}
}
```bash
不管调哪个构造方法,实例代码块都先执行。多个实例代码块按书写顺序依次跑。
它的本质是编译器帮你做的「代码搬运」:把实例代码块的内容复制到每个构造方法的开头。所以它相当于所有构造方法的公共前置逻辑。
> [!NOTE]
> 实例代码块在现代代码里用得不多。有公共初始化逻辑时,更常见的是抽一个私有 `init()` 方法,或者用 `this(...)` 串联构造。实例代码块的问题是位置隐蔽,读代码时容易漏看。
它真正有用的场合是**匿名内部类**——匿名类没法定义构造方法,只能靠实例代码块做初始化。
## 静态代码块:类加载时跑一次
`static {}` 在类第一次被加载时执行,且只执行一次。
```java
public class Config {
static String path;
static {
path = "config/default.properties";
System.out.println("静态代码块执行");
}
}
它的执行比实例代码块和构造方法都早,而且不管 new 几次对象只跑一遍。
多个静态代码块和静态变量的赋值语句,按书写顺序从上往下执行。这一点有个陷阱:
public class Trap {
static {
value = 10; // 合法:可以给下面声明的静态变量赋值
// System.out.println(value); // 编译错误:illegal forward reference
}
static int value = 20; // 后声明,会覆盖上面的 10
}
```bash
赋值允许,读取不允许。而且后面的显式赋值会盖掉前面静态块里的值,最终 `value` 是 20。所以静态块尽量写在静态变量声明之后。
## 完整执行顺序演示
把三种代码块和构造方法放一起,跑一遍看真实顺序:
```java
public class OrderDemo {
static {
System.out.println("1 静态代码块");
}
{
System.out.println("2 实例代码块");
}
OrderDemo() {
System.out.println("3 构造方法");
{
System.out.println("4 局部代码块");
}
}
public static void main(String[] args) {
System.out.println("--- 第一次 new ---");
new OrderDemo();
System.out.println("--- 第二次 new ---");
new OrderDemo();
}
}
编译运行:
javac OrderDemo.java
java OrderDemo
```java
输出:
```text
1 静态代码块
--- 第一次 new ---
2 实例代码块
3 构造方法
4 局部代码块
--- 第二次 new ---
2 实例代码块
3 构造方法
4 局部代码块
三个关键点,逐条对照输出看:
- 「1 静态代码块」跑在 main 方法的第一行打印之前。因为
java OrderDemo要执行 main,必须先加载OrderDemo这个类。 - 第二次 new 时,静态块没有再打印。它这辈子只跑这一次。
- 实例代码块和构造方法每次 new 都跑,而且实例块永远在前。
顺序速查表
| 执行时机 | 代码块类型 | 执行次数 |
|---|---|---|
| 类首次加载 | 静态代码块 / 静态变量赋值 | 全程仅一次 |
每次 new | 实例代码块 / 实例变量赋值 | 每个对象一次 |
每次 new | 构造方法体 | 每个对象一次 |
| 方法执行到该处 | 局部代码块 | 每次执行都跑 |
有继承时的顺序
一旦涉及父子类,顺序会多几步。规律是静态的先全部跑完,再跑实例的;同一层里父类先于子类。
class Parent {
static { System.out.println("1 父类静态块"); }
{ System.out.println("3 父类实例块"); }
Parent() { System.out.println("4 父类构造"); }
}
class Child extends Parent {
static { System.out.println("2 子类静态块"); }
{ System.out.println("5 子类实例块"); }
Child() { System.out.println("6 子类构造"); }
}
public class InheritDemo {
public static void main(String[] args) {
new Child();
}
}
```text
输出正好是 1 到 6 的顺序:
```text
1 父类静态块
2 子类静态块
3 父类实例块
4 父类构造
5 子类实例块
6 子类构造
为什么父类构造(第 4 步)跑在子类实例块(第 5 步)前面?因为子类构造方法的第一行隐含着 super(),必须先把父类那部分建好,子类才能在完整的地基上初始化自己。
这个顺序在第 41 章讲 super 时还会再碰到。
静态块只在类加载时跑
有个常见误解:以为写了静态块,程序一启动就会执行。
事实是——类没被用到,静态块就不会跑。
class Lazy {
static { System.out.println("Lazy 被加载了"); }
static int value = 100;
}
public class LazyDemo {
public static void main(String[] args) {
System.out.println("程序开始");
// 这行不写的话,上面那句打印永远不会出现
System.out.println(Lazy.value);
}
}
```bash
只有真正访问了 `Lazy.value`,JVM 才去加载 `Lazy` 类,静态块这时才执行。
> [!WARNING]
> 别把「程序启动就要做的事」放进某个业务类的静态块,它可能永远不被触发。这种逻辑应该放在 main 方法里显式调用,或者交给框架的启动钩子。
> [!TIP]
> 记忆口诀:静态最先且只一次,实例每次构造前,父类永远排子类前。把「全程只做一次的配置」放静态块,「每个对象都要的前置」放实例块或构造方法。
## 双括号初始化:看着聪明的坑
网上流传一种利用实例代码块的写法,叫「双括号初始化」:
```java
List<String> list = new ArrayList<>() {{
add("苹果");
add("香蕉");
}};
外层括号定义了一个匿名子类,内层括号是这个匿名类的实例代码块。写法确实紧凑,但代价不小。
每写一次就多生成一个匿名类的 class 文件,类越多类加载越慢。更麻烦的是这个匿名类持有外部类的隐式引用,本该被回收的对象会被它拖住不放,容易造成内存泄漏。
需要快速造一个不可变列表,用 Java 9 引入的工厂方法就行:
List<String> list = List.of("苹果", "香蕉");
```bash
一行搞定,没有额外的类,返回的还是不可变列表。看到双括号初始化的旧代码,知道它在干什么就够了,不要跟着写。
## 常见疑问
**问:静态代码块里能用 this 吗?**
不能。静态块属于类,执行时可能一个对象都没有,`this` 没有指向。写了会报 `error: non-static variable this cannot be referenced from a static context`。同理,静态块里也不能直接访问实例变量。
**问:实例代码块和在字段声明处直接赋值,有区别吗?**
执行时机上没有区别,两者按书写顺序混在一起跑。区别在能写多复杂:字段声明处只能写一个表达式,实例代码块里可以写循环、条件判断、异常处理。逻辑简单就直接赋值,需要几行才能算出来才用实例块。
**问:静态代码块里抛异常会怎样?**
后果比想象的严重。类初始化失败,JVM 会抛出 `ExceptionInInitializerError`,而且这个类**再也无法使用**——后续任何访问都直接抛 `NoClassDefFoundError`,连重试的机会都没有。所以静态块里的代码要格外稳妥,别放可能失败的网络请求、文件读取这类操作。