属性 or 字段
首先需要理解下属性和字段的区别:
字段是一个拥有值的类成员变量,可以是只读的或可变的,并可以用任何访问修饰符(例如public或private)进行标记。
属性包含一个私有字段和访问器(getter 和 setter),它也可以是只读或可变的。
var & val
Koltin 中可以通过关键字 var 和 val 来声明一个属性,完整语法如下所示:
1 | var <propertyName>[: <PropertyType>] [= <property_initializer>] |
var 是 variable 的缩写,表示该属性是可变的,val 是 value 的缩写,表示该属性是只读的,通过代码来看下,定义一个 Koltin 类:
1 | class Person { |
通过反编译 Kotlin 字节码为 Java 文件后,可以发现,var 声明的属性会自动生存 get 和 set 方法,而 val 声明的属性则只会生成 get 方法,并且还会加上 final。
1 | public final class Person { |
根据定义属性的完整语法,还可以在定义的时候添加自定义的 getter 和 setter:
1 | class Person { |
反编译看下 Java 代码:
1 | public final class Person { |
生成的 getName 和 setName 中的内容与之前定义的一致,但好像哪里不太对。。属性 name 去哪了?难道是因为声明的时候自定义了 getter 和 setter,但没有加初始值,所以给省略了么?话不多说,加上试试:
1 | class Person { |
很不幸,编译器提示有错误,看看报错信息:
Initializer is not allowed here because this property has no backing field
这个时候再反编译一下:
1 | public final class Person { |
诶。。。看到属性 name 了,但还缺少声明它的地方,结合之前的报错信息,那应该就是和这个 backing field
有点关系了。
幕后字段
上边说到的 backing field
即幕后字段,结合 Kotlin 文档来看,当 getter 和 setter 有一个为默认实现,或者在 getter 和 setter 中通过 filed
标识符引用幕后字段时,才会自动生成幕后字段,怎么理解呢?再看下上边的例子,如果我们改为:
1 | class Person { |
或是
1 | class Person { |
可以发现都不会再报错,反编译后,可以看到也有变量 name 生成:
1 | public final class Person { |
看到这里其实就很清楚了,幕后字段在有默认访问器的情况下,需要生成访问器,访问器里必定需要使用字段,而当自定义访问器里需要使用字段值时,也必须有该字段,否则就会存在在 getter 里调用 getter 这种递归调用的情况了。
幕后属性
首先来看文档给出的实例代码以及说明:
1 | private var _table: Map<String, Int>? = null |
对于 JVM 平台:通过默认 getter 和 setter 访问私有属性会被优化, 所以本例不会引入函数调用开销。
先是声明了一个私有的可变属性 _table,接着又声明了一个只读属性 stable,并在其 getter 中返回 _table。看下反编译后的 Java 代码:
1 | private Map _table; |
可以发现只有属性 _table ,而没有属性 table,这也对应了文档中的解释,在访问器中访问私有属性会被优化,因此,这里的优化就是不会去生成该属性。
因此可以看出 _table 属性对内是可变的,而对外是可读的,这就是“幕后属性”。那么它有什么作用呢?在 Koltin 中通过 private
修饰的属性是不会生成 getter 和 setter,其访问都是直接通过字段来访问,但如果此时需求是希望直接通过字段来访问,但同时又需要让外界可以获取到该属性值,那么就可以通过幕后属性来实现。