代码块、内部类与匿名内部类 | JavaSE

代码块、内部类与匿名内部类

一、学习目标

学完本章,你应该能够:

  1. 能够区分静态代码块与实例代码块,并解释它们各自的执行时机和典型用途。
  2. 能够解释内部类(Nested / Inner Class)的作用及主要分类。
  3. 能够正确创建成员内部类和静态嵌套类对象。
  4. 能够理解成员内部类为什么能够访问外部类实例成员,并正确使用 Outer.this
  5. 能够说明局部类的作用域特点。
  6. 能够手写匿名内部类,并将其作为接口或抽象类的一次性实现使用。
  7. 能够根据对象之间的关系判断是否适合使用内部类,而不是机械地把类嵌套在一起。

二、核心知识

2.1 Java 类中除了字段和方法,还有什么

此前我们接触过一个类中的:

成员变量
成员方法
构造器

除此之外,类中还可以存在代码块、嵌套类等结构。

例如:

public class User {

    private String name;

    static {
        System.out.println("静态代码块");
    }

    {
        System.out.println("实例代码块");
    }

    public User() {
        System.out.println("构造器");
    }

    class Profile {
    }
}

这里同时存在:

  • 成员变量;
  • 静态初始化块;
  • 实例初始化块;
  • 构造器;
  • 成员内部类。

2.2 静态代码块

静态代码块(Static Initializer)使用:

static {
    // 初始化代码
}

例如:

public class GameConfig {

    public static String gameName;
    public static int maxPlayers;

    static {
        gameName = "Star Rain";
        maxPlayers = 100;
        System.out.println("静态初始化完成");
    }
}

静态代码块属于类初始化逻辑

一个类在一次正常初始化过程中,静态初始化逻辑只会执行一次。

例如:

public class Demo {

    static {
        System.out.println("static");
    }

    public Demo() {
        System.out.println("constructor");
    }

    public static void main(String[] args) {
        new Demo();
        new Demo();
        new Demo();
    }
}

核心现象是:

static

constructor
constructor
constructor

也就是说:

静态代码块
    ↓
针对类

构造器
    ↓
针对对象

2.3 静态代码块适合做什么

静态代码块主要适合完成:

类级别静态资源的初始化。

例如:

public class CardGame {

    public static final String[] CARDS = new String[54];

    static {
        CARDS[0] = "黑桃A";
        CARDS[1] = "黑桃2";
        CARDS[2] = "黑桃3";
    }
}

如果某个静态字段的初始化:

  • 不是一个简单表达式能够完成;
  • 需要多条语句;
  • 需要循环、判断或者其他初始化过程;

就可以考虑静态代码块。

不过现代 Java 中不应该看到 static {} 就认为它一定“高级”。

如果可以简单写:

private static final String NAME = "星雨笔录";

就没有必要写:

private static final String NAME;

static {
    NAME = "星雨笔录";
}

原则仍然是:

使用最简单、最清晰的初始化方式。


2.4 实例代码块

实例代码块(Instance Initializer)没有 static

{
    // 对象初始化代码
}

例如:

public class Player {

    private String status;

    {
        status = "ONLINE";
        System.out.println("实例代码块执行");
    }

    public Player() {
        System.out.println("构造器执行");
    }
}

每创建一个对象,都需要执行相应的实例初始化逻辑。

new Player();
new Player();

因此可以建立模型:

static 初始化
    ↓
类级别
    ↓
通常一次

instance 初始化
    ↓
对象级别
    ↓
每创建对象都会参与执行

2.5 实例代码块和构造器是什么关系

例如:

public class Student {

    {
        System.out.println("实例代码块");
    }

    public Student() {
        System.out.println("无参构造器");
    }

    public Student(String name) {
        System.out.println("有参构造器:" + name);
    }
}

无论调用哪个构造器:

new Student();
new Student("LingXi");

实例代码块中的初始化逻辑都会参与对象初始化。

因此它特别适合抽取:

多个构造器共同需要执行的实例初始化逻辑。

不过在实际工程代码中,很多公共初始化同样可以通过:

  • 字段初始化;
  • 构造器链;
  • 私有方法;

完成。

所以实例代码块属于需要掌握的 Java 语言能力,但不是要求“能用就一定要用”。


2.6 什么是内部类

如果一个类声明在另一个类或接口的内部,它属于嵌套类结构。

例如:

