内部类(成员/静态/局部/匿名)

很多同学第一次看到"类里面还能写类"的时候,觉得这是多此一举。但当你真正读一些开源框架源码——Spring 的 CglibAopProxy、Guava 的 Maps 工具类、JDK 自身的 ArrayList 迭代器实现——你会发现内部类无处不在。

我自己写代码也经历过这个过程:从"内部类是什么"到"什么时候该用它"。今天把四种内部类的用法和坑点全部梳理清楚。

一、为什么需要内部类

内部类存在的根本原因:逻辑上紧密相关、只在一个地方使用的类,不需要单独成为一个文件

【直观类比】

想象一篇文章里的脚注。脚注和正文紧密相关,只在这篇文章里出现,不需要单独成册。但如果把脚注写在正文里,又会影响可读性。内部类就是这个"脚注"——在需要的地方写清楚,但又不会污染外部命名空间。

二、成员内部类

2.1 基本用法

成员内部类是定义在外部类的成员位置(字段或方法之间)的类:

public class Outer {
    private String outerField = "outer";

    // 成员内部类
    public class Inner {
        private String innerField = "inner";

        public void accessOuter() {
            // 可以直接访问外部类的所有成员(包括 private)
            System.out.println(outerField);
            System.out.println(Outer.this.outerField);  // 显式引用外部类实例
        }
    }
}

2.2 关键特性

  • 依附于外部类实例:必须有外部类对象,才能创建内部类对象
  • 可以访问外部类所有成员:包括 private
  • 编译产物Outer$Inner.class
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();  // 注意语法:outer.new Inner()
⚠️

创建成员内部类对象时,必须先有外部类实例:outer.new Inner(),而不是 new Inner()。这是因为内部类隐式持有外部类的引用。

三、静态内部类

3.1 基本用法

静态内部类和成员内部类的区别,只在一个关键字 static

public class Outer {
    private static String staticField = "static outer";
    private String instanceField = "instance outer";

    public static class StaticInner {
        public void access() {
            // 只能访问外部类的 static 成员
            System.out.println(staticField);
            // System.out.println(instanceField);  // 编译报错!
        }
    }
}

3.2 关键特性

  • 不依赖外部类实例:可以 new Outer.StaticInner() 直接创建
  • 只能访问外部类的 static 成员:不能访问实例字段
  • 编译产物Outer$StaticInner.class
  • 用途:如果内部类不需要访问外部类实例,就用 static,这样避免无谓的外部类引用
// 创建静态内部类,不需要外部类实例
Outer.StaticInner inner = new Outer.StaticInner();
💡

当你发现成员内部类从来不访问外部类实例时,应该改成静态内部类。这样更安全(不会意外持有外部类引用)、更高效(不需要创建外部类对象)。

四、局部内部类

4.1 基本用法

局部内部类定义在方法内部:

public class Outer {
    public void someMethod() {
        final int localVar = 100;  // effectively final

        // 局部内部类
        class LocalInner {
            public void print() {
                System.out.println("访问局部变量: " + localVar);
            }
        }

        LocalInner inner = new LocalInner();
        inner.print();
    }
}

4.2 关键特性

  • 作用域局限在方法内部:出了方法就不可见
  • 可以访问方法的局部变量:但必须是 final 或"有效 final"(JDK 8+)
  • 编译产物Outer$1LocalInner.class(带编号,因为类名不冲突)
public void someMethod() {
    int localVar = 100;  // effectively final in JDK 8+

    class LocalInner {
        public void print() {
            // localVar 在编译时被捕获为常量
            System.out.println(localVar);
        }
    }
    localVar = 200;  // 这里改了,局部内部类里看到的还是 100
    // 编译报错!localVar 被局部内部类引用后,不能再修改
}
⚠️

局部内部类引用的局部变量必须是 final 或 effectively final(JDK 8+)。原因:局部内部类实例可能比方法存活更久(被返回、存储到某处),如果局部变量在方法返回后消失,而内部类还在访问它,就会出问题。编译器通过"复制"变量的方式解决这个问题——把局部变量的值复制到内部类里,所以变量不能被修改。

五、匿名内部类

5.1 基本用法

匿名内部类没有名字,定义和实例化一起完成:

public interface Runnable {
    void run();
}

public class Outer {
    public void create() {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类实现");
            }
        };
        r.run();
    }
}

5.2 关键特性

  • 没有名字:类定义和实例化一起完成
  • 必须是接口或抽象类的实现:不能 new 一个具体类
  • 只能访问外部的 final 或 effectively final 变量
  • 编译产物Outer$1.classOuter$2.class(按定义顺序编号)
  • 常见用途:回调、事件监听、线程任务
// 常见用法:线程
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程任务");
    }
}).start();

// JDK 8 以后,可以用 Lambda 简化:
new Thread(() -> System.out.println("Lambda 写法")).start();
💡

匿名内部类的常见翻车点:过度使用。如果匿名内部类超过 20 行,考虑改成具名内部类或独立类,提高可读性。

六、对比总结

维度成员内部类静态内部类局部内部类匿名内部类
定义位置外部类成员位置外部类成员位置方法内部表达式内部
需要外部类实例方法调用时隐式持有方法调用时隐式持有
能访问外部类成员所有成员只能 static只能 static + 局部变量只能 static + 局部变量
能定义 static 成员
能访问局部变量不适用不适用必须 effectively final必须 effectively final
能被继承/实现其他类只能实现一个接口或继承一个类
命名空间Outer.InnerOuter.Inner无(方法内)
编译产物Outer$Inner.classOuter$StaticInner.classOuter$1LocalInner.classOuter$1.class

七、作用域与遮蔽

7.1 作用域

  • 成员内部类:整个外部类都可以访问
  • 静态内部类:同成员内部类
  • 局部内部类:仅在定义它的方法内
  • 匿名内部类:仅在定义它的表达式内

7.2 变量遮蔽

如果内部类和外部类有同名字段,内部类里默认访问的是自己的版本:

public class Outer {
    String field = "outer";

    public class Inner {
        String field = "inner";  // 遮蔽了外部类的 field

        public void print() {
            System.out.println(field);        // "inner" — 内部类自己的
            System.out.println(Outer.this.field);  // "outer" — 显式指定外部类
        }
    }
}

八、生产避坑

8.1 内存泄漏风险

成员内部类会隐式持有外部类引用。如果内部类里有长时间存活的对象(比如被 static 持有、被放入缓存),外部类对象就不会被 GC 回收:

// 错误写法:静态集合持有了成员内部类
public class Outer {
    private static List<Inner> cache = new ArrayList<>();  // 危险!

    public class Inner {
        String data;
    }
}

解决方案:使用静态内部类,它不持有外部类实例引用。

8.2 序列化问题

成员内部类和匿名内部类默认不能独立序列化(因为依赖外部类实例),如果需要序列化,考虑:

  1. 实现 Externalizable
  2. 将内部类改为 static
  3. 使用内部类作为独立的 DTO 类

【学习小结】

四种内部类的核心区别在于作用域和外部依赖。成员内部类持有外部类引用,适合需要访问外部类所有成员的场景;静态内部类不持有引用,适合工具类场景;局部内部类和匿名内部类作用域最小,适合一次性使用的回调和策略。生产中最容易踩的坑是:不需要外部引用时用了成员内部类,导致内存泄漏。