24:kotlin 类和对象 -- 对象表达式和声明(Object expressions and declarations)

2023-12-13 03:37:30

如果需要创建一个稍微修改了某个类的对象,而不需要显式地声明一个新的子类。Kotlin可以通过对象表达式(object expressions)和对象声明(object declarations)来处理这种情况。

对象表达式

对象表达式用于创建匿名类的对象。这种类对于一次性使用非常有用。你可以从头开始定义它们,继承现有的类,或者实现接口。匿名类的实例也被称为匿名对象,因为它们是由表达式定义的,而不是由名称定义的。

从头开始创建匿名对象

对象表达式以关键字object开始。

定义一个普通的无父类的匿名对象

val helloWorld = object {
    val hello = "Hello"
    val world = "World"
    // 重写Any的toString方法
    override fun toString() = "$hello $world"
}

fun main() {
    print(helloWorld)
}

继承

继承MouseAdapter

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

    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})

如果超类型有构造函数,则向其传递适当的构造函数参数

open class A(x: Int) {
    public open val y: Int = x
}

interface B { /*...*/ }

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

将匿名对象用作返回类型和值类型

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

class C {
    private fun getObject() = object {
        val x: String = "x"
    }

    fun printX() {
        println(getObject().x)
    }
}

如果获取匿名类的函数或属性是公共的或私有内联的,则其实际类型为:

  • 如果匿名对象没有声明的超类型,则为 Any
  • 如果有确切的一个声明的超类型,则为匿名对象的声明的超类型
  • 如果有多于一个声明的超类型,则为显式声明的类型

在所有这些情况下,无法访问在匿名对象中添加的成员。如果在函数或属性的实际类型中声明了重写的成员,则可以访问这些成员

interface A {
    fun funFromA() {}
}
interface B

class C {
    // 返回类型是Any,x变量 不能访问
    fun getObject() = object {
        val x: String = "x"
    }

    // 返回类型是A,x变量 不能访问
    fun getObjectA() = object: A {
        override fun funFromA() {}
        val x: String = "x"
    }

    // 返回类型是B,funFromA()和 x变量 均不能访问
    fun getObjectB(): B = object: A, B { // 需要显式返回类型
        override fun funFromA() {}
        val x: String = "x"
    }
}

访问匿名对象中的变量

对象表达式中的代码可以访问来自封闭范围的变量

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
}

对象声明

声明单例模式

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }

    val allDataProviders: Collection<DataProvider> = MutableList(0){DataProvider()}
        get() = field// ...
}

对象声明是在 object 关键字后面跟着一个名称。就像变量声明一样,对象声明不是一个表达式,不能在赋值语句的右侧使用

对象声明在首次访问时进行初始化,是线程安全的

要引用对象,直接使用它的名称

DataProviderManager.registerDataProvider(...)

继承

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

    override fun mouseEntered(e: MouseEvent) { ... }
}

对象声明不能在方法内部(不能是局部对象)

数据对象

打印一个普通的对象声明时,其字符串表示包含了它的名称和对象的哈希值

object MyObject

fun main() {
    println(MyObject()) // MyObject@1f32e575
}

就像数据类一样,你可以用 data修饰符标记一个对象声明。这会告诉编译器为你的对象生成一些函数

  • toString() 返回数据对象的名称
  • equals() hashCode() 成对出现

对于数据对象,不能提供自定义的 equalshashCode 实现

数据对象的 toString() 函数返回对象的名称

data object MyDataObject {
    val x: Int = 3
}

fun main() {
    println(MyDataObject) // MyDataObject
}

equals() 函数用于数据对象,确保具有与你的数据对象相同类型的所有对象都被视为相等。在大多数情况下,你在运行时只会有一个数据对象的实例(毕竟,数据对象声明了一个单例)。然而,在特殊情况下,如果在运行时生成了另一个相同类型的对象(例如,通过使用平台反射与java.lang.reflect或使用此 API 底层的 JVM 序列化库),这确保这些对象被视为相等

确保你只通过结构比较数据对象(使用 == 操作符),而绝不是通过引用比较(使用 === 操作符)。这有助于在运行时存在多个数据对象实例时避免陷阱

data object MySingleton

fun main() {
    val evilTwin = createInstanceViaReflection()

    println(MySingleton) // MySingleton
    println(evilTwin) // MySingleton

    // 使用 == 比较两个数据类返回 true
    println(MySingleton == evilTwin) // true

    // 不要通过 === 比较两个数据类
    println(MySingleton === evilTwin) // false
}

fun createInstanceViaReflection(): MySingleton {
    // kotlin反射不允许创建数据对象
    // 当前方法会强制创建一个MySingleton对象
    // 不要在代码中这样做
    return (MySingleton.javaClass.declaredConstructors[0].apply { isAccessible = true } as Constructor<MySingleton>).newInstance()
}

生成的 hashCode() 函数与 equals() 一样 ,因此数据对象的所有运行时实例都具有相同的哈希码

数据对象和数据类之间的差异
虽然数据对象和数据类声明通常一起使用并具有一些相似之处,但有一些函数不会为数据对象生成

  • 没有copy()函数。因为数据对象声明旨在用作单例对象,所以不会生成 copy()函数。单例模式将类的实例化限制为单个实例,允许创建实例的副本将违反这一原则
  • 没有 componentN() 函数。与数据类不同,数据对象没有任何数据属性。由于在没有数据属性的情况下尝试解构这样的对象是没有意义的,因此不会生成 componentN() 函数

使用数据对象处理密封层次结构

数据对象声明在处理密封层次结构(如密封类或密封接口)时特别有用,这可以更简洁的定义一个与其他密封类相同的类型

sealed interface ReadResult
data class Number(val number: Int) : ReadResult
data class Text(val text: String) : ReadResult
data object EndOfFile : ReadResult

fun printReadResult(r: ReadResult) {
    when(r) {
        is Number -> println("Num(${r.number}")
        is Text -> println("Txt(${r.text}")
        is EndOfFile -> println("EOF")
    }
}

fun main() {
    printReadResult(EndOfFile) // EOF
}

伴生对象

在类内部,可以使用 companion关键字标记的对象声明

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

伴生对象的成员可以通过使用类名作为限定符而被简单调用

val instance = MyClass.create()

伴生对象的名称可以省略,默认名称Companion

class MyClass {
    companion object{ }
}

val x = MyClass.Companion
fun main() {
    println(x)	// io.example.MyClass$default@35fb3008
}

类成员可以访问相应伴生对象的私有成员

一个类的名称(单独使用,而不是作为另一个名称的限定符)充当对该类的伴生对象的引用(无论是否命名)

class MyClass {
    companion object{ }
}

val y = MyClass	// 外部类名称做伴生对象的引用
fun main() {
    println(y)	// io.example.MyClass$default@35fb3008
}

请注意,尽管伴生对象的成员看起来像是其他语言中的静态成员,但在运行时,它们仍然是真实对象的实例成员,因此可以实现接口

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

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

fun main() {
    val f: Factory<MyClass> = MyClass
    println(f.create()) // io.example.MyClass@7225790e
}

JVM上,如果使用 @JvmStatic 注解,可以将伴生对象的成员生成为真正的静态方法和字段。有关详细信息,查看这里

对象表达式和对象声明之间存在一个重要的语义差异:

  • 对象表达式是立即执行(和初始化)的,它们在使用的地方立即执行。
  • 对象声明是懒初始化的,当首次访问时才会初始化。
  • 伴生对象在相应的类被加载(解析)时初始化,这符合 Java静态初始化程序的语义

文章来源:https://blog.csdn.net/qq_40104261/article/details/134804641
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。