public class Car {

    public class Engine {

    }
}

从业务语义理解:

Car
└── Engine

如果 Engine

  • Car 有非常紧密的关系;
  • 主要服务于 Car
  • 没有很强的独立存在价值;

将其设计在 Car 内部可能更加自然。


2.7 内部类的主要类型

在当前学习阶段,可以建立下面的分类:

嵌套类
│
├── 成员内部类
│
├── 静态嵌套类
│
├── 局部类
│
└── 匿名类

教学中经常把它们统称为“内部类”。

但从严格语言术语看:

static 修饰的成员嵌套类并不是严格意义上的 inner class,因为它不依赖外围类实例。

学习时最重要的是先理解它们的对象关系。


2.8 成员内部类

成员内部类定义在外部类的成员位置,而且没有 static

例如:

public class Car {

    private String brand;

    public Car(String brand) {
        this.brand = brand;
    }

    public class Engine {

        public void start() {
            System.out.println(brand + " 的发动机启动");
        }
    }
}

Engine 对象与某一个具体 Car 对象绑定。

因此创建它时通常需要先有外部类对象。


2.9 创建成员内部类对象

基本格式:

外部类名.内部类名 对象名 =
        new 外部类(...).new 内部类(...);

例如:

Car.Engine engine =
        new Car("BMW").new Engine();

更容易理解的写法:

Car car = new Car("BMW");

Car.Engine engine = car.new Engine();

engine.start();

这里表达的对象关系非常清晰:

car
 │
 └── engine

这个 engine 属于当前这个 car 对应的外围实例环境。


2.10 成员内部类可以访问外部类实例成员

例如:

public class Outer {

    private int age = 18;
    private static String school = "Star University";

    public class Inner {

        public void show() {
            System.out.println(age);
            System.out.println(school);
        }
    }
}

成员内部类可以直接访问:

age
school

其中最关键的是:

age

是外部类的实例字段

为什么内部类知道访问的是哪一个 Outer 对象?

因为成员内部类对象与一个外围 Outer 实例存在关联。


2.11 Outer.this

假设内外类存在同名变量:

public class Outer {

    private int age = 18;

    public class Inner {

        private int age = 20;

        public void show() {
            int age = 30;

            System.out.println(age);
            System.out.println(this.age);
            System.out.println(Outer.this.age);
        }
    }
}

三个 age 分别表示:

age
→ 当前方法局部变量
→ 30

this.age
→ 当前 Inner 对象字段
→ 20

Outer.this.age
→ 当前关联的 Outer 对象字段
→ 18

所以:

Outer.this

表示:

当前内部类对象所关联的外围类对象。

这是理解成员内部类对象模型的关键语法。


2.12 静态嵌套类

如果成员类使用:

static

修饰:

public class Outer {

    public static class Inner {

    }
}

它通常被称为:

静态嵌套类(Static Nested Class)。

教学资料中也经常称为“静态内部类”。

它最大的区别在于:

创建静态嵌套类对象不需要先创建外部类对象。


2.13 静态嵌套类对象的创建

格式:

Outer.Inner inner = new Outer.Inner();

例如:

public class Computer {

    public static class Builder {

        public void build() {
            System.out.println("build computer");
        }
    }
}

调用:

Computer.Builder builder =
        new Computer.Builder();

builder.build();

不需要:

new Computer().new Builder();

因为 Builder 不依赖某一个具体 Computer 对象。


2.14 静态嵌套类访问外部类成员

例如:

public class Outer {

    private static String school = "Star University";
    private int age = 18;

    public static class Inner {

        public void show() {
            System.out.println(school);

            // 不能直接访问 age
        }
    }
}

静态嵌套类可以直接访问外围类的静态成员:

school

但不能凭空直接访问某一个外围对象的:

age

原因非常简单:

static Inner 对象
        ↓
没有天然绑定某个 Outer 对象

如果显式获得一个对象:

Outer outer = new Outer();

当然仍然可以通过这个对象进行合法访问。

问题不是“永远不能访问实例数据”,而是:

没有外围实例,就不存在可以直接使用的外围实例成员。


2.15 局部类

局部类(Local Class)声明在方法、构造器或代码块内部。

例如:

public class Demo {

    public void test() {

        class LocalTask {

            public void run() {
                System.out.println("running");
            }
        }

        LocalTask task = new LocalTask();
        task.run();
    }
}

