kotlin中的 object 关键字

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

对象表达式

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

创建匿名类

1
2
3
4
5
6
7
8
9
10
11
12
13
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是空的。当然了匿名类也是类,只是没有名字而已,当然做了继承其他类,实现其他接口。注意,同样只能单继承多实现,并且父类构造函数需要参数时可以传适当的构造参数。
比如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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?) {

}
}

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

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

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

1
2
3
4
5
6
7
8
9
10
class C {
private fun getObject()= object {
val x = "x"
}

private fun test(){
println(getObject().x)
}
}

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

  • 如果没有明确声明类型,则是Any
  • 如果只有一个父类或者一个接口,则是改父类或者接口类型
  • 如果有一个父类和多个接口,则是方法明确返回的类型
    比如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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)
    }
    }
    }

    匿名对象访问变量

    对象表达式中的代码可以访问来自包含它的作用域的变量:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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修饰类即可,当然这个单例类也是可以有父类的

1
2
3
4
5
6
7
8
9
10
11
12
13
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对象声明时,字符串表示同时包含其名称和对象的哈希:

1
2
3
4
5
object MyObject

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

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

1
2
3
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关键字达到这个效果

1
2
3
4
5
6
7
8
9
10
class  MyClassOne{
object A{
fun createA(): MyClassOne = MyClassOne()
}
companion object AA{
fun createAA(): MyClassOne = MyClassOne()
}
}
MyClassOne.A.createA()
MyClassOne.createAA()

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

1
2
3
4
5
6
class MyClassOneJVMStatic{
companion object AAA {
@JvmStatic
fun create(): MyClassOneJVMStatic = MyClassOneJVMStatic()
}
}

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

1
2
3
4
5
6
7
class MyClassTwo{
companion object {
fun create(): MyClassTwo = MyClassTwo()
}
}
MyClassTwo.Companion.create()//正确,但会提示Companion是不必要的
MyClassTwo.create()//正确

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

1
2
3
4
5
6
7
8
9
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
静态字段


已学习:

  • 扩展

    • 扩展函数
    • 扩展属性
    • 作用域
  • 函数类型

    • 带有接收者的函数类型
    • Lambda表达式
    • SAM 转换
  • 泛型

    • 逆变
    • 协变
    • 类型投影
    • 星投影
    • 泛型约束
  • 关键字

    • 作用域函数:with、let、run、apply、also
    • object:匿名内部类、单例模式、伴生对象
  • 委托

    • 委托类
    • 委托属性
    • 自定义委托

未学习:

  • 关键字

    • Unit
    • Nothing
    • inline,noinline,crossinline
  • 协程

    • 启动
    • 挂起
    • Job
    • Context
    • Channel
    • Flow
    • select
    • 并发、异常
    • launch
    • Dispatchers
    • CoroutineScope

kotlin中的 object 关键字
https://blog.huangyuanlove.com/2024/05/31/kotlin中的-object-关键字/
作者
HuangYuan_xuan
发布于
2024年5月31日
许可协议
BY HUANG兄