上篇文章咱們瞭解了Kotlin中的各類類,從Kotlin的類開始提及,而類中則有屬性和方法,Kotlin 中的類屬性和Java的類成員變量仍是有很大區別,同時類屬性也有一些比較難以理解的東西,如:屬性的聲明形式、幕後字段、幕後屬性等等。本篇文章咱們將詳細深刻的瞭解這些東西。java
在Kotlin中,聲明一個屬性涉及到2個關鍵字,var
和 val
。bash
經過關鍵字var
聲明一個屬性:markdown
class Person { var name:String = "Paul"//聲明一個可變屬性,默認值爲 Paul } 複製代碼
經過var
聲明的屬性是能夠改變屬性的值的,以下所示:dom
fun main(args: Array<String>) { var person = Person() // 第一次打印name的值 println("name:${person.name}") // 從新給name賦值 person.name = "Jake" //打印name的新值 println("name:${person.name}") } 複製代碼
打印結果以下:jvm
name:Paul
name:Jake
複製代碼
若是把name
屬性換成val
聲明爲只讀屬性,在來改變的的值呢?ide
class Person { val name:String = "Paul" } 複製代碼
能夠看到,從新給val
聲明的屬性賦值時,編譯器就會報錯Val cannot be reassigned
,它的值只能是初始化時的值,不能再從新指定。oop
這是Kotlin的兩種聲明屬性方式,這不是很簡單嗎?一行代碼。表面很簡單,不過這一行代碼包含的東西不少,只是沒有顯示出來而已,咱們來看一下一個屬性的完整聲明形式:測試
// 可變屬性 var <propertyName>[: <PropertyType>] [= <property_initializer>] [<getter>] [<setter>] // 只讀屬性 val <propertyName>[: <PropertyType>] [= <property_initializer>] [<getter>] 複製代碼
瞬間多了不少東西,其初始器(initializer)、getter 和 setter 都是可選的。屬性類型若是能夠從初始器 (或者從其 getter 返回值,以下文所示)中推斷出來,也能夠省略。也就是咱們上面看到的屬性聲明,實際上是省略了getter 和 setter 的,已默認提供this
var name:String = "Paul" // 使用默認的getter 和setter 複製代碼
其中初始化的是一個字符串,所以能夠從初始化起推斷這個屬性就是一個String
類型,因此屬性類型能夠省略,變成這樣:spa
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技術雜貨鋪】