内部类分类与区别

面试官问:"Java 有哪几种内部类?"

候选人小宋答:"有静态内部类和非静态内部类,还有匿名内部类。"

面试官追问:"还有呢?"

小宋:"...成员内部类?"

面试官写了一段代码:

class Outer {
    class Inner { }              // 1
    static class StaticInner { } // 2

    void method() {
        class LocalInner { }     // 3
        Runnable r = new Runnable() { // 4
            public void run() { }
        };
    }
}

"这四种内部类有什么区别?"

小宋答不上来。

【面试官心理】 这道题看似简单,但四种内部类的区别是 Java 基础中的进阶点。能说出"是否持有外部类引用"和"是否独立编译"的候选人,说明真正理解了内部类的设计意图。

一、四种内部类分类 🔴

类型语法位置特点
成员内部类class Inner外部类成员级别依赖外部类实例
静态内部类static class Inner外部类成员级别不依赖外部类实例
局部内部类class Inner方法内部作用域在方法内
匿名内部类new Runnable() { }方法内部没有名字,简洁的一次性使用

二、成员内部类 🔴

2.1 基本语法

class Outer {
    private int outerField = 10;

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

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

// 使用
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner(); // 通过外部类引用创建

2.2 核心特性:持有外部类引用

class Outer {
    int outerData = 10;

    class Inner {
        int innerData = 20;

        // 编译器会生成:
        // final Outer this$0; // 隐式持有外部类引用

        void access() {
            // 编译器展开为:
            System.out.println(this$0.outerData);
        }
    }
}

重要:非静态内部类持有外部类引用,这意味着:

  • 内部类对象依赖于外部类对象存在
  • 一个外部类可以有多个内部类实例,每个都持有同一个外部类引用

三、静态内部类 🔴

3.1 基本语法

class Outer {
    static int staticField = 10;

    // 静态内部类
    static class StaticInner {
        int innerData = 20;

        public void access() {
            // ✅ 可以访问外部类的静态成员
            System.out.println(staticField);
            // ❌ 不能访问外部类的实例成员
            // System.out.println(outerData); // 编译错误
        }
    }
}

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

3.2 与成员内部类的核心区别

维度成员内部类静态内部类
外部类引用持有(隐式 Outer.this不持有
创建方式outer.new Inner()new Outer.Inner()
访问外部类成员可以访问所有成员(静态+实例)只能访问静态成员
独立编译否(编译器生成 Outer$Inner.class是(编译器生成 Outer$StaticInner.class,但包含外部类引用)
内存泄漏风险有(持有外部类引用)
💡

能用静态内部类就不用成员内部类。成员内部类持有外部类引用,如果被外部持有引用(如作为回调),可能导致外部类无法被 GC 回收,产生内存泄漏。Android 开发中这个问题尤为突出。

四、局部内部类 🔴

4.1 基本语法

class Outer {
    void method() {
        final int localVar = 10; // JDK 8 之前必须是 final

        class LocalInner {
            public void access() {
                // ✅ 可以访问方法的局部变量(JDK 8+ 不要求显式 final,但必须是 effectively final)
                System.out.println(localVar);
            }
        }

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

4.2 作用域限制

局部内部类只在定义它的方法内可见,其他方法无法访问。

五、匿名内部类 🔴

5.1 基本语法

// 常见用法:线程创建
Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Running");
    }
});
t.start();

// JDK 8+ Lambda 简化(Runnable 接口只有一个抽象方法)
Thread t2 = new Thread(() -> System.out.println("Running"));

5.2 匿名内部类的限制

// ❌ 匿名内部类不能有构造器
new MyClass() {
    // public MyClass() { } // 编译错误
    private int data = 0;
    @Override
    void method() { }
};

// ✅ 如果需要初始值,使用实例初始化块
new MyClass() {
    private int data;
    {
        // 实例初始化块,等价于构造器
        this.data = 10;
    }
};

5.3 匿名内部类的继承意义

// 匿名内部类实际上是在创建一个继承/实现某个类/接口的子类
// 以下两种写法等价:

// 匿名内部类
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

// Lambda 表达式(JDK 8+,只适用于函数式接口)
Collections.sort(list, (a, b) -> a.compareTo(b));

六、面试追问链

面试官:"内部类编译后生成几个 class 文件?"

class Outer {
    class Inner { }
    static class StaticInner { }
    void method() {
        class LocalInner { }
        Runnable r = new Runnable() { };
    }
}

// 编译后生成 5 个 class 文件:
// 1. Outer.class
// 2. Outer$Inner.class      (成员内部类)
// 3. Outer$StaticInner.class (静态内部类)
// 4. Outer$1LocalInner.class (局部内部类,编译器生成编号)
// 5. Outer$1.class          (匿名内部类)

面试官:"局部内部类和匿名内部类有什么区别?"

// 局部内部类:有名字,可以重复使用
class LocalInner { }

// 匿名内部类:没有名字,一次性使用
new Runnable() { };

【面试官心理】 问"生成几个 class 文件"能直接测试候选人对编译器的了解程度。能说出 $ 分隔符和编号规则的候选人,说明研究过 class 文件结构。