代码块、内部类与匿名内部类 | JavaSE
代码块、内部类与匿名内部类
一、学习目标
学完本章,你应该能够:
- 能够区分静态代码块与实例代码块,并解释它们各自的执行时机和典型用途。
- 能够解释内部类(Nested / Inner Class)的作用及主要分类。
- 能够正确创建成员内部类和静态嵌套类对象。
- 能够理解成员内部类为什么能够访问外部类实例成员,并正确使用
Outer.this。 - 能够说明局部类的作用域特点。
- 能够手写匿名内部类,并将其作为接口或抽象类的一次性实现使用。
- 能够根据对象之间的关系判断是否适合使用内部类,而不是机械地把类嵌套在一起。
二、核心知识
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 知识问答
- 什么是静态代码块?
- 静态代码块在什么阶段执行?
- 静态代码块主要适合初始化什么数据?
- 什么是实例代码块?
- 实例代码块和对象创建是什么关系?
- 什么是内部类?
- 什么是成员内部类?
- 成员内部类为什么能够访问外部类实例成员?
Outer.this表示什么?- 普通成员内部类如何创建对象?
- 什么是静态嵌套类?
- 静态嵌套类为什么不能直接访问外围类实例成员?
- 什么是局部类?
- 什么是匿名内部类?
- 匿名内部类能否直接理解成“new 接口”?
- 匿名内部类为什么经常作为方法参数使用?
- 什么场景适合内部类?
- 什么场景应该使用独立顶级类?
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();
}
}
不运行代码,回答:
- 每个字母分别输出几次?
- 第一次创建对象时大致顺序是什么?
- 第二次创建对象时为什么不会再次输出相同次数的
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);
}
}
}
回答:
- 三次输出分别访问哪个变量?
this当前指向哪个对象?Outer.this当前指向哪个对象?
7.3 手写代码
- 创建同时具有静态代码块、实例代码块、无参构造器和有参构造器的类,并分析执行顺序。
- 设计
Car.Engine成员内部类,使Engine能读取汽车品牌。 - 设计一个
User.Builder静态嵌套类。 - 编写局部类并说明它的作用域。
- 使用匿名内部类实现一个只有
run()方法的接口。 - 把匿名内部类对象直接作为方法参数传递。
- 使用匿名内部类继承一个抽象类并完成抽象方法。
7.4 Debug
下面代码存在问题:
class Outer {
class Inner {
}
}
public class Test {
public static void main(String[] args) {
Outer.Inner inner = new Outer.Inner();
}
}
要求:
- 判断问题原因。
- 判断
Inner是否依赖Outer实例。 - 修复对象创建代码。
分析:
class Outer {
int age = 18;
static class Inner {
void show() {
System.out.println(age);
}
}
}
要求:
- 判断能否编译。
- 解释问题本质。
- 至少提供一种合理修改方案。
分析:
interface Runner {
void run();
}
public class Test {
public static void main(String[] args) {
Runner r = new Runner();
}
}
要求:
- 为什么不能直接创建接口对象?
- 使用匿名内部类修复。
- 解释修复后真正创建的是什么对象。
7.5 综合训练
设计一个“任务执行器”。
要求:
- 定义
Task接口,仅包含execute()。 - 定义方法:
runTask(Task task)
- 不单独创建实现类文件。
- 使用匿名内部类完成“保存文件”任务。
- 再使用匿名内部类完成“发送消息”任务。
- 两个任务都通过同一个
runTask()执行。 - 用自己的语言说明其中体现的:
- 接口;
- 多态;
- 匿名内部类;
- 行为参数化思想。
7.6 本章验收
闭卷完成:
- [ ] 手写静态代码块。
- [ ] 手写实例代码块。
- [ ] 解释两者执行时机。
- [ ] 手写成员内部类。
- [ ] 正确创建成员内部类对象。
- [ ] 使用
Outer.this。 - [ ] 手写静态嵌套类。
- [ ] 解释静态嵌套类为什么不依赖外围实例。
- [ ] 说明局部类作用域。
- [ ] 手写接口匿名内部类。
- [ ] 手写抽象类匿名子类。
- [ ] 把匿名内部类作为方法参数。
- [ ] 能够 Debug 三种典型内部类对象创建错误。
如果只能看懂:
new Interface() {
...
};
却不能解释它到底创建了什么,那么匿名内部类还没有真正掌握。