它的主要特点是:

只在声明它的局部作用域中可见。

方法外部不能直接:

new LocalTask();

局部类通常适合:

  • 某一小段局部逻辑需要一个类;
  • 该类型没有暴露到方法外部的价值。

在现代 Java 普通业务代码中直接编写具名局部类的频率并不算特别高,但它是理解匿名类与 Lambda 的重要基础。


2.16 匿名内部类

匿名类(Anonymous Class)是一种没有显式类名的类表达式。

典型语法:

new 父类或接口类型(...) {

    // 类体
};

例如存在接口:

public interface Runner {

    void run();
}

过去可能先写:

public class StudentRunner implements Runner {

    @Override
    public void run() {
        System.out.println("学生跑步");
    }
}

然后:

Runner runner = new StudentRunner();

如果这个实现只使用一次,可以直接:

Runner runner = new Runner() {

    @Override
    public void run() {
        System.out.println("学生跑步");
    }
};

这就是匿名内部类。


2.17 匿名内部类到底是什么

很多初学者容易把:

new Runner() {
    ...
}

理解成:

“接口居然可以 new 了。”

这是错误的。

Runner 接口本身仍然不能直接实例化。

真正发生的是:

根据 Runner 创建一个匿名实现类
          ↓
重写 run()
          ↓
立即创建这个匿名实现类的对象
          ↓
把对象赋值给 Runner 引用

因此:

Runner runner = new Runner() {
    @Override
    public void run() {
        System.out.println("run");
    }
};

本质可以理解成:

匿名实现类对象
        ↓
Runner runner

仍然属于:

多态。


三、使用方法

3.1 匿名内部类实现接口

定义:

public interface Swimming {

    void swim();
}

使用:

public class Demo {

    public static void main(String[] args) {

        Swimming student = new Swimming() {

            @Override
            public void swim() {
                System.out.println("学生正在游泳");
            }
        };

        student.swim();
    }
}

3.2 匿名内部类继承抽象类

例如:

public abstract class Animal {

    public abstract void cry();
}

使用:

Animal cat = new Animal() {

    @Override
    public void cry() {
        System.out.println("喵喵喵");
    }
};

cat.cry();

这里实际上创建了:

Animal 的匿名子类对象

而不是直接创建 Animal 抽象类对象。


3.3 匿名内部类作为方法参数

接口:

public interface Swimming {

    void swim();
}

方法:

public static void startSwimming(Swimming swimming) {
    System.out.println("比赛开始");
    swimming.swim();
}

调用时:

startSwimming(new Swimming() {

    @Override
    public void swim() {
        System.out.println("选手游泳");
    }
});

这就是匿名内部类极其经典的使用方式:

方法需要一个接口对象
        ↓
临时创建一个一次性实现
        ↓
直接作为参数传入

3.4 为什么匿名内部类适合一次性实现

假设只需要一处:

Runner

实现。

如果专门创建:

StudentRunner.java
TeacherRunner.java
TemporaryRunner.java

会增加许多只使用一次的小类。

匿名内部类可以把:

实现类声明
+
对象创建

合并在一起。

因此它特别适用于:

小型、局部、一次性的行为实现。


3.5 什么时候不应该使用匿名内部类

如果逻辑越来越复杂:

new Task() {
    // 100 行代码……
};

代码可读性会迅速下降。

如果一个实现:

  • 需要重复使用;
  • 包含复杂状态;
  • 包含大量方法;
  • 需要独立测试;
  • 有明确业务语义;

更应该创建正式的具名类。


四、原理与进阶

4.1 “嵌套类”与“内部类”的严格区别

日常教学经常把所有写在类内部的类都叫“内部类”。

但严格来说,可以区分:

Nested Class
嵌套类
│
├── static nested class
│   静态嵌套类
│
└── inner class
    内部类

JDK 21 语言规范中的 inner class 指:

非显式或隐式 static 的嵌套类。

所以:

class Outer {
    class A {
    }

    static class B {
    }
}

其中:

A
→ inner class

B
→ nested class
→ 但不是 inner class

实际开发交流中知道这个术语差异即可,不必因为名称影响正常编码。


4.2 成员内部类为什么需要外围实例

观察:

Outer outer = new Outer();

Outer.Inner inner = outer.new Inner();

这里不是偶然的语法设计。

它表达的是:

Inner 实例
   │
   └── 与 outer 实例存在 enclosing instance 关系

