静态内部类与非静态内部类

面试官问:"静态内部类和非静态内部类有什么区别?"

候选人小雷答:"静态内部类用 static 修饰,不依赖于外部类实例;非静态内部类需要外部类对象。"

面试官点点头,继续追问:"为什么非静态内部类不能定义 static 字段?"

小雷愣住了。

【面试官心理】 追问 static 字段的限制,是在测候选人对内部类编译实现的理解程度。能说出"非静态内部类在实例层面,而非静态字段属于类层面"的候选人,是真正理解了 Java 内存模型的。

一、核心区别对比表 🔴

维度非静态内部类静态内部类
修饰符static
外部类引用持有(隐式 Outer.this不持有
创建方式outer.new Inner()new Outer.Inner()
外部类成员访问所有成员(静态+实例)仅静态成员
静态字段❌ 不能有(除常量)✅ 可以
独立存在否,依赖外部类对象是,可以独立使用
内存泄漏风险

二、编译后的差异 🔴

2.1 非静态内部类的编译器处理

// 源码
class Outer {
    int outerField = 10;
    class Inner {
        int innerField = 20;
        void access() {
            System.out.println(outerField);
        }
    }
}

// 编译器生成的字节码(Outer$Inner.class)
// 编译器自动添加外部类引用:
class Outer$Inner {
    // 编译器生成:持有外部类的引用
    final Outer this$0;

    Outer$Inner(Outer outer) {
        this.this$0 = outer;
    }

    void access() {
        System.out.println(this$0.outerField);
    }
}

2.2 静态内部类的编译器处理

// 源码
class Outer {
    static int staticField = 10;
    static class StaticInner {
        int innerField = 20;
        void access() {
            System.out.println(staticField); // 直接访问
        }
    }
}

// 编译后(Outer$StaticInner.class)
// 就是一个普通的 final 类,没有外部类引用
class Outer$StaticInner {
    // 没有外部类引用字段
    void access() {
        System.out.println(Outer.staticField);
    }
}

三、为什么非静态内部类不能有 static 字段?🔴

3.1 原因分析

class Outer {
    class Inner {
        static int CONST = 100; // ✅ 常量可以(有 final 修饰)
        static int counter;     // ❌ 编译错误
    }
}

为什么不能有 static 非 final 字段?

  1. static 字段属于(Class 对象),而不是实例
  2. 非静态内部类属于实例(持有外部类引用)
  3. 如果允许定义 static 字段,语义混乱:应该通过哪个外部类实例来访问这个 static 字段?
// 如果允许,语义是什么?
Outer o1 = new Outer();
Outer o2 = new Outer();
Outer.Inner.counter; // o1 的 counter 还是 o2 的 counter?

// 明确语义:
Outer.Inner staticField; // 类级别,和任何实例无关
o1.new Inner().counter;  // 实例级别,但 staticField 却是类级别?

3.2 为什么常量可以?

static final int CONST = 100;
// 常量在编译期就被内联了,不占用类加载时的存储空间
// 没有运行时状态,不需要区分"属于哪个实例"

四、非静态内部类的内存泄漏风险 🔴

4.1 典型泄漏场景

class Outer {
    class HeavyInner {
        byte[] hugeData = new byte[10 * 1024 * 1024]; // 10MB
    }

    void startAsync() {
        // 场景:异步回调持有内部类引用
        executor.submit(new HeavyInner() {
            @Override
            public void run() {
                // 异步执行,内部类对象被 executor 持有
                // 而内部类对象持有外部类引用
                // → 外部类无法被 GC 回收
            }
        });
    }
}
⚠️

非静态内部类导致的内存泄漏

  1. Handler 持有 Activity 引用 → Activity 无法销毁
  2. 回调对象持有内部类 → 外部类无法 GC
  3. 集合持有内部类迭代器/回调

解决方案:将非静态内部类改为静态内部类 + WeakReference。

4.2 正确的做法

class Outer {
    // ✅ 静态内部类:不在实例层面,不持有外部类引用
    static class HeavyInner {
        byte[] hugeData = new byte[10 * 1024 * 1024];
    }

    void startAsync() {
        // 静态内部类不持有外部类引用
        // 但如果需要访问外部类实例,通过显式参数传入
        executor.submit(() -> process(new WeakReference<>(this)));
    }
}

五、静态内部类的典型应用 🔴

5.1 单例模式(双重检查锁)

public class Singleton {
    // 静态内部类实现延迟加载 + 线程安全
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }

    private Singleton() { }
}

原理:Holder 是静态内部类,只有在 getInstance() 被调用时才会加载,而类加载是线程安全的。

5.2 建造者模式

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;

    public static class Builder {
        private int servingSize = 0;
        private int servings = 0;
        private int calories = 0;

        public Builder servingSize(int val) {
            servingSize = val; return this;
        }
        public Builder servings(int val) {
            servings = val; return this;
        }
        public Builder calories(int val) {
            calories = val; return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
    }
}

// 使用
NutritionFacts cocaCola = new NutritionFacts.Builder()
    .servingSize(240).servings(8).calories(100).build();

5.3 Map.Entry

// JDK 中 Map 的 Entry 接口经常用静态内部类实现
public interface Map<K, V> {
    interface Entry<K, V> {
        K getKey();
        V getValue();
        // ...
    }
}

// HashMap 的实现:
public class HashMap<K, V> {
    static class Node<K, V> implements Entry<K, V> {
        final int hash;
        final K key;
        V value;
        Node<K, V> next;
    }
}

HashMap 用静态内部类 Node 来表示键值对,因为 Node 不需要访问 HashMap 的实例字段(HashMap 的实例字段是 tablesize 等,Node 不需要),用静态内部类避免了持有外部类引用。

六、追问升级

面试官:"HashMap 的 Entry 为什么用静态内部类而不是非静态?"

// HashMap 的关键字段:
public class HashMap<K, V> {
    transient Node<K, V>[] table; // 哈希桶数组
    transient int size;
    // ...
}

// Node(键值对节点):
static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;
    // ...
}

// 如果用非静态内部类:
// 每个 Node 都持有 HashMap 引用 → 内存浪费
// 当 HashMap 扩容重新 hash 时,大量 Node 对象被创建/销毁
// 持有无用的引用 → GC 压力增大

答案:节省内存 + 避免无意义的引用持有。Node 是纯数据结构,不需要访问 HashMap 实例。

【面试官心理】 能说出 HashMap Node 用静态内部类原因的候选人,说明对 JDK 源码有过深入阅读,并且理解了"静态内部类节省内存"的设计意图。这是 P6+ 的加分项。