×

上篇我们聊了成员内部类和静态内部类,这篇搞定剩下的两个:局部内部类匿名内部类

这两种写法和前面最大的区别是什么?它们都在方法体内部定义——作用域被限制在了方法里,外面完全看不见。

一、局部内部类(Local Inner Class)

1.1 定义在方法里的类

局部内部类写起来就像这样:

public class Outer {

    public void sayHello() {
        // 局部内部类:定义在方法里面
        class LocalInner {
            private String name;

            public LocalInner(String name) {
                this.name = name;
            }

            public void greet() {
                System.out.println("你好," + name);
            }
        }

        // 只能在方法内使用
        LocalInner inner = new LocalInner("小明");
        inner.greet();
    }
}

看到了吧?LocalInner 类定义在了 sayHello() 方法里面。它的作用域仅限于这个方法——方法跑完了,外面谁也找不到它。

1.2 局部内部类的特性

  • 作用域仅限于所在的方法,方法外完全不可见
  • 可以访问外部类的所有成员(跟成员内部类一样)
  • 可以访问方法内的 final有效 final 的局部变量
  • Java 8+ 要求局部变量是「有效 final」(即变量值不改变,即使没写 final 关键字)

来看一个具体的:

public class Outer {
    private int num = 100;

    public void process() {
        int localNum = 50;  // 有效 final——值没有改变

        class Processor {
            public void print() {
                // 访问外部类成员 ✅
                System.out.println("外部类成员 num = " + num);
                // 访问方法内的局部变量 ✅(必须是有效 final)
                System.out.println("局部变量 localNum = " + localNum);
            }
        }

        new Processor().print();
    }
}

那为什么局部变量必须是 final 或有效 final?原因跟内存有关——方法执行完栈帧就销毁了,但内部类对象可能还在堆里活着。Java 其实把局部变量的值复制了一份到内部类对象中,如果这个变量还能被修改,就会出现「复制品跟原件不一致」的 bug。所以干脆只让内部类访问不可变的变量,从根源上避免问题。

1.3 什么时候用局部内部类?

说实话,局部内部类在日常写业务代码时用得不多。最典型的场景是:你有一个方法,里面需要创建一个稍微复杂一点的类来完成某个局部任务,但这个类没啥复用价值,写在外面又显得多余。

比如你写一个读取文件的工具方法,里面需要一个临时数据结构:

public void readConfig() {
    // 局部内部类:只在读取配置时临时用一下
    class ConfigField {
        String key;
        String value;
        int lineNumber;

        ConfigField(String key, String value, int lineNumber) {
            this.key = key;
            this.value = value;
            this.lineNumber = lineNumber;
        }
    }

    // 解析过程...
    List<ConfigField> fields = parseLines();
    // ...
}

但这种场景……说实话直接用 Map 或者 Record(Java 16+)就完事了。所以局部内部类我个人的感觉是:知道有这么个东西就行,面试不考,实战用得少

二、匿名内部类(Anonymous Inner Class)

这个就不一样了——匿名内部类在实际开发中非常常见

匿名内部类,顾名思义,没有名字的内部类。它在声明的同时直接创建对象,而且只能使用一次。

2.1 基本语法

// 写法:
new 父类或接口() {
    // 实现或重写的方法
};

来看具体的例子。假设有一个 Animal 接口:

interface Animal {
    void speak();
}

// 以前你得这么写:
class Dog implements Animal {
    @Override
    public void speak() {
        System.out.println("汪汪汪");
    }
}
Animal dog = new Dog();

// 匿名内部类版本:
Animal cat = new Animal() {
    @Override
    public void speak() {
        System.out.println("喵喵喵");
    }
};

看到了吧?不用单独定义一个 Cat 类,直接 new Animal() { ... } 就搞定了一个「匿名的 Animal 实现」。这个类没有名字,只在这里用一次。

2.2 匿名内部类 + 抽象类

不仅是接口,抽象类也可以:

abstract class Logger {
    public abstract void log(String msg);
}

public class App {
    public static void main(String[] args) {
        // 匿名内部类实现抽象类
        Logger logger = new Logger() {
            @Override
            public void log(String msg) {
                System.out.println("[LOG] " + msg);
            }
        };

        logger.log("应用启动了");
    }
}