所以内部类中才能自然使用:

Outer.this

并访问对应对象的实例成员。


4.3 匿名内部类不是“完全不存在类”

“匿名”只意味着:

源代码中没有为它显式声明一个普通类名。

它仍然是一个真实的类,并且最终创建真实对象。

因此不要把它理解成:

没有类
直接创建接口对象

正确模型是:

匿名类
+
匿名类对象

只不过编译器负责处理名称和生成细节。


4.4 匿名内部类与 Lambda 的关系

例如:

Runnable runnable = new Runnable() {

    @Override
    public void run() {
        System.out.println("run");
    }
};

以后学习 Lambda 时,可以写成:

Runnable runnable =
        () -> System.out.println("run");

但二者不能简单理解成“完全相同的语法糖”。

本章只需要建立一个前置认识:

Lambda 可以简化某些函数式接口场景下的匿名内部类式写法。

Lambda 的:

  • 适用条件;
  • this 语义;
  • 函数式接口;
  • 方法引用;

将在 GROUP 06 单独系统学习。


五、实践应用

5.1 Car 与 Engine

如果发动机只作为汽车内部模型存在:

public class Car {

    private final String brand;

    public Car(String brand) {
        this.brand = brand;
    }

    public class Engine {

        public void start() {
            System.out.println(
                    brand + " engine started"
            );
        }
    }
}

这种设计表达:

Engine 强依附于某辆 Car

5.2 Builder 与静态嵌套类

以后会经常看到:

public class User {

    private String name;
    private int age;

    public static class Builder {

        private String name;
        private int age;

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }
    }
}

Builder 不天然需要依附某一个已经存在的 User 对象,因此使用静态嵌套类更加自然。

这也是静态嵌套类在工程设计中的常见场景。


5.3 回调式行为

假设某个方法需要临时行为:

public static void execute(Runnable task) {
    task.run();
}

可以:

execute(new Runnable() {

    @Override
    public void run() {
        System.out.println("执行临时任务");
    }
});

这就是:

把“行为实现”封装成对象
        ↓
作为参数传递

它也为之后理解:

  • Lambda;
  • 回调;
  • Stream;
  • GUI 事件监听;

打下基础。


六、常见问题

6.1 静态代码块什么时候执行?

更准确的说法是:

在类初始化过程中执行,而不是简单理解成“.class 文件一加载就立即执行”。

“加载”和“初始化”是 JVM 生命周期中不同的概念。

初学阶段重点记住:

第一次触发类初始化
        ↓
执行静态初始化逻辑
        ↓
一次初始化过程中只执行一次

6.2 静态代码块每创建一个对象都会执行吗?

不会。

new User();
new User();
new User();

不会因此把同一个类的静态代码块重新执行三次。

实例初始化逻辑则会参与每个新对象的初始化。


6.3 实例代码块一定比构造器先执行吗?

对于当前类的实例初始化流程,可以理解为:

实例字段初始化 / 实例代码块
        ↓
构造器主体

但完整对象创建还涉及:

父类构造过程

所以真正理解继承下的对象初始化顺序时,要结合此前学习的 super(...) 一起分析。


6.4 成员内部类为什么不能直接 new Inner()?

在外部类之外,普通成员内部类需要对应一个外围类实例。

所以:

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

才完整表达了对象关系。


6.5 静态嵌套类为什么不需要外围对象?

因为它本身处在静态成员位置,不与某个具体外围对象绑定。

因此:

Outer.Inner inner = new Outer.Inner();

即可。


6.6 匿名内部类是不是接口对象?

不是。

例如:

new Runnable() {
    ...
}

创建的是:

Runnable 的匿名实现类对象。


6.7 匿名内部类有构造器吗?

匿名类不能像普通具名类一样声明一个与类名同名的构造器,因为它没有可以在源码中使用的普通类名。

如果需要复杂对象构造逻辑,往往意味着应该重新考虑是否应使用正式具名类。


6.8 内部类越多设计就越好吗?

不是。

内部类应该表达:

强关联和封装边界。

如果两个类:

  • 都有独立业务意义;
  • 都会被其他模块大量使用;
  • 生命周期完全独立;

通常应该分别设计为顶级类。


七、练习与验收

