内部类与匿名类

为什么需要内部类

有时候一个类只为另一个类服务,没必要单独建一个文件,直接塞进去更干净

就像你的钱包里有卡槽——卡槽只为钱包服务,没必要单独存在

Java 有四种内部类:静态内部类成员内部类局部内部类匿名内部类

静态内部类 —— 最常用、最简单

static 修饰的内部类,和外部类关系最”疏远”——不依赖外部类的实例

可以理解为碰巧写在另一个类里面的普通类,只是逻辑上有归属关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class School {
private String schoolName = "清华大学";

// 静态内部类
public static class Address {
String city;
String street;

public Address(String city, String street) {
this.city = city;
this.street = street;
}

public void print() {
// ❌ 不能直接访问外部类的非静态成员
// System.out.println(schoolName); // 编译报错!
System.out.println(city + " " + street);
}
}
}

@Test
public void testStaticInner() {
// 创建静态内部类的对象:不需要外部类实例
School.Address addr = new School.Address("北京", "清华路1号");
addr.print(); // 北京 清华路1号
}

使用场景

Builder 模式(Person.Builder

Map 里的 Map.Entry

把逻辑相关的小类组织在一起

实际开发中最推荐用这种,没有乱七八糟的隐式引用

成员内部类 —— 绑定外部实例

没有 static 修饰的内部类,每个内部类对象都绑定一个外部类对象

就像每个”器官”都依附于一个”人”,不能独立存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Person {
private String name;

public Person(String name) {
this.name = name;
}

// 成员内部类(没有 static)
public class Heart {
public void beat() {
// ✅ 可以直接访问外部类的私有成员!
System.out.println(name + "的心脏在跳动");
}
}
}

@Test
public void testMemberInner() {
Person person = new Person("张三");

// 创建成员内部类:必须先有外部类实例
Person.Heart heart = person.new Heart(); // 注意语法:外部对象.new 内部类()
heart.beat(); // 张三的心脏在跳动
}

注意事项

成员内部类会隐式持有外部类的引用(Person.this

这可能导致内存泄漏——外部类对象本该被回收,但内部类还引用着它

所以如果不需要访问外部类的实例成员,优先用静态内部类

局部内部类 —— 几乎不用

定义在方法体内部的类,作用域仅限于这个方法

就像临时工,干完这个活就走,别的地方不认识它

1
2
3
4
5
6
7
8
9
10
11
12
public void someMethod() {
// 局部内部类:只在这个方法里能用
class LocalHelper {
void help() {
System.out.println("我是临时工");
}
}

LocalHelper helper = new LocalHelper();
helper.help();
}
// 出了方法,LocalHelper 就不存在了

实际开发几乎不用,知道有这个东西就行,通常用匿名类或 Lambda 代替

匿名内部类 —— 一次性的类

没有名字的类,写完就用,用完就扔,常用于回调事件处理

就像叫外卖——你不需要知道骑手的名字,只需要他把东西送到就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testAnonymousClass() {
// 正常写法:先定义类,再创建对象
// class MyComparator implements Comparator<String> { ... }
// Comparator<String> comp = new MyComparator();

// 匿名内部类写法:定义和创建一步到位
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length(); // 按字符串长度排序
}
}; // 注意这里有分号!

List<String> names = Arrays.asList("Alice", "Bob", "Christina");
Collections.sort(names, comp);
System.out.println(names); // [Bob, Alice, Christina]
}

更常见的写法是直接内联,不用变量接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testAnonymousInline() {
List<String> names = Arrays.asList("Alice", "Bob", "Christina");

// 直接把匿名类塞进方法参数里
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});

System.out.println(names); // [Bob, Alice, Christina]
}

使用场景

实现只用一次的接口或抽象类

事件监听器(Swing/Android开发大量使用)

线程创建:new Thread(new Runnable() { ... })

匿名类 → Lambda 的进化(预告)

Java 8 引入了 Lambda 表达式,可以大幅简化匿名类的写法

条件:接口只有一个抽象方法(叫”函数式接口”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testAnonymousVsLambda() {
List<String> names = Arrays.asList("Alice", "Bob", "Christina");

// 匿名类写法(Java 7 及之前)—— 啰嗦
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});

// Lambda 写法(Java 8+)—— 清爽
Collections.sort(names, (a, b) -> a.length() - b.length());

// 更简洁的方法引用写法
Collections.sort(names, Comparator.comparingInt(String::length));
}

看到没?6行代码变成了1行。Lambda本质上就是匿名类的语法糖

后面学到 函数式编程 会详细讲 Lambda,现在只需要知道匿名类是它的”前身”

四种内部类对比表

特性 静态内部类 成员内部类 局部内部类 匿名内部类
关键字 static class class 方法内 class new 接口/类() { }
能否访问外部类实例成员
创建方式 new 外部类.内部类() 外部对象.new 内部类() 方法内直接 new 定义时就创建
有没有名字 没有
使用频率 ⭐⭐⭐ 高 ⭐⭐ 中 ⭐ 极低 ⭐⭐⭐ 高(Java 8 前)
推荐程度 首选 谨慎用 不推荐 Java 8 后用 Lambda 替代

访问修饰符方面:静态内部类和成员内部类可以用四种修饰符(参见 访问修饰符),局部和匿名类不能加修饰符

常见坑

坑1:成员内部类导致内存泄漏

成员内部类隐式持有外部类的引用 OuterClass.this

如果内部类对象的生命周期比外部类长,外部类就无法被垃圾回收

解决:能用静态内部类就用静态的

坑2:匿名类访问局部变量必须是 final 或 effectively final

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testFinalVariable() {
int count = 10; // 没有写 final,但后面没有修改过,所以是 "effectively final"

Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(count); // ✅ 可以访问
// count++; // ❌ 不能修改外部局部变量!
}
};

// count = 20; // 如果加了这行,上面匿名类里访问 count 也会编译报错
}

原因:匿名类拷贝了一份局部变量的值,如果允许修改就会导致数据不一致

坑3:匿名类不能定义构造器

因为没有类名,就没法写构造器。如果需要初始化逻辑,用 { } 实例初始化块

坑4:匿名类中的 this

匿名类中的 this 指向匿名类自己,不是外部类

要访问外部类用 外部类名.this

练习题

题1:下面的代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
public class Outer {
String msg = "外部类";

class Inner {
String msg = "内部类";
void print() {
System.out.println(msg); // ?
System.out.println(this.msg); // ?
System.out.println(Outer.this.msg); // ?
}
}
}

答案:内部类、内部类、外部类。当内外部类有同名成员时,优先访问内部类自己的,要访问外部类的需要用 Outer.this.msg

题2:为什么推荐优先使用静态内部类?

答案:静态内部类不持有外部类的隐式引用,不会导致内存泄漏,也更容易理解和维护

题3:把下面的匿名类改写成 Lambda 表达式

1
2
3
4
5
6
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};

答案:Runnable r = () -> System.out.println("Hello");


上一章 目录 下一章
接口与抽象类 java基础 JavaBean规范