24:kotlin 类和对象 -- 对象表达式和声明(Object expressions and declarations)
如果需要创建一个稍微修改了某个类的对象,而不需要显式地声明一个新的子类。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()
成对出现
对于数据对象,不能提供自定义的 equals
或 hashCode
实现
数据对象的 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
静态初始化程序的语义
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!