1. 概述

在 Java 中,我们有匿名类,它允许我们在使用点直接声明并实例化一个没有名字的类。

在本教程中,我们将探讨 Kotlin 中的匿名类和匿名对象。

2. 匿名对象简介

匿名类的实例被称为匿名对象,它们通过表达式而非名称来定义。

通常,匿名对象结构简单、轻量,适用于一次性场景。在 Kotlin 中,匿名对象是通过object expressions创建的。

⚠️ 注意:这里提到的 object expression 与用于创建单例对象的 object declaration 是不同的概念。

在本文中,我们将先了解如何从一个超类(如抽象类)创建匿名对象,然后探讨一个有趣的用法:从零定义匿名对象

此外,我们还将比较 Kotlin 与 Java 中的匿名对象行为。

为了验证匿名对象是否正常工作,我们会使用单元测试断言进行检查。

接下来,我们来看具体用法。

3. 从抽象类创建匿名对象

与 Java 一样,在 Kotlin 中也不能直接实例化抽象类。例如,我们定义一个抽象类 Doc

abstract class Doc(
    val title: String,
    val author: String,
    var words: Long = 0L
) {
    abstract fun summary(): String
}

如果我们尝试直接实例化:

val article = Doc(title = "A nice article", author = "Kai", words = 420) // 编译失败!

编译器会报错:“cannot create an instance of an abstract class”。

这时,我们可以使用 object 表达式创建一个继承自 Doc 的匿名类实例:

val article = object : Doc(title = "A nice article", author = "Kai", words = 420) {
    override fun summary() = "Title: <$title> ($words words) By $author"
}

语法结构如下:

object : TheType(...constructor parameters...) { ... implementations ... }

我们实现了抽象方法 summary(),现在可以验证该对象是否按预期工作:

article.let {
    assertThat(it).isInstanceOf(Doc::class.java)
    assertThat(it.summary()).isEqualTo("Title: <A nice article> (420 words) By Kai")
}

测试通过,说明 article 对象工作正常。

除了抽象类,我们也可以用类似语法从接口创建匿名对象。接下来我们看接口的用法。

4. 从接口创建匿名对象

Kotlin 中的接口不能有构造函数,因此语法略有不同:

object : TheInterface { ... implementations ... }

假设我们有一个接口:

interface Printable {
    val content: String
    fun print(): String
}

我们可以通过匿名对象实现它:

val sentence = object : Printable {
    override val content: String = "A beautiful sentence."
    override fun print(): String = "[Print Result]\n$content"
}

验证代码如下:

sentence.let {
    assertThat(it).isInstanceOf(Printable::class.java)
    assertThat(it.print()).isEqualTo("[Print Result]\nA beautiful sentence.")
}

测试通过,说明接口的匿名对象也正常工作。

接下来,我们看一个更有趣的用法:从零定义匿名对象

5. 从零定义匿名对象

除了从已有类型创建匿名对象,Kotlin 还支持直接定义一个没有明确超类型的匿名对象。例如:

val player = object {
    val name = "Kai"
    val gamePlayed = 6L
    val points = 42L
    fun pointsPerGame() = "$name: AVG points per Game: ${points / gamePlayed}"
}

此时,player 变量持有一个匿名对象,它没有显式继承任何类或实现任何接口。

验证代码如下:

player.let {
    assertThat(it.name).isEqualTo("Kai")
    assertThat(it.pointsPerGame()).isEqualTo("Kai: AVG points per Game: 7")
}

这个特性在 Java 中是没有的。Java 的匿名类必须有超类型(至少是 Object)。不过从 Java 10 开始可以使用 var 关键字实现类似效果:

var player = new Object() {
    String name = "Kai";
    Long gamePlayed = 6L;
    Long points = 42L;
    String pointsPerGame() {
        return name + ": AVG points per Game: " + points / gamePlayed;
    }
};

但注意,如果返回类型是 Object,则无法访问其成员。但在 Kotlin 中,如果匿名对象是私有方法的返回值,我们仍然可以访问它的成员

class PlayerService() {
    private fun giveMeAPlayer() = object {
        val name = "Kai"
        val gamePlayed = 6L
        val points = 42L
        fun pointsPerGame() = "$name: AVG points per Game: ${points / gamePlayed}"
    }

    fun getTheName(): String {
        val thePlayer = giveMeAPlayer()
        println(thePlayer.pointsPerGame())
        return thePlayer.name
    }
}

这是 Kotlin 相比 Java 在匿名对象灵活性上的一个优势。

6. 匿名对象的类型转换

类型转换是 Kotlin 中的基础概念之一。下面我们来看匿名对象在类型转换方面的行为。

6.1. 有超类型的匿名对象

当我们从接口或抽象类创建匿名对象时,它会自动具有该超类型。例如:

val article = object : Doc(...) { ... }

此时 article 的类型就是 Doc,可以直接传递给接受 Doc 类型参数的方法:

fun docTitleToUppercase(doc: Doc) = doc.title.uppercase()

调用时无需显式转换:

assertThat(docTitleToUppercase(article)).isEqualTo("A NICE ARTICLE")

6.2. 从零定义的匿名对象

如果我们定义了一个没有超类型的匿名对象:

val anonymousPlayer = object {
    val name = "Kai"
    val gamePlayed = 6L
    val points = 42L
    fun pointsPerGame() = "$name: AVG points per Game: ${points / gamePlayed}"
}

它的类型是 Any。如果我们试图将它转换为其他类型,即使结构相同,也会抛出 ClassCastException

data class Player(val name: String, val gamePlayed: Long, val points: Long) {
    fun pointsPerGame() = "$name: AVG points per Game: ${points / gamePlayed}"
}

assertFailsWith<ClassCastException> { anonymousPlayer as Player } // 抛出异常

使用安全转换操作符 as? 也会返回 null

val alwaysNull = anonymousPlayer as? Player
assertThat(alwaysNull).isNull()

⚠️ 踩坑提醒:Kotlin 是静态类型语言,不支持“鸭子类型”。即使两个对象结构相同,也不能直接互换使用。

例如,下面这段代码在 Python 中可以工作,但在 Kotlin 中会编译失败:

val realPlayer = Player(name = "Liam", gamePlayed = 7L, points = 77L)
val stringList = listOf(realPlayer, anonymousPlayer).map {
    it.pointsPerGame() // ❌ 编译错误:Any 没有 pointsPerGame 方法
}

因此,如果你需要将对象用于外部处理,建议直接创建具体类型的实例,而不是使用从零定义的匿名对象

7. 小结

在本文中,我们学习了如何使用 object 表达式在 Kotlin 中创建匿名对象:

✅ 可以从抽象类或接口创建匿名对象
✅ 也可以从零定义匿名对象
❌ 匿名对象不能直接转换为其他类型,即使结构相同
✅ 从零定义的匿名对象可被 Kotlin 推断出成员类型(私有方法中)
❌ Java 中匿名对象不能访问其成员方法,除非使用 var

如果你希望在项目中使用匿名对象,请注意其适用范围,避免因类型不匹配导致的运行时错误。

完整代码示例请参考 GitHub 项目


原始标题:Anonymous Objects in Kotlin