Skip to content

第10课:内部类

🎯 学习目标

  • 理解为什么需要内部类(封装、逻辑分组、访问外部私有、回调闭包)
  • 掌握 4 种内部类:成员、静态、局部、匿名,及各自的语法与使用场景
  • 理解内部类的编译产物与 this 引用机制
  • 识别内部类的常见陷阱(effectively final、内存泄漏、static 限制)
  • 能够用内部类实现迭代器、回调等真实场景

📖 一、概念讲解:为什么需要内部类

1. 内部类是什么

内部类(Inner Class) 是定义在另一个类内部的类。它不是 Java 独有的概念,但 Java 把它做成了「真正的类」——有自己的 .class 文件、可以访问外部类的私有成员、与外部类形成一种强耦合的协作关系。

java
public class Outer {          // 外部类
    private int x = 10;

    class Inner {              // 内部类
        void show() {
            System.out.println(x);  // 直接访问外部类私有成员
        }
    }
}

2. 为什么需要内部类

如果不理解「为什么」,4 种内部类的语法就是一堆死规则。内部类解决的是这几个问题:

需求不用内部类用内部类
一个类只服务于另一个类,不该对外暴露写成顶层 public 类,谁都能用写成内部类,封装起来
需要访问外部类的私有成员要么开放访问器破坏封装,要么手动传引用自动拥有访问权
一次性使用的回调/事件处理写一个命名的顶层类,只为用一次匿名内部类,就地定义
实现迭代器、视图等「视图类」视图类与数据类分离,耦合靠约定视图类自然持有数据类引用

一句话:内部类 = 「只为外部类服务」+「需要访问外部私有」+「可能只一次性使用」的类。

3. 编译产物

内部类编译后会生成独立的 .class 文件,命名规律是 Outer$Inner.class

Outer.java                    源文件
  ↓ javac
Outer.class                   外部类
Outer$Inner.class             成员内部类
Outer$1.class                 匿名内部类(按出现顺序编号)

这说明内部类在编译后是独立的类,JVM 层面并没有「内部」的概念——「内部」是编译期的语法糖,编译器通过注入额外的访问方法(如 access$0)让内部类能访问外部私有成员。


📖 二、四种内部类详解

1. 成员内部类(Member Inner Class)

定义在外部类中、与方法平级。持有外部类实例的引用,因此能访问外部类的实例成员。

java
public class Outer {
    private int outerField = 10;

    // 成员内部类
    public class Inner {
        private int innerField = 20;

        public void display() {
            System.out.println("外部类实例字段: " + outerField);   // 访问外部私有
            System.out.println("内部类字段: " + innerField);
        }
    }
}

// 使用:必须先有外部类实例
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();   // 注意 outer.new 语法
inner.display();

关键点:创建成员内部类实例需要外部类实例(outer.new Inner()),因为它隐含持有 outer 的引用。

this 引用机制

成员内部类里有两个 this:自己的 this 和外部类的 Outer.this

java
public class Outer {
    class Inner {
        public void show() {
            System.out.println(this);          // Inner 实例
            System.out.println(Outer.this);    // 外部 Outer 实例
        }
    }
}

2. 静态内部类(Static Nested Class)

static 修饰。不持有外部类实例的引用,类似一个放在外部类「命名空间」里的普通类。

java
public class Outer {
    private static int staticField = 10;
    private int instanceField = 20;

    // 静态内部类
    public static class StaticInner {
        public void display() {
            System.out.println(staticField);     // ✅ 可访问外部静态成员
            // System.out.println(instanceField); // ❌ 不能访问实例成员(没有外部实例)
        }
    }
}

// 使用:不需要外部类实例
Outer.StaticInner inner = new Outer.StaticInner();
inner.display();

关键点:创建静态内部类实例不需要外部类实例(new Outer.StaticInner())。它是最常用的内部类形式——HashMap 的 Node、ArrayList 的 Itr 都是静态内部类。

3. 局部内部类(Local Inner Class)

定义在方法内部,作用域限于该方法。

java
public class Outer {
    public Runnable createTask() {
        final int localVar = 30;          // 方法局部变量

        // 局部内部类:只在方法内可见
        class LocalInner implements Runnable {
            @Override
            public void run() {
                // 可访问外部实例成员 + 方法的 final/effectively final 局部变量
                System.out.println("localVar = " + localVar);
            }
        }
        return new LocalInner();          // 返回出去,闭包带走 localVar
    }
}

关键点:局部内部类能访问方法的局部变量,但该变量必须是 final 或 effectively final(见陷阱1)。它适合「只想在某个方法内用一次、但又需要多次实例化」的场景。

4. 匿名内部类(Anonymous Inner Class)

没有名字,就地定义并实例化,常用于一次性回调。

java
// 实现接口
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("匿名内部类实现接口");
    }
};

// 继承类
Animal a = new Animal() {
    @Override
    public void eat() {
        System.out.println("匿名内部类重写方法");
    }
};

关键点:匿名内部类只能用一次(创建时同时实例化),不能有显式构造方法(没有名字就没法写构造器),常用于事件监听、回调、线程任务。JDK 8+ 若接口是函数式接口(只有一个抽象方法),可用 Lambda 替代。


⚠️ 三、常见陷阱

陷阱1:访问局部变量必须 effectively final

java
public void method() {
    int count = 0;
    Runnable r = new Runnable() {
        @Override
        public void run() {
            // count = 5;  // ❌ 编译错误:匿名内部类捕获的局部变量必须 final
            System.out.println(count);  // ✅ 只读可以
        }
    };
}

为什么:匿名/局部内部类在方法返回后仍可能被调用(如回调),此时方法的栈帧已销毁,局部变量已不存在。编译器把捕获的变量拷贝一份到内部类实例里。如果允许修改,就会出现「内部类里的拷贝」与「外部变量」不一致的歧义。所以要求 final/effectively final——本质是保证拷贝语义清晰。

