4.6 内部类
内部类 = 类里面再定义类。它解决两件事:① 成员内部类能直接访问外部类的 private 成员——关系铁到能开保险柜;② 匿名内部类让"只用一次的实现"不用起名字——4.3 学的接口/抽象类,在这里迎来最爽的用法。
1. 为什么要把类塞进类里?
先给动机,不然内部类就是"语法炫技":
- 场景一:心脏和人体——
Heart只为Person服务,单独建个文件显得小题大做,把 Heart 写进 Person 里面,关系一目了然。而且 Heart 能直接摸 Person 的 private 属性(普通外部类做不到)。 - 场景二:某个接口的实现全项目只用一次——为它单独建类、起名、存文件,太隆重。匿名内部类三行写完,用完即弃。
四兄弟一览(上面那张图):
| 类型 | 写在哪 | 关键规则 | 重点程度 |
|---|---|---|---|
| 成员内部类 | 类里、方法外 | 能访问外部一切成员(含 private) | ⭐ 必会 |
| 静态内部类 | 成员内部类加 static | 跟类不跟对象,不 new 外部就能用 | 会认 |
| 局部内部类 | 方法体内 | 出了方法就用不了 | 会认 |
| 匿名内部类 | 用的地方现场写 | 没有名字,实现即用即弃 | ⭐⭐ 必精 |
2. 成员内部类:能开外部保险柜的"自己人"
public class Person {
private String name = "波仔"; // 私有属性
private int heartRate = 72;
// 成员内部类:写在类里、方法外
class Heart {
public void beat() {
// 直接访问外部类的 private 成员!普通类做不到
System.out.println(name + " 的心脏在跳,心率 " + heartRate);
}
}
}
注意这打破了 3.3 的规矩:private 是"门外汉免进",但成员内部类不是门外汉——它是外部类身体的一部分,天然有钥匙。这就是"关系紧密才用内部类"的判断标准:能给内部类的,都是敢让它看私房的关系。
怎么创建和使用
public class TestInner {
public static void main(String[] args) {
// 格式:外部类.内部类 变量 = 外部对象.new 内部类();
Person p = new Person();
Person.Heart h = p.new Heart(); // 注意这个不太顺眼的 new
h.beat(); // 输出:波仔 的心脏在跳,心率 72
}
}
格式拆解:
Person.Heart → 内部类的全名(类型得带上"姓")
p.new Heart() → 内部类对象依附于外部对象 p 存在(先有人,才有心脏)
干净的写法是让外部类提供一个方法返回内部类(public Heart getHeart() { return new Heart(); }),外界只管 p.getHeart().beat()——内部类藏在 Person 肚子里,外界甚至不用 import 它。这也是内部类的隐藏价值:比 private 更深的隐藏。
3. 静态内部类与局部内部类:会认就行
静态内部类
class Outer {
static class Inner { } // 加了 static
}
Outer.Inner in = new Outer.Inner(); // 不需要外部对象,直接 new
对照 3.7 全通:static = 属于类不属于对象——所以不用先 new Outer(),格式从 外部对象.new 变成 外部类.new。代价是它碰不了外部类的实例成员(老理由:外部对象可能还没出生)。
局部内部类
class Outer {
void work() {
class Helper { … } // 写在方法里
Helper h = new Helper();
h.help();
}
// work() 外面:Helper 不存在,用不了
}
和 2.2 局部变量一个命:作用域只在方法内,出了花括号就消失。实际开发用得很少,见到认识即可。
4. 匿名内部类(本节主角):一次性实现的正确姿势
先看"没有它"的麻烦。4.3 你写过 Flyable 接口,如果某个实现只在 main 里用一次:
// 传统姿势:为一次性使用专门建类、起名
class Bird implements Flyable {
@Override
public void fly() { System.out.println("扑棱扑棱"); }
}
public class Test {
public static void main(String[] args) {
Flyable f = new Bird(); // 就用这一次……Bird 这个名字有点浪费
f.fly();
}
}
匿名内部类把"建类 + 起名 + 实例化"压缩成一步:
public class Test {
public static void main(String[] args) {
Flyable f = new Flyable() { // new 接口名() { ... } —— 没有类名!
@Override
public void fly() {
System.out.println("扑棱扑棱");
}
};
f.fly(); // 输出:扑棱扑棱
}
}
这段代码的读法:
new Flyable() { ... }
→ "现场造一个 Flyable 的子类"(编译器暗中帮你建了个类)
→ 立刻 new 出它的对象,交给了变量 f
→ 类没名字,所以叫"匿名"——用完即弃,全项目就这一处
三个要点:
- 它本质是多态:
Flyable f左边是接口,右边是"没有名字的实现类"——4.4 的"编译看左运行看右"原样适用。 - 抽象类同样适用:
new Animal() { ... }现场补全所有抽象方法——"必须全兑现否则自己也 abstract"的规矩,匿名类用"当场兑现"绕过了"不能 new"的限制(其实是编译器现场造了个子类帮你 new)。 - 适用判断一条:这个实现只用一次就匿名;要用两次以上,老老实实建正经类。
给按钮绑点击事件、给列表排个序、开个线程干点活——全是匿名内部类的主场。Android/桌面开发里一屏代码一半是它。现在把它写顺,后面章节直接起飞。
多方法接口的诚实提醒
接口有 2 个以上方法时,匿名内部类必须全部实现(合同规则不变):
new Flyable() {
public void fly() { ... }
public void land() { ... } // 少一个都编译报错
}
方法多了匿名类会又臭又长——那是函数式接口(单方法)+ Lambda 的主场,Java 8 的语法糖,这里先混个脸熟:Flyable f = () -> System.out.println("扑棱扑棱"); 能把五行压一行。看不懂没关系,学到这里自然会明白。
5. 动手:心脏 + 一次性飞机
在 IDEA04 里新建 TestInner(Person/Heart 照第 2 节抄):
public class TestInner {
public static void main(String[] args) {
// ① 成员内部类:心脏跳动
Person p = new Person();
p.getHeart().beat(); // 推荐姿势;也可 Person.Heart h = p.new Heart();
// ② 匿名内部类:一次性飞机
Flyable plane = new Flyable() {
@Override
public void fly() { System.out.println("飞机起飞"); }
};
plane.fly();
// ③ 4.4 老朋友串场:Animal 数组 + 匿名类混搭
Animal[] zoo = { new Dog(), new Cat(), new Bird() };
for (Animal a : zoo) a.sound(); // 多态照常工作
}
}
成功标准:三段各出对应输出;注释掉 Person.getHeart() 直接在外部 p.new Heart() 也能跑——但推荐走 getHeart,体会"藏在肚子里"的封装。
三个 30 秒实验
- 内部类摸私房:把 Person 的 name 保持 private,Heart 里照样能访问;再把 Heart 挪出 Person 变成独立类——立刻编译报错"name 具有 private 访问权限"。一挪就懂"自己人"的含金量。
- 匿名类必须全兑现:Flyable 加一个
land()方法,匿名类里只写 fly()——看编译报错"未覆盖抽象方法"。 - 静态内部类不碰实例成员:给静态内部类里写一句访问外部实例属性——看报错,3.7 的"对象可能还没出生"在内部类身上照样成立。
6. 常见误区
它换来两样实打实的东西:访问外部 private 的权限和更深的隐藏(外界甚至感知不到内部类存在)。关系紧密才用——能用普通类解决的别硬塞。
说法不严谨:接口不能被继承(是 implements/实现),类才能 extends(继承)。匿名内部类对接口是"现场实现",对普通类/抽象类是"现场继承"。考试注意措辞。
每次 new 接口名() {...} 都是现场造一个新类 + 新对象——写三次就造三个不同的类。要用多次,请建正经类。
访问不了的是实例成员(跟对象走的);外部类的静态成员它随便用——3.7 的访问规则表原样适用。
成员内部类对象依附于外部对象存在——new Heart() 单独写会编译报错,必须 外部对象.new Heart() 或在外部类内部 new。"先有人,才有心脏"。
7. 自测题(先自己答,再展开)
private 的规矩是"类外免进",但成员内部类写在 Person 类里面,属于"自己人"——它是外部类身体的一部分,天然持有钥匙。普通类在 Person 类外,规矩照挡。
只有 Heart 前面那一个是 new(造内部类对象);p.new 的 p 是外部对象引用,表示这颗心脏依附于 p 这个人存在——成员内部类对象必须挂在某个外部对象上,所以不能脱离 p 单独造。
编译器现场造了一个 Flyable 的无名实现类(自动 implements 并要求你补全所有抽象方法),立刻 new 出它的对象交给你。本质是多态:左边接口变量,右边无名实现类对象;只重写部分抽象方法会编译报错——合同规则不变。
实现只用一次且逻辑不长 → 匿名内部类(免起名免建文件);要用两次以上或逻辑复杂 → 正经类(可复用、可测试、可读)。判断标准就一条:复用次数。
Flyable f = new Flyable() {
@Override
public void fly() { System.out.println("扑棱扑棱"); }
};
省:不用建 Bird.java 文件、不用起类名,定义与使用一处完成。代价:这个实现没有名字,无法复用、无法单独测试,代码混在调用现场——所以只用一次才划算。
8. 下一步学什么
- ☑ 4.7 函数式编程 ✅ —— Lambda 表达式:函数式接口(单抽象方法)是入场券、三段式语法与三条省略规则、匿名内部类的瘦身版,4.6 结尾的伏笔在此收网。点此复习
相关笔记
- 4.5 Object类 ← 上一篇:Object 三件套,类设计的收尾
- 4.3 抽象类和接口 ← 匿名内部类的最佳搭档:接口/抽象类的现场兑现
- 4.4 多态 ← 匿名内部类本质是多态:编译看左,运行看右
- 3.7 static关键字 ← 静态内部类的规矩全从这里推
- 2.2 Java中的变量 ← 局部内部类的作用域规则同源
- 📇 Java基础 · 索引
- 🏠 返回主页