Kotlin 类属性可以说是 Kotlin 类最难理解和掌握的部分了,变化多、关键字多、限制多,与 Java 差别相当大,很多人都会在这上面栽不少坑,今天我们就一起来深入看看 Kotlin 类属性。
Kotlin 中去除的类属性关键字
Kotlin 中去除了几个用于类属性的关键字,需要用注解实现。
-
transient:这个关键字表示该属性是「瞬时」的,不参与类的序列化。Kotlin 中用 @Transient 标注;
-
volatile:这个关键字用于多线程,可以视为「轻量化的 synchronized」。Kotlin 中用 @Volatile 标注。
@JvmField 的作用
@JvmField 是 kotlin.jvm.JvmPlatformAnnotations.kt 中提供的注解。
要理解 @JvmField 的作用,我们首先要明白一个 Kotlin 类属性编译后会变成什么。我们还是用老朋友 Person 类来举例:
class Person(val name: String)
虽然我们定义一个属性 name,但 Kotlin 并没有允许 Person 类对外(对 Java)暴露这个属性,编译后的 name 属性实际上是这样的:
@NotNull private final String name;
@NotNull public final String getName() {
return name;
}
我们在外部并不能直接访问 name 属性,一定要通过 getName() 方法间接访问它,这也就是我们最开始就提到过的「标准化 getter / setter」。
但实际使用过程中,我们有时需要将 Kotlin 类的属性直接暴露出来(虽然不利于封装),这时就可以使用 @JvmField 注解:
class Person(@JvmField val name: String)
编译之后 name 属性将直接暴露:
@JvmField @NotNull
public final String name;
private 类属性的细节
用 private 修饰的类属性只能在这个类内部访问,外部无法访问,也就没有提供对外接口的必要。有以下一些细节:
-
private 类属性默认不生成 getter / setter,对它的访问都是直接访问;
-
一旦 private 属性有自定义的 getter / setter,访问时就要通过 getter / setter 了;
-
@JvmField 注解无法用于 private 类属性,没有必要;
-
private 类属性可以放在主构造函数中定义。
幕后字段和 field
什么是幕后字段?
要理解这个概念,我们得再深一步探究:getter 和 setter 一定要与某个属性相关联吗? 答案当然是否定的。getter 是从对象中获取特定的值,这个值完全可能是每次访问时临时计算的,也可能是从其他对象那里得到的;setter 也可能是在设置其他对象的属性。事实上,Kotlin 只会为满足特定条件的类属性添加 JVM 意义上的类属性。
我们为 Person 添加一个 nameHash 属性:
class Person(val name: String) {
val nameHash get() = name.hashCode()
}
这个 nameHash 属性并没有初始化,只定义了一个 getter 方法。这个属性在编译之后就会消失,只留下一个 getNameHash() 方法:
public final int getNameHash() {
return name.hashCode();
}
我们可以这样说:「nameHash 是一个没有幕后字段的属性」,它作为类属性只存在于 Kotlin 代码中,并没有一个真正的 JVM 类属性与之对应。怎么给 nameHash 添加一个幕后字段呢?Kotlin 并不允许我们手动添加,但会自动为满足条件的类属性添加幕后字段:
-
使用默认 getter / setter 的属性,一定有幕后字段。对于 var 属性来说,只要 getter / setter 中有一个使用默认实现,就会生成幕后字段;
-
在自定义 getter / setter 中使用了 field 的属性,一定有幕后字段。这个 field 就是我们访问幕后字段的「关键字」,它与 Lambda 表达式中的 it 类似,并不是一个真正的关键字,只在特定的语句内有特殊的意思,在其他语句内都不是关键字。
比如,我们可以写:
class Person(val name: String, val gender: Gender, age: Int) {
val age = age
get() = when (gender) {
Gender.FEMALE -> (field * 0.8).toInt()
Gender.MALE -> field
}
}
enum class Gender {
MALE, FEMALE
}
我们这里通过幕后字段实现了 age 属性根据性别的不同行为。(´・ω・`)
虽然我们为 age 属性自定义了 getter,但因为在 getter 中用了 field 关键字,Kotlin 就会为我们生成一个 private final int age 属性作为幕后字段。这个幕后字段同时需要初始化,我们就用主构造函数中传入的 age 来初始化它。
幕后属性大用处
很多时候,我们希望定义这样的属性:
-
对外表现为 val 属性,只能读不能写;
-
在类内部表现为 var 属性,也就是说只能在类内部改变它的值。
这在 Kotlin 集合框架中应用十分广泛。比如 Collection 接口定义的 size 属性就是一个 val 属性,对外只读;但对于一个 MutableCollection 来说,size 的值在内部一定是能改变的,也只允许内部修改它。我们可以这样设计:
override val size get() = _size
private var _size: Int = 0
我们在内部增删元素时,改动的就是 _size 属性的值;外部只能访问到 size 属性,不能修改 _size 的值。这里的 _size 就叫做「幕后属性」。
kotlin.collections.AbstractMap.kt 中也用到了幕后属性,我们看一下:
private @Volatile var _keys: Set<K>? = null
override val keys: Set<K> get() {
if (_keys == null) {
_keys = object : AbstractSet<K>() {
override operator fun contains(element: K): Boolean = containsKey(element)
override operator fun iterator(): Iterator<K> {
val entryIterator = entries.iterator()
return object : Iterator<K> {
override fun hasNext(): Boolean = entryIterator.hasNext()
override fun next(): K = entryIterator.next().key
}
}
override val size: Int get() = this@AbstractMap.size
}
}
return _keys!!
}
keys 是对外的 API,只读且非空,每次访问都被委托到访问 _keys 属性上,_keys 属性在内部可以改动也可以为 null,给我们带来方便。
在设计类属性时,可以通过幕后属性灵活地实现功能。