前言(Kotlin的普通属性)
在Kotlin中,getter、setter 是属性声明的一部分,声明一个属性默认提供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
}
}
不好意思,一运行就会报错,直接栈内存溢出(stackOverFlowError)了,为什么呢?转换为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一个重要的东西:幕后字段。
幕后字段
千呼万唤始出来,什么是幕后字段? 没有一个确切的定义,在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
}
}
用幕后字段,我们可以在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变量。