上篇文章咱们了解了Kotlin中的各类类,从Kotlin的类开始提及,而类中则有属性和方法,Kotlin 中的类属性和Java的类成员变量仍是有很大区别,同时类属性也有一些比较难以理解的东西,如:属性的声明形式、幕后字段、幕后属性等等。本篇文章咱们将详细深刻的了解这些东西。java
在Kotlin中,声明一个属性涉及到2个关键字,var
和 val
。bash
经过关键字var
声明一个属性:dom
class Person {
var name:String = "Paul"//声明一个可变属性,默认值为 Paul
}
复制代码
经过var
声明的属性是能够改变属性的值的,以下所示:jvm
fun main(args: Array<String>) {
var person = Person()
// 第一次打印name的值
println("name:${person.name}")
// 从新给name赋值
person.name = "Jake"
//打印name的新值
println("name:${person.name}")
}
复制代码
打印结果以下:ide
name:Paul
name:Jake
复制代码
若是把name
属性换成val
声明为只读属性,在来改变的的值呢?测试
class Person {
val name:String = "Paul"
}
复制代码
能够看到,从新给val
声明的属性赋值时,编译器就会报错Val cannot be reassigned
,它的值只能是初始化时的值,不能再从新指定。ui
这是Kotlin的两种声明属性方式,这不是很简单吗?一行代码。表面很简单,不过这一行代码包含的东西不少,只是没有显示出来而已,咱们来看一下一个属性的完整声明形式:this
// 可变属性
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
// 只读属性
val <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
复制代码
瞬间多了不少东西,其初始器(initializer)、getter 和 setter 都是可选的。属性类型若是能够从初始器 (或者从其 getter 返回值,以下文所示)中推断出来,也能够省略。也就是咱们上面看到的属性声明,实际上是省略了getter 和 setter 的,已默认提供spa
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一个重要的东西幕后字段
千呼万唤始出来,什么是幕后字段? 没有一个确切的定义,在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变量
理解了幕后字段,再来看看幕后属性
有时候有这种需求,咱们但愿一个属性:对外表现为只读,对内表现为可读可写,咱们将这个属性成为幕后属性。 如:
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
复制代码
有兴趣的能够去翻翻其余源码。
本文讲了Kotlin 属性相关的一些知识点,其中须要注意几个点:
一、属性的访问是经过它的访问器getter和setter, 你能够改变getter和setter 的可见性,好比在setter前添加private
,那么这个setter就是私有的。
var setterVisibility: String = "abc"
private set // 此 setter 是私有的而且有默认实现
复制代码
二、Kotlin 自动提供幕后字段是要符合条件的(知足之一):
使用默认 getter / setter 的属性,必定有幕后字段。对于 var 属性来讲,只要 getter / setter 中有一个使用默认实现,就会生成幕后字段;
在自定义 getter / setter 中使用了 field 的属性
三、幕后属性的场景:对外表现为只读,对内表现为可读可写。
以上就是本文所有内容,欢迎讨论。
更多Android干货文章,关注公众号 【Android技术杂货铺】