Kotlin object 关键字详解:对象表达式、对象声明与伴生对象

QuibblerQuibbler 2020-06-18 约 15 分钟 209 次阅读

Kotlin object 关键字详解:对象表达式、对象声明与伴生对象

在 Java 里,"匿名内部类""单例""静态成员"是三套各不相同的写法,琐碎且易错。Kotlin 用一个 object 关键字把它们统一了——根据出现位置和是否带名字,分别表达"匿名对象""单例""类级静态成员"三种语义。本文把这三种用法、它们的差异与陷阱一次讲透。

参考文档:Kotlin 官方文档 · Object declarations。

1、一个 object,三种用法

Kotlin 的 object 关键字有三种形态,先建立全局认知,后面再逐个拆解:

// 形态一:对象表达式(object expression)—— 匿名对象,用完即弃
val listener = object : MouseAdapter() { /* ... */ }

// 形态二:对象声明(object declaration)—— 命名单例
object Singleton { fun hello() = println("hi") }

// 形态三:伴生对象(companion object)—— 类内部的"静态"成员
class Foo {
    companion object { const val TAG = "Foo" }
}

       - 对象表达式:创建一个匿名对象,类似 Java 匿名内部类,每次执行都生成新实例

       - 对象声明:定义一个命名的单例,全局唯一,首次访问时初始化

       - 伴生对象:写在类内部、用 companion 标记,提供"类级别"的成员(Kotlin 没有 static 关键字,靠它实现)

2、对象表达式:匿名对象

对象表达式用 object : 开头,当场创建一个匿名对象,不必事先声明子类。最常见的用法是把它作为方法参数,比如注册一个鼠标监听器:

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /* ... */ }
    override fun mouseEntered(e: MouseEvent) { /* ... */ }
})

它可以同时继承基类并实现多个接口(用逗号分隔)。若超类型有构造函数,必须传参:

open class A(x: Int) { open val y: Int = x }
interface B { /* ... */ }

val ab: A = object : A(1), B {
    override val y = 15
}

甚至可以不带任何超类型,直接得到一个"裸"匿名对象——适合临时封装几个字段:

fun main(args: Array<String>) {
    val site = object {
        var name: String = "菜鸟教程"
        var url: String = "www.runoob.com"
    }
    println(site.name)   // 菜鸟教程
    println(site.url)    // www.runoob.com
}

对象表达式还能捕获作用域中的变量(闭包),这对回调尤其方便——可以在匿名对象里直接读写外层的局部变量:

fun countClicks(window: JComponent) {
    var clickCount = 0
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++   // 直接访问并修改外层变量
        }
    })
}

3、对象声明:优雅的单例

对象声明用 object Name { ... },给一个名字,就得到一个线程安全的单例——无需手写 Java 那套"私有构造 + 静态实例 + getInstance"。引用时直接用名字:

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) { /* ... */ }
    val allDataProviders: Collection<DataProvider>
        get() = /* ... */
}

// 直接用名称调用,无需创建实例
DataProviderManager.registerDataProvider(/* ... */)

它是货真价实的单例:无论用几个变量接收,指向的都是同一个对象,改一个全都变:

object Site {
    var url: String = ""
    val name: String = "菜鸟教程"
}
fun main() {
    val s1 = Site
    val s2 = Site
    s1.url = "www.runoob.com"
    println(s2.url)   // www.runoob.com —— s1、s2 是同一实例
}

对象声明同样可以有超类型(继承基类/实现接口),用于"带默认实现的唯一实例"场景:

object DefaultListener : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /* ... */ }
}

4、伴生对象:Kotlin 的"静态"成员

Kotlin 没有 static 关键字。要在类上挂"类级别"的成员(常量、工厂方法),就把对象声明写进类里、并用 companion 标记,这就是伴生对象。它的成员通过外部类名直接访问:

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}
val instance = MyClass.create()   // 像调用静态方法一样

伴生对象的名字可以省略,默认叫 Companion:

class MyClass {
    companion object { /* ... */ }
}
val x = MyClass.Companion   // 用默认名访问伴生对象本身

两个要点:第一,一个类只能有一个伴生对象(companion 只能用一次);第二,伴生对象的成员"看起来像静态",但运行时它仍是真实对象的实例成员——所以伴生对象本身还能实现接口,常用于"工厂接口"模式:

interface Factory<T> { fun create(): T }

class MyClass {
    // 伴生对象实现 Factory 接口,充当类的工厂
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

5、三者对比与初始化时机

三者最本质的差别在"实例数量"与"初始化时机":

形态          语法                  实例数     初始化时机             典型用途
----------------------------------------------------------------------------------
对象表达式    object : Super {…}    每次新建    使用处立即执行         匿名对象/回调/监听器
对象声明      object Name {…}       全局唯一    首次访问时延迟初始化   单例
伴生对象      companion object {…}  全局唯一    类被加载(解析)时     类级常量/工厂方法

这个初始化时机的"语义差异"最容易踩坑,单独强调:对象表达式在使用处立即执行(每次走到这行就 new 一个);对象声明在首次被访问时延迟初始化(懒加载);伴生对象在外部类被加载时初始化,语义与 Java 的静态初始化器完全匹配。理解这一点,才能解释"为什么单例里的初始化逻辑不会在程序启动就跑"。

6、对象表达式的作用域陷阱

对象表达式有一个隐蔽规则:匿名对象的类型,取决于它在哪暴露。如果只用于局部变量或私有函数返回值,它能保留完整的匿名类型,新增的成员都可访问;但一旦作为公有函数的返回值或公有属性的类型,它的类型会"退化"成声明的超类型(没声明超类型就退化成 Any),你在匿名对象里新增的成员就访问不到了:

class C {
    // 私有函数 → 返回类型是匿名对象类型,成员可访问
    private fun foo() = object { val x: String = "x" }
    // 公有函数 → 返回类型退化为 Any,成员访问不到
    fun publicFoo() = object { val x: String = "x" }

