☕ 第四章 面向对象(下)

4.6 内部类

📄 一句话概括

内部类 = 类里面再定义类。它解决两件事:① 成员内部类能直接访问外部类的 private 成员——关系铁到能开保险柜;② 匿名内部类让"只用一次的实现"不用起名字——4.3 学的接口/抽象类,在这里迎来最爽的用法。

1. 为什么要把类塞进类里?

先给动机,不然内部类就是"语法炫技":

四兄弟一览(上面那张图):

类型 写在哪 关键规则 重点程度
成员内部类 类里、方法外 能访问外部一切成员(含 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
  → 类没名字,所以叫"匿名"——用完即弃,全项目就这一处

三个要点:

  1. 它本质是多态:Flyable f 左边是接口,右边是"没有名字的实现类"——4.4 的"编译看左运行看右"原样适用。
  2. 抽象类同样适用:new Animal() { ... } 现场补全所有抽象方法——"必须全兑现否则自己也 abstract"的规矩,匿名类用"当场兑现"绕过了"不能 new"的限制(其实是编译器现场造了个子类帮你 new)。
  3. 适用判断一条:这个实现只用一次就匿名;要用两次以上,老老实实建正经类。
💡 你以后天天会见到它

给按钮绑点击事件、给列表排个序、开个线程干点活——全是匿名内部类的主场。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 秒实验

  1. 内部类摸私房:把 Person 的 name 保持 private,Heart 里照样能访问;再把 Heart 挪出 Person 变成独立类——立刻编译报错"name 具有 private 访问权限"。一挪就懂"自己人"的含金量。
  2. 匿名类必须全兑现:Flyable 加一个 land() 方法,匿名类里只写 fly()——看编译报错"未覆盖抽象方法"。
  3. 静态内部类不碰实例成员:给静态内部类里写一句访问外部实例属性——看报错,3.7 的"对象可能还没出生"在内部类身上照样成立。

6. 常见误区

⚠️ 误区 1:内部类就是"把代码写在一起",没有实际意义

它换来两样实打实的东西:访问外部 private 的权限和更深的隐藏(外界甚至感知不到内部类存在)。关系紧密才用——能用普通类解决的别硬塞。

⚠️ 误区 2:匿名内部类"继承了接口"

说法不严谨:接口不能被继承(是 implements/实现),类才能 extends(继承)。匿名内部类对接口是"现场实现",对普通类/抽象类是"现场继承"。考试注意措辞。

⚠️ 误区 3:匿名内部类可以重复使用

每次 new 接口名() {...} 都是现场造一个新类 + 新对象——写三次就造三个不同的类。要用多次,请建正经类。

⚠️ 误区 4:静态内部类访问不了外部任何成员

访问不了的是实例成员(跟对象走的);外部类的静态成员它随便用——3.7 的访问规则表原样适用。

⚠️ 误区 5:成员内部类随便 new

成员内部类对象依附于外部对象存在——new Heart() 单独写会编译报错,必须 外部对象.new Heart() 或在外部类内部 new。"先有人,才有心脏"。

7. 自测题(先自己答,再展开)

❓ Q1:成员内部类为什么能访问外部类的 private 成员?普通类为什么不能?

private 的规矩是"类外免进",但成员内部类写在 Person 类里面,属于"自己人"——它是外部类身体的一部分,天然持有钥匙。普通类在 Person 类外,规矩照挡。

❓ Q2:`Person.Heart h = p.new Heart();` 这行里两个 new 位置各是什么意思?

只有 Heart 前面那一个是 new(造内部类对象);p.new 的 p 是外部对象引用,表示这颗心脏依附于 p 这个人存在——成员内部类对象必须挂在某个外部对象上,所以不能脱离 p 单独造。

❓ Q3:匿名内部类 `new Flyable() { ... }` 到底发生了什么?

编译器现场造了一个 Flyable 的无名实现类(自动 implements 并要求你补全所有抽象方法),立刻 new 出它的对象交给你。本质是多态:左边接口变量,右边无名实现类对象;只重写部分抽象方法会编译报错——合同规则不变。

❓ Q4:什么时候用匿名内部类,什么时候建正经类?

实现只用一次且逻辑不长 → 匿名内部类(免起名免建文件);要用两次以上或逻辑复杂 → 正经类(可复用、可测试、可读)。判断标准就一条:复用次数。

❓ Q5(毕业题):把 4.3 的 Flyable 用匿名内部类实现一次 fly(),并说出和"建 Bird 类"相比省了什么、代价是什么
Flyable f = new Flyable() {
    @Override
    public void fly() { System.out.println("扑棱扑棱"); }
};

省:不用建 Bird.java 文件、不用起类名,定义与使用一处完成。代价:这个实现没有名字,无法复用、无法单独测试,代码混在调用现场——所以只用一次才划算。

8. 下一步学什么


相关笔记