2.3 匿名内部类与回调

匿名内部类最经典的应用就是 回调(Callback)。你写一个工具方法,需要让调用者决定某个步骤怎么做——这时候传一个匿名内部类进去就非常自然。

// 定义一个回调接口
interface Callback {
    void onSuccess(String result);
    void onFailure(String error);
}

// 模拟一个网络请求工具类
class HttpUtil {
    public static void get(String url, Callback callback) {
        if (url == null || url.isEmpty()) {
            callback.onFailure("URL 不能为空");
            return;
        }
        // 模拟请求成功
        System.out.println("正在请求: " + url);
        callback.onSuccess("响应结果: 200 OK");
    }
}

// 使用匿名内部类做回调
public class Test {
    public static void main(String[] args) {
        HttpUtil.get("https://api.example.com/user", new Callback() {
            @Override
            public void onSuccess(String result) {
                System.out.println("✅ 成功: " + result);
            }

            @Override
            public void onFailure(String error) {
                System.out.println("❌ 失败: " + error);
            }
        });
    }
}

这个写法眼熟不?学 Android 开发的更是天天见——Button.setOnClickListener(new OnClickListener() { ... }) 就是标准的匿名内部类。

2.4 与 Lambda 表达式的关系

Java 8 引入 Lambda 表达式后,如果一个接口只有一个抽象方法(即函数式接口,用 @FunctionalInterface 标记),就可以用 Lambda 替代匿名内部类:

// 匿名内部类方式
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("线程运行中...");
    }
};

// Lambda 表达式方式
Runnable r2 = () -> System.out.println("线程运行中...");

Lambda 比匿名内部类更简洁、可读性更强,而且 Java 对 Lambda 做了性能优化(invokedynamic 指令),不会像匿名内部类那样每次调用都生成一个 .class 文件。

所以现在写代码:能用 Lambda 的就别用匿名内部类。但 Lambda 不能覆盖所有情况——当接口有多个抽象方法,或者你需要 this 指向内部类对象而不是外部的时候,匿名内部类还是得用。

三、完整对比:四种内部类

类型定义位置持有外部类引用能否定义静态成员使用频率
成员内部类类中⭐⭐⭐
静态内部类类中(static)⭐⭐⭐⭐
局部内部类方法内
匿名内部类方法内(表达式)⭐⭐⭐

四、内部类的一些”坑”

4.1 内存泄漏风险

成员内部类持有外部类引用,这听起来方便,但暗藏风险:如果内部类对象生命周期比外部类长,外部类就无法被 GC 回收。

public class BigDataProcessor {
    private byte[] hugeData = new byte[1024 * 1024 * 100]; // 100MB

    public class Processor {
        public void process() {
            System.out.println("处理数据中...");
        }
    }

    public Processor getProcessor() {
        return new Processor();  // 返回内部类对象
    }
}

// 使用
BigDataProcessor bdp = new BigDataProcessor();
BigDataProcessor.Processor p = bdp.getProcessor();
bdp = null;  // 以为 bdp 可以回收了?

// 但实际上 p 还持有 bdp 的引用,100MB 内存释放不了!

这就是经典的内存泄漏场景。解决方案?如果内部类不需要访问外部类的实例成员,就声明为 静态内部类——这样就不持有外部引用,不会有泄漏问题。

4.2 序列化问题

内部类默认不实现 Serializable,而且编译器生成的构造器和字段名是不可预测的。如果你需要序列化内部类对象,最好改成静态内部类,或者干脆提取成顶层类。

五、小结

  • 局部内部类定义在方法体里,作用域局限在方法内,只能用有效 final 的局部变量
  • 匿名内部类没有名字,声明即创建对象,常用于回调、事件监听、一次性实现
  • 匿名内部类可以用 Lambda(函数式接口)替代,更简洁
  • 成员内部类注意内存泄漏——不需要访问外部实例成员时用静态内部类
  • 四种内部类的 .class 文件命名:Outer$类型名.class

内部类就到这里了,两篇讲完。下一篇我们开始聊 Object 类——它是所有类的老祖宗,toString()equals()hashCode() 这些天天打交道的方法,你真的搞懂了吗?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

作者

2248768396@qq.com

相关文章