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 项目。