首页 / Java 入门教程 / 代码块

Java 入门教程

代码块

本教程共 100 篇 · 第 38 篇 · 更新于 2026-08-05 · 约 13 分钟阅读

JavaJava 入门教程代码块静态代码块实例代码块初始化顺序

本节目标:认识静态代码块、实例代码块、局部代码块三种代码块,弄清它们的执行时机和先后顺序,避免把初始化逻辑放错地方。

代码块就是一对花括号 {} 包起来的一段代码。按出现的位置,分三种:局部代码块、实例代码块、静态代码块,执行时机各不相同。

搞不清顺序会出什么事?变量还没初始化就被用了,程序抛空指针,你却盯着代码看半天想不通——明明赋值语句写在那儿。

局部代码块:限定变量范围

写在方法里的普通 {},用来缩小变量作用域。

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);
    }
}

放开注释那行,编译器会说:

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("带参构造");
    }
}

不管调哪个构造方法,实例代码块都先执行。多个实例代码块按书写顺序依次跑。

它的本质是编译器帮你做的「代码搬运」:把实例代码块的内容复制到每个构造方法的开头。所以它相当于所有构造方法的公共前置逻辑。

Note

实例代码块在现代代码里用得不多。有公共初始化逻辑时,更常见的是抽一个私有 init() 方法,或者用 this(...) 串联构造。实例代码块的问题是位置隐蔽,读代码时容易漏看。

它真正有用的场合是匿名内部类——匿名类没法定义构造方法,只能靠实例代码块做初始化。

静态代码块:类加载时跑一次

static {} 在类第一次被加载时执行,且只执行一次。

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
}

赋值允许,读取不允许。而且后面的显式赋值会盖掉前面静态块里的值,最终 value 是 20。所以静态块尽量写在静态变量声明之后。

完整执行顺序演示

把三种代码块和构造方法放一起,跑一遍看真实顺序:

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

输出:

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();
    }
}

输出正好是 1 到 6 的顺序:

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);
    }
}

只有真正访问了 Lazy.value,JVM 才去加载 Lazy 类,静态块这时才执行。

Warning

别把「程序启动就要做的事」放进某个业务类的静态块,它可能永远不被触发。这种逻辑应该放在 main 方法里显式调用,或者交给框架的启动钩子。

Tip

记忆口诀:静态最先且只一次,实例每次构造前,父类永远排子类前。把「全程只做一次的配置」放静态块,「每个对象都要的前置」放实例块或构造方法。

双括号初始化:看着聪明的坑

网上流传一种利用实例代码块的写法,叫「双括号初始化」:

List<String> list = new ArrayList<>() {{
    add("苹果");
    add("香蕉");
}};

外层括号定义了一个匿名子类,内层括号是这个匿名类的实例代码块。写法确实紧凑,但代价不小。

每写一次就多生成一个匿名类的 class 文件,类越多类加载越慢。更麻烦的是这个匿名类持有外部类的隐式引用,本该被回收的对象会被它拖住不放,容易造成内存泄漏。

需要快速造一个不可变列表,用 Java 9 引入的工厂方法就行:

List<String> list = List.of("苹果", "香蕉");

一行搞定,没有额外的类,返回的还是不可变列表。看到双括号初始化的旧代码,知道它在干什么就够了,不要跟着写。

常见疑问

问:静态代码块里能用 this 吗?

不能。静态块属于类,执行时可能一个对象都没有,this 没有指向。写了会报 error: non-static variable this cannot be referenced from a static context。同理,静态块里也不能直接访问实例变量。

问:实例代码块和在字段声明处直接赋值,有区别吗?

执行时机上没有区别,两者按书写顺序混在一起跑。区别在能写多复杂:字段声明处只能写一个表达式,实例代码块里可以写循环、条件判断、异常处理。逻辑简单就直接赋值,需要几行才能算出来才用实例块。

问:静态代码块里抛异常会怎样?

后果比想象的严重。类初始化失败,JVM 会抛出 ExceptionInInitializerError,而且这个类再也无法使用——后续任何访问都直接抛 NoClassDefFoundError,连重试的机会都没有。所以静态块里的代码要格外稳妥,别放可能失败的网络请求、文件读取这类操作。