7.1 知识问答

  1. 什么是静态代码块?
  2. 静态代码块在什么阶段执行?
  3. 静态代码块主要适合初始化什么数据?
  4. 什么是实例代码块?
  5. 实例代码块和对象创建是什么关系?
  6. 什么是内部类?
  7. 什么是成员内部类?
  8. 成员内部类为什么能够访问外部类实例成员?
  9. Outer.this 表示什么?
  10. 普通成员内部类如何创建对象?
  11. 什么是静态嵌套类?
  12. 静态嵌套类为什么不能直接访问外围类实例成员?
  13. 什么是局部类?
  14. 什么是匿名内部类?
  15. 匿名内部类能否直接理解成“new 接口”?
  16. 匿名内部类为什么经常作为方法参数使用?
  17. 什么场景适合内部类?
  18. 什么场景应该使用独立顶级类?

7.2 代码阅读

阅读:

public class Demo {

    static {
        System.out.println("A");
    }

    {
        System.out.println("B");
    }

    public Demo() {
        System.out.println("C");
    }

    public static void main(String[] args) {
        new Demo();
        new Demo();
    }
}

不运行代码,回答:

  1. 每个字母分别输出几次?
  2. 第一次创建对象时大致顺序是什么?
  3. 第二次创建对象时为什么不会再次输出相同次数的 A

阅读:

class Outer {

    int value = 10;

    class Inner {

        int value = 20;

        void print() {
            int value = 30;

            System.out.println(value);
            System.out.println(this.value);
            System.out.println(Outer.this.value);
        }
    }
}

回答:

  1. 三次输出分别访问哪个变量?
  2. this 当前指向哪个对象?
  3. Outer.this 当前指向哪个对象?

7.3 手写代码

  1. 创建同时具有静态代码块、实例代码块、无参构造器和有参构造器的类,并分析执行顺序。
  2. 设计 Car.Engine 成员内部类,使 Engine 能读取汽车品牌。
  3. 设计一个 User.Builder 静态嵌套类。
  4. 编写局部类并说明它的作用域。
  5. 使用匿名内部类实现一个只有 run() 方法的接口。
  6. 把匿名内部类对象直接作为方法参数传递。
  7. 使用匿名内部类继承一个抽象类并完成抽象方法。

7.4 Debug

下面代码存在问题:

class Outer {

    class Inner {
    }
}

public class Test {

    public static void main(String[] args) {
        Outer.Inner inner = new Outer.Inner();
    }
}

要求:

  1. 判断问题原因。
  2. 判断 Inner 是否依赖 Outer 实例。
  3. 修复对象创建代码。

分析:

class Outer {

    int age = 18;

    static class Inner {

        void show() {
            System.out.println(age);
        }
    }
}

要求:

  1. 判断能否编译。
  2. 解释问题本质。
  3. 至少提供一种合理修改方案。

分析:

interface Runner {
    void run();
}

public class Test {

    public static void main(String[] args) {

        Runner r = new Runner();
    }
}

要求:

  1. 为什么不能直接创建接口对象?
  2. 使用匿名内部类修复。
  3. 解释修复后真正创建的是什么对象。

7.5 综合训练

设计一个“任务执行器”。

要求:

  1. 定义 Task 接口,仅包含 execute()
  2. 定义方法:
runTask(Task task)
  1. 不单独创建实现类文件。
  2. 使用匿名内部类完成“保存文件”任务。
  3. 再使用匿名内部类完成“发送消息”任务。
  4. 两个任务都通过同一个 runTask() 执行。
  5. 用自己的语言说明其中体现的:
    • 接口;
    • 多态;
    • 匿名内部类;
    • 行为参数化思想。

7.6 本章验收

闭卷完成:

  • [ ] 手写静态代码块。
  • [ ] 手写实例代码块。
  • [ ] 解释两者执行时机。
  • [ ] 手写成员内部类。
  • [ ] 正确创建成员内部类对象。
  • [ ] 使用 Outer.this
  • [ ] 手写静态嵌套类。
  • [ ] 解释静态嵌套类为什么不依赖外围实例。
  • [ ] 说明局部类作用域。
  • [ ] 手写接口匿名内部类。
  • [ ] 手写抽象类匿名子类。
  • [ ] 把匿名内部类作为方法参数。
  • [ ] 能够 Debug 三种典型内部类对象创建错误。

如果只能看懂:

new Interface() {
    ...
};

却不能解释它到底创建了什么,那么匿名内部类还没有真正掌握。