1. 概述

本文将介绍如何在 Kotlin 中使用对象表达式(object expression)来创建匿名内部类。

我们会先了解 Kotlin 创建匿名内部类的语法,然后深入字节码层面,看看背后的实现机制。对于有 Java 背景的开发者来说,这部分内容能帮助你更好地理解 Kotlin 与 JVM 的交互方式,避免一些“踩坑”场景。

2. 匿名内部类的创建

在 Java 中,我们通过 new ClassName() { ... } 的语法创建匿名内部类。例如,为 NIO 的 Channel 接口创建实现:

Channel channel = new Channel() {
            
    @Override
    public boolean isOpen() {
        return false;
    }

    @Override
    public void close() throws IOException {
        // omitted
    }
};

而在 Kotlin 中,必须使用 object : 语法来创建匿名内部类,也就是所谓的“对象表达式”。上面的代码在 Kotlin 中等价写法如下:

val channel = object : Channel {
    override fun isOpen() = false

    override fun close() {
        // omitted
    }
}

✅ 关键点:

  • 不再使用 new 关键字
  • 使用 object : Supertype 开头
  • 如果父类有构造函数,必须传参

构造函数参数传递

当继承的类带有构造参数时,需在冒号后直接调用其构造函数:

val maxEntries = 10000
val lruCache = object : LinkedHashMap<String, Int>(50, 0.75f) {
    override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Int>?): Boolean {
        return size > maxEntries
    }
}

⚠️ 注意:即使父类构造函数无参,括号也不能省略:

val map = object : LinkedHashMap<String, Int>() {
    // 正确:空括号不能少
}

❌ 错误写法(编译不通过):

val map = object : LinkedHashMap<String, Int> { ... }

多重继承支持

Kotlin 允许一个匿名类同时继承一个类并实现多个接口,或仅实现多个接口:

val serializableChannel = object : Channel, Serializable {
    // 实现 Channel 方法 + Serializable 标记接口
}

无超类型的对象表达式

更灵活的是,Kotlin 支持创建不继承任何类或接口的匿名对象:

val obj = object {
    val question = "answer"
    val answer = 42
}
println("The ${obj.question} is ${obj.answer}")

这种写法常用于临时封装数据,作用类似于轻量级 DTO,但仅限于当前作用域使用。

3. 字节码层面分析

要想查看 Kotlin 编译后的字节码,可以使用标准工具链流程。

编译与反编译步骤

假设源文件名为 Anonymous.kt,执行以下命令进行编译:

$ kotlinc Anonymous.kt

生成 .class 文件后,使用 javap 查看字节码细节:

$ javap -c -p -v AnonymousKt

字节码结构解析

channel 变量为例,其字节码片段如下:

0: new           #11            // class AnonymousKt$main$channel$1
3: dup
4: invokespecial #14           // Method AnonymousKt$main$channel$1."<init>":()V
// omitted
InnerClasses:
  public static final #11;    // class AnonymousKt$main$channel$1

关键信息解读:

✅ Kotlin 编译器会为每个对象表达式生成一个静态内部类,命名格式为:
OuterClass$functionName$variableName$index

例如:AnonymousKt$main$channel$1

✅ 随后通过 new + <init> 调用构造器实例化该类,行为与 Java 完全一致。

带参数和闭包的情况

当匿名类引用了外部变量(如 maxEntries),字节码会将其作为构造参数传递,实现“闭包”效果:

val maxEntries = 10
val lruCache = object : LinkedHashMap<String, Int>(10, 0.75f) {
    override fun removeEldestEntry(...): Boolean {
        return size > maxEntries  // 引用了外部变量
    }
}

对应字节码片段:

16: bipush        10   // capacity
18: ldc           #17  // float 0.75f
20: invokespecial #20  // Method ..."<init>":(IIF)V

其中 (IIF) 表示构造函数接受两个 int 和一个 float:

  • 第一个 int:maxEntries(被捕获的外部变量)
  • 第二个 int:10(LinkedHashMap 容量)
  • float:0.75f(加载因子)

✅ 这说明 Kotlin 的匿名内部类支持变量捕获(closure),机制上与 Java 类似,但更简洁。

核心结论

⚠️ Kotlin 编译器会将所有对象表达式翻译成静态内部类,并通过构造函数传参实现对外部变量的访问。
这意味着:

  • 不会造成非静态内部类常见的内存泄漏问题(无需隐式持有外层实例引用)
  • 性能开销可控,接近原生 Java 实现
  • 可以放心在循环或高频调用中使用,只要注意对象生命周期即可

4. 总结

通过本篇内容,我们掌握了:

  • ✅ 如何使用 object : 语法创建匿名内部类
  • ✅ 继承带参构造函数类时的正确写法
  • ✅ 支持多接口实现和独立对象表达式
  • ✅ 字节码层面的本质:静态内部类 + 构造注入
  • ✅ 闭包变量捕获的实现原理

这些知识不仅有助于写出更规范的 Kotlin 代码,也能在调试性能问题或反序列化异常时提供底层视角。

示例代码已上传至 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-lang-oop-2


原始标题:Anonymous Inner Classes in Kotlin