Skip to content

内部类

概述

内部类(Inner Class)是定义在另一个类内部的类。根据定义位置和是否带 static,Java 将内部类分为四类:成员内部类局部内部类匿名内部类静态内部类。内部类可以访问外部类的成员(包括 private),便于将逻辑上紧密相关的类型组织在一起,或实现回调、监听器等模式;同时内部类也会持有外部类实例的引用(静态内部类除外),使用时需注意生命周期与内存泄漏问题。

通俗理解:把「小类」写进「大类」里,小类能直接使用大类的数据和方法,代码更内聚;但不同种类的内部类适用场景不同,需要分清何时用哪种、何时用静态内部类避免多余引用。

前置建议

已掌握 类与对象封装与访问修饰符,理解实例成员与静态成员、this 的含义。


为什么需要内部类

  • 逻辑内聚:把只服务于当前外部类的类型放在内部,避免单独文件、命名空间更清晰(如 Node 只在链表类内使用)。
  • 访问外部成员:非静态内部类可直接访问外部类的实例成员(包括 private),无需额外传参,适合回调、迭代器、事件监听等。
  • 隐藏实现细节:内部类可以是 private,对外完全隐藏,只通过外部类提供的接口使用。
  • 多继承/多类型的补充:一个外部类可以有多个内部类,每个实现不同接口或继承不同类,间接实现「一个类多种形态」(配合 接口 常见于 GUI 事件、比较器等)。

内部类的四种类型

类型定义位置是否持有外部类引用典型用途
成员内部类类的成员位置迭代器、回调、与实例强关联
局部内部类方法或代码块内仅方法内使用的辅助类型
匿名内部类通常方法内,无类名一次性实现接口/抽象类
静态内部类类的成员位置 + static与外部类逻辑相关但独立于实例

下面按类型说明语法与示例。


成员内部类

成员内部类是作为外部类的成员直接定义的类,没有 static 修饰。每个成员内部类对象都会隐式持有外部类对象的引用,因此可以直接访问外部类的实例成员(包括 private);创建成员内部类对象时,必须先存在外部类对象。

基本语法

java
public class Outer {
    private String name = "Outer";

    // 成员内部类:写在类的成员位置,如同方法、字段
    public class Inner {
        public void say() {
            // 直接访问外部类的实例成员
            System.out.println("Inner sees: " + name);
        }
    }

    public void useInner() {
        Inner in = new Inner();  // 在外部类内部:直接 new Inner()
        in.say();
    }
}

在外部创建内部类对象

外部类之外创建成员内部类对象时,必须通过外部类实例new

java
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();  // 语法:外部类实例.new 内部类()
inner.say();   // Inner sees: Outer

注意

成员内部类不能定义 static 成员(除常量 static final),因为成员内部类本身依赖外部类实例,与「静态」语义冲突。若需要静态成员,请使用 静态内部类

成员内部类中的 this

在内部类方法中,this 默认指向内部类对象。若要访问外部类对象,使用 外部类名.this

java
public class Outer {
    int x = 10;

    public class Inner {
        int x = 20;

        void print() {
            System.out.println(x);           // 20,内部类的 x
            System.out.println(Outer.this.x); // 10,外部类的 x
        }
    }
}

局部内部类

局部内部类定义在方法或代码块内部,作用域仅限于该方法/块,外界无法引用该类型。局部内部类可以访问外部类的成员,也可以访问所在方法的 effectively final 的局部变量(或 JDK 8+ 的 final 或等效 final 的变量)。

基本语法与示例

java
public class Parser {
    private String prefix = "> ";

    public void parse(String path) {
        // 仅在此方法内需要的辅助类型
        class LocalHelper {
            void process(String line) {
                System.out.println(prefix + line);  // 可访问外部类成员
            }
        }
        LocalHelper helper = new LocalHelper();
        helper.process(path);
    }
}

提示

局部内部类适合「只在一个方法里用、且与外部类实例相关」的小型类型,便于把实现收拢在一处,避免污染类成员命名空间。


匿名内部类

匿名内部类没有类名,在 new 的同时实现接口继承(子类化)抽象类/具体类,常用于只需要一次性实现某个接口或抽象类的场景(如监听器、比较器、线程 Runnable)。

实现接口

java
// 接口
public interface OnClickListener {
    void onClick();
}

// 使用匿名内部类实现接口
public class Button {
    private OnClickListener listener;

    public void setOnClickListener(OnClickListener listener) {
        this.listener = listener;
    }

    public void click() {
        if (listener != null) listener.onClick();
    }
}

// 调用处:匿名内部类实现 OnClickListener
Button btn = new Button();
btn.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick() {
        System.out.println("Button clicked!");
    }
});
btn.click();   // Button clicked!

继承抽象类

java
abstract class Animal {
    abstract void speak();
}

// 匿名内部类继承 Animal,只使用一次
Animal a = new Animal() {
    @Override
    void speak() {
        System.out.println("Anonymous: Meow");
    }
};
a.speak();   // Anonymous: Meow

说明

JDK 8+ 中,很多「单抽象方法接口」可以用 Lambda 表达式 替代匿名内部类,代码更简洁。但匿名内部类可以同时实现接口并访问外部类的实例变量、或继承非接口类型,仍有很多使用场景。


静态内部类

静态内部类是用 static 修饰的、定义在类内部的类。它不持有外部类对象的引用,因此不能直接访问外部类的实例成员,只能访问外部类的 static 成员。静态内部类可以有自己的静态成员,创建对象时也不需要先有外部类实例。

基本语法与示例

java
public class Outer {
    private static int count = 0;
    private String name = "Outer";

    // 静态内部类:不依赖外部类实例
    public static class StaticInner {
        public void print() {
            System.out.println("count = " + count);  // 可访问外部类 static 成员
            // System.out.println(name);  // 编译错误:不能访问实例成员 name
        }
    }
}

// 创建静态内部类对象:不需要外部类实例
Outer.StaticInner si = new Outer.StaticInner();
si.print();

典型用途:与外部类逻辑相关但独立

例如链表节点只服务于链表,但节点本身不需要持有「链表对象」的引用,用静态内部类最合适:

java
public class MyLinkedList<E> {
    private Node head;

    // 节点只与链表相关,但不依赖链表实例的字段,用 static
    private static class Node<E> {
        E data;
        Node<E> next;
        Node(E data) { this.data = data; }
    }

    public void addFirst(E e) {
        Node<E> n = new Node<>(e);
        n.next = head;
        head = n;
    }
}

提示

若内部类不需要访问外部类的实例成员,优先使用 静态内部类,可避免隐式持有外部类引用,减少内存占用和潜在的内存泄漏(例如在集合中误把大对象通过内部类引用长期持有)。


注意事项

成员/局部/匿名内部类与外部类引用

非静态内部类会隐式持有外部类引用。在异步或长时间存活的对象(如监听器、线程)中使用时,若外部类本应被回收却被内部类引用,会导致内存泄漏。此时可考虑改为静态内部类 + 显式传入所需数据,或使用弱引用等。

重名与遮蔽

内部类中若定义了与外部类同名的成员,会遮蔽外部类成员;在内部类里访问外部类同名成员需写 外部类名.this.成员名(见上文成员内部类中的 this)。

序列化

包含非静态内部类的类在序列化时行为更复杂(内部类会持有外部类引用),一般建议将需要序列化的「内部」类型改为静态内部类或顶层类。


相关链接

基于 VitePress 构建