陷阱2:成员内部类持有外部引用,易致内存泄漏

java
public class Cache {
    private Object data;

    public Runnable getTask() {
        // 成员内部类隐含持有外部 Cache 实例
        return new Runnable() {
            @Override
            public void run() { System.out.println(Cache.this.data); }
        };
    }
}
// 若这个 Runnable 被长期持有,外部 Cache 实例也无法回收 → 内存泄漏

规避:不需要访问外部实例时,优先用静态内部类(不持有外部引用)或 Lambda。

陷阱3:成员内部类不能有 static 成员

java
public class Outer {
    class Inner {
        // static int x = 1;          // ❌ 编译错误(JDK 16 前)
        // static final int N = 1;    // ✅ compile-time constant 例外
    }
    static class StaticInner {
        static int x = 1;            // ✅ 静态内部类可以有 static 成员
    }
}

为什么:成员内部类依赖外部实例,而 static 成员应在类加载时就可用,二者矛盾。(JDK 16+ 放宽了此限制,但旧代码仍需注意。)

陷阱4:this 歧义

java
public class Outer {
    class Inner {
        int x = 1;
        public void show() {
            int x = this.x;          // Inner 的 x
            // 想拿 Outer 的实例:必须 Outer.this,不能直接 this
        }
    }
}

陷阱5:匿名内部类与 Lambda 的 this 指向不同

java
public class Outer {
    public void test() {
        // 匿名内部类:this 指向匿名类实例
        Runnable r1 = new Runnable() {
            public void run() { System.out.println(this.getClass()); }  // Outer$1
        };
        // Lambda:this 指向外部类实例(不产生新作用域)
        Runnable r2 = () -> System.out.println(this.getClass());        // Outer
        r1.run(); r2.run();
    }
}

为什么:匿名内部类会创建一个新的类实例,this 绑定到它;Lambda 不引入新作用域,this 绑定到外围实例。把 Lambda 当匿名内部类用 this 时容易踩坑。


🆚 四、Java vs C 对比

特性C 语言Java
内部类型无类概念;可用嵌套 struct,但 struct 无方法内部类是真正的类,有方法、可继承、可多态
访问外部私有C 无法直接访问另一结构体私有内部类自动可访问外部私有成员
回调机制函数指针 + 手动传 void* 上下文匿名内部类 / Lambda 自动捕获上下文
封装靠 static 函数 + 文件作用域内部类天然封装在外部类命名空间内
迭代器视图手动传数据指针成员内部类持有数据类引用,自然实现

对 C 程序员的理解方式:把成员内部类想象成「C 里一个持有外部结构体指针 + 能访问其所有字段的辅助函数集合」,而匿名内部类想象成「带捕获上下文的函数指针」。区别是 Java 把它对象化了,能有多态和状态。


💡 五、最佳实践

  1. 优先静态内部类:不需要访问外部实例时,用 static 修饰,避免持有外部引用(内存与性能都更优)。这是 Effective Java 的明确建议。
  2. 函数式接口用 Lambda 替代匿名内部类:JDK 8+ 更简洁。但 Lambda 无法定义额外方法/状态时,仍用匿名内部类。
  3. 内部类只服务于外部类:如果一个类会被多处独立使用,它不该是内部类。
  4. 视图类用成员内部类:迭代器、不可变视图等持有数据类引用的辅助类,是成员内部类的最佳场景。
  5. 注意匿名内部类的变量捕获:捕获的局部变量保持 effectively final,避免在 Lambda/匿名类内意外修改。

🧪 六、实战案例:Builder 静态内部类

静态内部类常用于 Builder 模式。

java
public class User {
    private final String name;
    private final int age;

    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.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;
        }

        public User build() {
            return new User(this);
        }
    }
}

使用:

java
User user = new User.Builder()
    .name("Alice")
    .age(20)
    .build();

为什么 Builder 适合静态内部类?

text
Builder 只服务于 User。
Builder 不需要持有某个 User 实例。
静态内部类不会意外持有外部对象引用。
命名上天然归属 User。

🛠 七、内部类选择清单

选择哪一种内部类:

text
需要访问外部实例成员:成员内部类。
不需要访问外部实例,只是逻辑归属外部类:静态内部类。
只在一个方法内部使用:局部内部类。
一次性实现接口或抽象类:匿名内部类。
函数式接口的一次性实现:优先 Lambda。

避免:

text
成员内部类被长生命周期对象持有。
匿名内部类里写大量复杂逻辑。
内部类被多个外部模块直接依赖。
为了“少建文件”把无关类塞成内部类。

✅ 八、掌握标准

学完本课后,应能做到:

text
能区分成员内部类、静态内部类、局部内部类、匿名内部类。
能解释成员内部类为什么持有外部类引用。
能使用 Outer.this 访问外部对象。
能说明匿名内部类和 Lambda 的 this 差异。
能解释 effectively final 的原因。
能用静态内部类实现 Builder。
能判断内部类是否会造成内存泄漏。

内部类不是为了让代码“嵌套得更深”,而是为了表达强归属关系、回调封装和局部实现。


📝 练习预告

完成 练习/Ex10_InternalClass.java 中的 6 道题:

  1. 成员内部类访问外部私有字段
  2. 静态内部类的构建与使用(对比成员内部类)
  3. 匿名内部类实现接口
  4. 局部内部类与变量捕获
  5. effectively final 陷阱识别
  6. 综合:用内部类实现自定义迭代器

完成后对比 答案/Sol10.java,查看逐行讲解与多解法。


🎓 下一步

  • 第11课:枚举 — enum 的本质、枚举方法、EnumSet/EnumMap、枚举单例