kotlin中的 object 关键字

513 阅读6分钟

kotlin 中的object关键字用处比较广泛,在官方文档对象表达式与对象声明有详细的介绍,比如:创建匿名对象、创建匿名内部类并继承某个类,实现多个接口、使用匿名对象作为返回和值类型、从匿名对象访问变量、单例模式、数据对象、伴生对象等,不过文章是从对象表达式对象声明角度来区分的。

对象表达式

对象表达式可以用来创建匿名类,就是不用class来声明的类,当这个类只用一次的时候是很有帮助的。我们可以从头开始定义匿名类,也可以从现有类继承,还可以实现接口。匿名类的实例也称为匿名对象,因为它们是由表达式而不是名称定义的。

创建匿名类

fun main() {
    val helloWorld = object {
        val hello = "Hello"
        val world = "World"
        //object expressions extend Any, so `override` is required on `toString()`
        override fun toString(): String {
            return "$hello $world"
        }
    }
    println(helloWorld)
    println(helloWorld.javaClass.simpleName)
    println(helloWorld.javaClass.name)
}

可以看到,这种方式在某种意义上和 js 中创建对象差不多,helloWorld这个实例的helloWorld.javaClass.simpleName是空的。当然了匿名类也是类,只是没有名字而已,当然做了继承其他类,实现其他接口。注意,同样只能单继承多实现,并且父类构造函数需要参数时可以传适当的构造参数。 比如这样:

interface OnClickListener {
    fun onClick(view: View?)
}
interface  OnLongClickListener{
    fun onLongClick(view: View?)
}
abstract class OnScroll{
    abstract fun onScroll(direction: Int)
}
val viewListener = object : OnScroll(),OnLongClickListener, OnClickListener {
    override fun onScroll(direction: Int) {

    }
    override fun onClick(view: View?) {
        println("view clicked")
    }
    override fun onLongClick(view: View?) {

    }
}

当然也可以直接当成参数传入调用的方法,都是一样的。本质上都是创建了一个对象。

使用匿名对象作为返回值或类型

当匿名对象被用作局部或私有但非内联声明(函数或属性)的类型时,其所有成员都可通过该函数或属性访问:

class C {
  private fun getObject()= object {
       val x = "x"
  }
    
  private fun test(){
      println(getObject().x)
  }
}

如果方法供或者属性是 public 或者 private inline 时,它的实际类型可能如下:

  • 如果没有明确声明类型,则是Any
  • 如果只有一个父类或者一个接口,则是改父类或者接口类型
  • 如果有一个父类和多个接口,则是方法明确返回的类型 比如
class CC {
    // 返回值类型是 Any; 不能在其他方法中访问 x
    fun getObject() = object {
        val x: String = "x"
    }

    // 返回值类型是 AA; 不能在其他方法中访问 x
    fun getObjectAA() = object: AA {
        override fun funFromA() {}
        val x: String = "x"
        fun test(){
            //这里会报错,访问不到 x 属性
            println(getObject().x)
        }
    }

    // 返回值类型是 BB; 不能在其他方法中访问 x
    fun getObjectBB(): BB = object: AA, BB { // explicit return type is required
        override fun funFromA() {}
        val x: String = "x"
        fun test(){
            //这里会报错,访问不到 x 属性
            println(getObject().x)
        }
    }
}

匿名对象访问变量

对象表达式中的代码可以访问来自包含它的作用域的变量:

class AAA{
    val x = "x"
    val y = "y"
    fun getObject() = object {
        val xx = x//不报错
        val yy = y//不报错
    }
    class AAAA{
        val xx = x //报错
    }
    object BBBB{
        val xx = x//报错
    }
}

对象声明

实现单例模式

在Kotlin当中,要实现单例模式其实非常简单,我们直接用object修饰类即可,当然这个单例类也是可以有父类的

object MyViewListener : OnClickListener, OnLongClickListener {
    override fun onClick(view: View?) {
        println("view clicked")
    }

    override fun onLongClick(view: View?) {
        println("view long clicked")
    }
    fun test(){
        println("test")
    }
}

调用的时候直接类名.方法名即可。这里有个注意点:对象声明不能在局部作用域(即不能直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中。

数据对象(data object)

当我们想要打印object对象声明时,字符串表示同时包含其名称和对象的哈希:

object MyObject

fun main() {
    println(MyObject) // MyObject@3ac3fd8b
}

我们可以还用data关键字来修饰它

data object  MyDataObject{
    val x = 3
}

这样的话,编译器会为这个对象生成toString()方法,该方法只会返回对象名字。还有成对出现的equals()/hashCode().这里有一点需要注意: 被重写的equals()方法会将所有相同名字的data object都返回true,这个解释不是太严谨,因为绝大部分情况下,data object是单例的,在运行时只会有一个对象,但我们可以通过平台相关的方法创建另外一个实例对象,比如 jvm 上的反射、序列化和反序列化等。因此,在比较data object是否相同时,请使用==而不是===

data class 和 data object 的不同

虽然数据类和数据对象经常一起使用,并且有一些相同点,但有些函数在data object中是不会生成的

  • 没有copy()方法,因为data object用作单例对象,所以不会生成该方法。如果允许创建另外个实例对象,则违反了该模式。
  • 没有componentN()方法,该方法的一个简单的用法就是用于结构对象,允许一次性获取对象的所有属性值,并将它们作为单独的参数传递给函数或构造器。但由于data object没有属性,生成这些方法是没有意义的。

伴生对象

kotlin中并没有static关键字,那么我们如何实现静态方法的效果?我们可以使用companionobject关键字达到这个效果

class  MyClassOne{
    object A{
        fun createA(): MyClassOne = MyClassOne()
    }
    companion object AA{
        fun createAA(): MyClassOne = MyClassOne()
    }
}
MyClassOne.A.createA()
MyClassOne.createAA()

但是看反编译之后的代码,编译器还是为我们创建了AAA两个类。如果在jvm平台,我们可以使用@JvmStatic注解,将伴生对象的成员生成为真正的静态方法和字段。

class MyClassOneJVMStatic{
    companion object AAA {
        @JvmStatic
        fun create(): MyClassOneJVMStatic = MyClassOneJVMStatic()
    }
}

当然,对于上面MyClassOne中的AA是可以省略名字的

class MyClassTwo{
    companion object {
        fun create(): MyClassTwo = MyClassTwo()
    }
}
MyClassTwo.Companion.create()//正确,但会提示Companion是不必要的
MyClassTwo.create()//正确

请注意,即使伴生对象的成员看起来像其他语言的静态成员,在运行时他们仍然是真实对象的实例成员,而且,例如还可以实现接口:

interface Factory<T> {
    fun create(): T
}
class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}
val f: Factory<MyClass> = MyClass

对象表达式和对象声明之间的语义差异

对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行(及初始化)的。
  • 对象声明是在第一次被访问到时延迟初始化的。
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配 。

参考:
对象表达式与对象声明
Object expressions and declarations
静态字段