    fun bar() {
        val x1 = foo().x        // OK
        val x2 = publicFoo().x  // 错误:未能解析的引用 "x"
    }
}

对策:要让匿名对象的成员对外可见,就显式声明一个超类型(接口或抽象类),让它作为返回类型,而不是裸用 object。

7、嵌套对象声明的访问规则

对象声明嵌套在类内部时(非 companion),有两条容易踩的规则:第一,不能通过外部类的实例去访问它,只能通过类名;第二,它不能直接访问外部类的成员——因为它是"类级别的单例",和某个外部实例没有绑定关系。

class Site {
    var name = "菜鸟教程"
    object DeskTop {
        var url = "www.runoob.com"
        fun showName() {
            // print("desk legs $name")  // 错误:访问不到外部类实例的 name
        }
    }
}
fun main() {
    val site = Site()
    // site.DeskTop.url   // 错误:不能通过实例访问
    Site.DeskTop.url      // 正确:只能用类名访问
}

如果你需要"和外部类实例绑定的静态式成员",那不是 object 的职责,而应该用普通内部类(inner class)或直接用成员属性。

8、实战场景与最佳实践

       - 单例:用对象声明替代 Java 的各种手写单例(饿汉/双重检查锁),代码更短、线程安全由语言保证

       - 工厂方法:放伴生对象里,调用形如 MyClass.create(),比独立工厂对象更内聚

       - 常量:类级常量用 companion object + const val,编译期内联、零开销

       - 回调/监听器:临时实现一个接口用对象表达式,比单独写个类更轻

       - 与 Java 互操作:在伴生对象里加 @JvmStatic 可暴露成真正的静态方法、@JvmField 暴露成静态字段,方便 Java 调用方

       - 一个判断口诀:要"唯一实例"用对象声明,要"一次性匿名对象"用对象表达式,要"挂在类上的静态成员"用伴生对象

9、总结

Kotlin 用 object 一个关键字,把"匿名对象、单例、静态成员"三件事优雅地统一起来:对象表达式(object :)当场造匿名对象,对象声明(object Name)定义单例,伴生对象(companion object)提供类级成员。三者的本质差异在实例数量与初始化时机——对象表达式用完即弃、立即执行,对象声明全局唯一、懒加载,伴生对象随类加载初始化。

关键要点:

       - object : 表达式 = 匿名对象(闭包可捕获外层变量);object Name 声明 = 单例;companion object = 类级静态成员

       - 伴生对象运行时仍是真实对象,可实现接口;一个类只能有一个 companion

       - 初始化时机:对象表达式立即、对象声明懒加载、伴生对象随类加载

       - 陷阱:匿名对象作为公有返回会退化为 Any;嵌套 object 不能经实例访问、访问不到外部类成员

对于从 Java 转向 Kotlin 的开发者而言,把 object 的三种用法与差异内化,是写出地道 Kotlin 的关键一步。下次面对"要不要写个单例""这个回调怎么传""常量放哪里",先想想 object 的哪一种形态最贴合——一旦用顺手,你会发现 Kotlin 在这类"日常样板"上比 Java 简洁太多,代码量少了,可读性和正确性反而更高。

        Kotlin 对象表达式和对象声明(菜鸟教程)

相关推荐

Parcelize:Parcelable 实现生成器
Kotlin

Parcelize:Parcelable 实现生成器

Parcelize:Parcelable 实现生成器Parcelable是安卓开发中常用的序列化方式,之前分享过一个Parcelable序列化插件,方便开发者快速生成代码,提高开发效率。1、Parcelize插件Kotlin官方也推出一个Parcelable自动生成的Gradle插件:Parcelize。1.1、引入插件在项目的build.gradle中添加:id("kotlin-parceliz

2.6k
Kotlin的继承
Kotlin

Kotlin的继承

Kotlin的继承1、超类:Any类似于Java中的Object类,Kotlin也有一个叫Any的超类,是所有类的父类,所有类都默认继承自该类。超类的定义如下,其中定义了三个函数:2、open注意上面的Any类用open修饰,表示该类可以被继承。Kotlin中的类默认都是final的,无法被继承:如果想让类可以被继承,就需要用在定义类的地方用open关键字标识该类可以被继承。3、构造器继承中涉及到

2.1k
 Kotlin基本语法
Kotlin

Kotlin基本语法

Kotlin基本语法了解Kotlin语言的基本语法。1、包kotlin文件以.kt结尾,编译文件还是如此只不过在文件名后面加上Kt后缀。这一点和Java不同,Java源文件.java,编译后为.class文件。1.1、包定义每个kotlin文件的第一行就用关键字package定义该文件所在的包。例:1.2、导包用到其它kotlin文件需要使用关键字import导入,这一点和Java一样。kotli

2.1k