博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Kotlin 什么是幕后字段?
阅读量:5745 次
发布时间:2019-06-18

本文共 8429 字,大约阅读时间需要 28 分钟。

上篇文章我们了解了Kotlin中的各种类,,而类中则有属性和方法,Kotlin 中的类属性和Java的类成员变量还是有很大区别,同时类属性也有一些比较难以理解的东西,如:属性的声明形式、幕后字段、幕后属性等等。本篇文章我们将详细深入的了解这些东西。

1 . 前戏(Kotlin的普通属性)

在Kotlin中,声明一个属性涉及到2个关键字,varval

  • var 声明一个可变属性
  • val 声明一个只读属性

通过关键字var 声明一个属性:

class Person {    var name:String = "Paul"//声明一个可变属性,默认值为 Paul}复制代码

通过var 声明的属性是可以改变属性的值的,如下所示:

fun main(args: Array
) { var person = Person() // 第一次打印name的值 println("name:${person.name}") // 重新给name赋值 person.name = "Jake" //打印name的新值 println("name:${person.name}")}复制代码

打印结果如下:

name:Paulname:Jake复制代码

如果把name属性换成val声明为只读属性,在来改变的的值呢?

class Person {    val name:String = "Paul"}复制代码

可以看到,重新给val声明的属性赋值时,编译器就会报错Val cannot be reassigned ,它的值只能是初始化时的值,不能再重新指定。

这是Kotlin的两种声明属性方式,这不是很简单吗?一行代码。表面很简单,不过这一行代码包含的东西很多,只是没有显示出来而已,我们来看一下一个属性的完整声明形式:

// 可变属性var 
[:
] [=
] [
] [
]// 只读属性val
[:
] [=
] [
]复制代码

瞬间多了很多东西,其初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器 (或者从其 getter 返回值,如下文所示)中推断出来,也可以省略。也就是我们上面看到的属性声明,其实是省略了getter 和 setter 的,已默认提供

var name:String = "Paul" //  使用默认的getter 和setter复制代码

其中初始化的是一个字符串,因此可以从初始化起推断这个属性就是一个String类型,所以属性类型可以省略,变成这样:

var name = "Paul" //  能推断出属性类型,使用默认的getter 和setter复制代码

1. 2 getter & setter

在Kotlin中,gettersetter 是属性声明的一部分,声明一个属性默认提供gettersetter ,当然了,如果有需要,你也可以自定义gettersetter。既然要自定义,我们得先理解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中gettersetter 跟Java 中的getXX 和 setXX方法作用一样,叫做访问器

getter 叫读访问器,setter叫写访问器。val 声明的变量只有读访问器getter ,var声明的变量读写访问器都有。

Q: 在Kotlin 中,访问一个属性的实质是什么呢?

A: 读一个属性,通过.表示,它的实质就是执行了属性的getter访问器,举个例子:

class Person {    var name:String = "Paul"}//测试 fun main(args: Array
) { 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
) { 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
) { 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 = ""}复制代码

上面两个属性的声明是等价的,例子一中的gettersetter 就是默认的gettersetter。其中幕后字段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
) { // 性别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
? = nullpublic val table: Map
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 AbstractListSubList源码:

private class SubList
(private val list: AbstractList
, private val fromIndex: Int, toIndex: Int) : AbstractList
(), 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
get() { if (_keys == null) { _keys = object : AbstractSet
() { override operator fun contains(element: K): Boolean = containsKey(element) override operator fun iterator(): Iterator
{ val entryIterator = entries.iterator() return object : Iterator
{ 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
? = 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、幕后属性的场景:对外表现为只读,对内表现为可读可写。

以上就是本文全部内容,欢迎讨论。

更多Android干货文章,关注公众号 【Android技术杂货铺】

转载地址:http://inazx.baihongyu.com/

你可能感兴趣的文章
Linux系统程序包管理工具-RPM
查看>>
[JavaScript] 面向对象
查看>>
2017国赛小结
查看>>
Windows8/Silverlight/WPF/WP7/HTML5周学习导读(1月28日-2月3日)
查看>>
rsync工作方式介绍03
查看>>
PowerBI从SCCM数据库中分析数据和KPI展现
查看>>
SIEM/SOC用户的现状和诉求调查
查看>>
五一 带你观航母(附带视频)
查看>>
0.16版本salt的安装与日常应用
查看>>
Hyper-V用差异磁盘克隆系统
查看>>
Kerberos学习(二)
查看>>
2-Tenor AF AFT400-实战-Lync Server 2010-集成-2012-01-19
查看>>
SFB 项目经验-20-Skype for Business for Android-下载到电脑
查看>>
【翻译】优化基于ExtJS 4.1的应用
查看>>
从无到有,WebService Apache Axis2初步实践
查看>>
FOSCommentBundle功能包:设置Doctrine ODM映射(投票)
查看>>
RAID6结构原理详解
查看>>
可穿戴设备创业,卖Jawbone up2配件的故事
查看>>
Azure手把手系列6:存储服务介绍
查看>>
Hyper-V 3虚拟机快照之一 快照应用介绍
查看>>