上篇文章我们了解了 Kotlin 中的各种类, 从 Kotlin 的类开始说起 https://www.jianshu.com/p/e715aa24a0d0 , 而类中则有属性和方法, Kotlin 中的类属性和 Java 的类成员变量还是有很大区别, 同时类属性也有一些比较难以理解的东西, 如: 属性的声明形式, 幕后字段, 幕后属性等等. 本篇文章我们将详细深入的了解这些东西.
1 . 前戏(Kotlin 的普通属性)
在 Kotlin 中, 声明一个属性涉及到 2 个关键字, var 和 val .
var 声明一个可变属性
val 声明一个只读属性
通过关键字 var 声明一个属性:
- class Person {
- var name:String = "Paul"// 声明一个可变属性, 默认值为 Paul
- }
复制代码
通过 var 声明的属性是可以改变属性的值的, 如下所示:
- fun main(args: Array<String>) {
- var person = Person()
- // 第一次打印 name 的值
- println("name:${person.name}")
- // 重新给 name 赋值
- person.name = "Jake"
- // 打印 name 的新值
- println("name:${person.name}")
- }
复制代码
打印结果如下:
- name:Paul
- name:Jake
复制代码
如果把 name 属性换成 val 声明为只读属性, 在来改变的的值呢?
- class Person {
- val name:String = "Paul"
- }
复制代码
可以看到, 重新给 val 声明的属性赋值时, 编译器就会报错 Val cannot be reassigned , 它的值只能是初始化时的值, 不能再重新指定.
这是 Kotlin 的两种声明属性方式, 这不是很简单吗? 一行代码. 表面很简单, 不过这一行代码包含的东西很多, 只是没有显示出来而已, 我们来看一下一个属性的完整声明形式:
- // 可变属性
- var <propertyName>[: <PropertyType>] [= <property_initializer>]
- [<getter>]
- [<setter>]
- // 只读属性
- val <propertyName>[: <PropertyType>] [= <property_initializer>]
- [<getter>]
复制代码
瞬间多了很多东西, 其初始器 (initializer),getter 和 setter 都是可选的. 属性类型如果可以从初始器 (或者从其 getter 返回值, 如下文所示) 中推断出来, 也可以省略. 也就是我们上面看到的属性声明, 其实是省略了 getter 和 setter 的, 已默认提供
var name:String = "Paul" // 使用默认的 getter 和 setter
复制代码
其中初始化的是一个字符串, 因此可以从初始化起推断这个属性就是一个 String 类型, 所以属性类型可以省略, 变成这样:
var name = "Paul" // 能推断出属性类型, 使用默认的 getter 和 setter
复制代码
1. 2 getter & setter
在 Kotlin 中, getter,setter 是属性声明的一部分, 声明一个属性默认提供 getter 和 setter , 当然了, 如果有需要, 你也可以自定义 getter 和 setter. 既然要自定义, 我们得先理解 getter 和 setter 是什么东西.
在 Java 中, 外部不能访问一个类的私有变量, 必须提供一个 setXXX 方法和 getXXX 方法来访问, 比如 Java 类 Person, 提供了 getName()和 setName()方法供外面方法私有变量 name:
- public class Person{
- private String name;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
复制代码
在 Kotlin 中 getter 和 setter 跟 Java 中的 getXX 和 setXX 方法作用一样, 叫做访问器.
getter 叫读访问器, setter 叫写访问器. val 声明的变量只有读访问器 getter ,var 声明的变量读写访问器都有.
Q: 在 Kotlin 中, 访问一个属性的实质是什么呢?
A: 读一个属性, 通过. 表示, 它的实质就是执行了属性的 getter 访问器, 举个例子:
- class Person {
- var name:String = "Paul"
- }
- // 测试
- fun main(args: Array<String>) {
- var person = Person()
- // 读 name 属性
- val name = person.name
- println("打印结果:$name")
- }
复制代码
打印的结果肯定是:
打印结果: Paul
复制代码
然后, 我们再来修改 getter 的返回值如下:
- class Person {
- var name:String = "Paul"
- get() = "i am getter,name is Jake"
- }
- // 测试
- fun main(args: Array<String>) {
- var person = Person()
- // 读 name 属性
- val name = person.name
- println("打印结果:$name")
- }
复制代码
执行结果如下:
打印结果: i am getter,name is Jake
复制代码
因此, 读一个属性的本质是执行了 getter, 这跟 Java 很像, 读取一个 Java 类的私有变量, 需要通过它提供的 get 方法.
类似的, 在 Kotlin 中, 写一个属性的实质就是执行了属性的写访问器 setter. 还是这个例子, 我们修改一下 setter:
- class Person {
- var name:String = "Paul"
- set(value) {
- println("执行了写访问器, 参数为:$value")
- }
- }
- // 测试
- fun main(args: Array<String>) {
- var person = Person()
- // 写 name 属性
- person.name = "hi,this is new value"
- println("打印结果:${person.name}")
- }
复制代码
执行结果为:
执行了写访问器, 参数为: hi,this is new value
打印结果: Paul
复制代码
可以看到给一个给一个属性赋值时, 确实是执行了写访问器 setter, 但是为什么结果还是默认值 Paul 呢? 因为我们重写了 setter, 却没有给属性赋值, 当然还是默认值.
那么一个属性的默认的 setter 涨什么样子呢? 聪明的你可能一下就想到了, 这还不简单, 跟 Java 的 setXXX 方法差不多嘛(傲娇脸). 一下就写出来了, 如下:
- class Person {
- // 错误的演示
- var name = ""
- set(value) {
- this.name = value
- }
- }
复制代码
不好意思, 一运行就会报错, 直接 StackOverFlow 了, 内存溢出, 为什么呢? 转换为 Java 代码看一下你就明白了, 将 Person 类转为 Java 类:
- public final class Person {
- @NotNull
- private String name = "Paul";
- @NotNull
- public final String getName() {
- return this.name;
- }
- public final void setName(@NotNull String value) {
- this.setName(value);
- }
- }
复制代码
看到没, 方法循环调用了, setName 中又调用了 setName , 死循环了, 直到内存溢出, 程序崩溃. Kotlin 代码也一样, 在 setter 中又给属性赋值, 导致一直执行 setter, 陷入死循环, 直到内存溢出崩溃. 那么这个怎么解决了? 这就引入了 Kotlin 一个重要的东西幕后字段
2 . 幕后字段
千呼万唤始出来, 什么是幕后字段? 没有一个确切的定义, 在 Kotlin 中, 如果属性至少一个访问器使用默认实现, 那么 Kotlin 会自动提供幕后字段, 用关键字 field 表示, 幕后字段主要用于自定义 getter 和 setter 中, 并且只能在 getter 和 setter 中访问.
回到上面的自定义 setter 例子中, 怎么给属性赋值呢? 答案是给幕后字段 field 赋值, 如下:
- class Person {
- // 错误的演示
- var name = ""
- set(value) {
- field = value
- }
- }
复制代码
getter 也一样, 返回了幕后字段:
- // 例子一
- class Person {
- var name:String = ""
- get() = field
- set(value) {
- field = value
- }
- }
- // 例子二
- class Person {
- var name:String = ""
- }
复制代码
上面两个属性的声明是等价的, 例子一中的 getter 和 setter 就是默认的 getter 和 setter. 其中幕后字段 field 指的就是当前的这个属性, 它不是一个关键字, 只是在 setter 和 getter 的这个两个特殊作用域中有着特殊的含义, 就像一个类中的 this, 代表当前这个类.
用幕后字段, 我们可以在 getter 和 setter 中做很多事, 一般用于让一个属性在不同的条件下有不同的值, 比如下面这个场景:
场景: 我们可以根据性别的不同, 来返回不同的姓名
- class Person(var gender:Gender){
- var name:String = ""
- set(value) {
- field = when(gender){
- Gender.MALE -> "Jake.$value"
- Gender.FEMALE -> "Rose.$value"
- }
- }
- }
- enum class Gender{
- MALE,
- FEMALE
- }
- fun main(args: Array<String>) {
- // 性别 MALE
- var person = Person(Gender.MALE)
- person.name="Love"
- println("打印结果:${person.name}")
- // 性别: FEMALE
- var person2 = Person(Gender.FEMALE)
- person2.name="Love"
- println("打印结果:${person2.name}")
- }
复制代码
打印结果:
打印结果: Jake.Love
打印结果: Rose.Love
复制代码
如上, 我们实现了 name 属性通过 gender 的值不同而行为不同. 幕后字段大多也用于类似场景.
是不是 Kotlin 所有属性都会有幕后字段呢? 当然不是, 需要满足下面条件之一:
使用默认 getter / setter 的属性, 一定有幕后字段. 对于 var 属性来说, 只要 getter / setter 中有一个使用默认实现, 就会生成幕后字段;
在自定义 getter / setter 中使用了 field 的属性
举一个没有幕后字段的例子:
- class NoField {
- var size = 0
- //isEmpty 没有幕后字段
- var isEmpty
- get() = size == 0
- set(value) {
- size *= 2
- }
- }
复制代码
如上, isEmpty 是没有幕后字段的, 重写了 setter 和 getter, 没有在其中使用 field, 这或许有点不好理解, 我们把它转换成 Java 代码看一下你可能就明白了, Java 代码如下:
- public final class NoField {
- private int size;
- public final int getSize() {
- return this.size;
- }
- public final void setSize(int var1) {
- this.size = var1;
- }
- public final boolean isEmpty() {
- return this.size == 0;
- }
- public final void setEmpty(boolean value) {
- this.size *= 2;
- }
- }
复制代码
看到没, 翻译成 Java 代码, 只有一个 size 变量, isEmpty 翻译成了 isEmpty()和 setEmpty()两个方法. 返回值取决于 size 的值.
有幕后字段的属性转换成 Java 代码一定有一个对应的 Java 变量
3 . 幕后属性
理解了幕后字段, 再来看看幕后属性
有时候有这种需求, 我们希望一个属性: 对外表现为只读, 对内表现为可读可写, 我们将这个属性成为幕后属性. 如:
- private var _table: Map<String, Int>? = null
- public val table: Map<String, Int>
- get() {
- if (_table == null) {
- _table = HashMap() // 类型参数已推断出
- }
- return _table ?: throw AssertionError("Set to null by another thread")
- }
复制代码
将_table 属性声明为 private, 因此外部是不能访问的, 内部可以访问, 外部访问通过 table 属性, 而 table 属性的值取决于_table, 这里_table 就是幕后属性.
幕后属性这中设计在 Kotlin 的的集合 Collection 中用得非常多, Collection 中有个 size 字段, size 对外是只读的, size 的值的改变根据集合的元素的变换而改变, 这是在集合内部进行的, 这用幕后属性来实现非常方便.
如 Kotlin AbstractList 中 SubList 源码:
- private class SubList<out E>(private val list: AbstractList<E>, private val fromIndex: Int, toIndex: Int) : AbstractList<E>(), RandomAccess {
- // 幕后属性
- private var _size: Int = 0
- init {
- checkRangeIndexes(fromIndex, toIndex, list.size)
- this._size = toIndex - fromIndex
- }
- override fun get(index: Int): E {
- checkElementIndex(index, _size)
- return list[fromIndex + index]
- }
- override val size: Int get() = _size
- }
复制代码
AbstractMap 源码中的 keys 和 values 也用到了幕后属性
- /**
- * Returns a read-only [Set] of all keys in this map.
- *
- * Accessing this property first time creates a keys view from [entries].
- * All subsequent accesses just return the created instance.
- */
- 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!!
- }
- @kotlin.jvm.Volatile
- private var _keys: Set<K>? = null
复制代码
有兴趣的可以去翻翻其他源码.
4 . 本文总结
本文讲了 Kotlin 属性相关的一些知识点, 其中需要注意几个点:
1, 属性的访问是通过它的访问器 getter 和 setter, 你可以改变 getter 和 setter 的可见性, 比如在 setter 前添加 private, 那么这个 setter 就是私有的.
- var setterVisibility: String = "abc"
- private set // 此 setter 是私有的并且有默认实现
复制代码
2,Kotlin 自动提供幕后字段是要符合条件的(满足之一):
使用默认 getter / setter 的属性, 一定有幕后字段. 对于 var 属性来说, 只要 getter / setter 中有一个使用默认实现, 就会生成幕后字段;
在自定义 getter / setter 中使用了 field 的属性
3, 幕后属性的场景: 对外表现为只读, 对内表现为可读可写.
来源: https://juejin.im/post/5b95321ae51d450e6475b7c6