Kotlin專題「八」:屬性與字段(Getter()與Setter(),後備字段field)

一、概述
  前面已經爲大家講解了類的使用以及屬性的相關知識,在一個類中基本上都會出現屬性和字段的,屬性在變量和常量的文章中有詳細講解到,這裏會重新簡單介紹到。

1.1 聲明屬性
Java 類中的變量聲明爲成員變量,而 Kotlin 中聲明爲屬性,Kotlin 類中的屬性可以使用 var 關鍵字聲明爲可變,也可以使用 val 關鍵字聲明爲只讀。類中的屬性必須初始化值,否則會報錯。

class Person {
    val id: String = "0" //不可變,值爲0
    var nameA: String? = "Android" //可變,允許爲null
    var age: Int = 22 //可變,非空類型
}

前面有提到 Kotlin 能有效解決空指針問題,實際上定義類型時增加了可空和非空的標誌區分,如上面聲明的類型後面有?表示屬性可爲空,類型後面沒有有?表示屬性不可爲空。如 name: String? 中 name 可以爲 null,age: Int 中 age 不可爲 null。在使用時,編譯器會根據屬性是否可爲空做出判斷,告知開發者是否需要處理,從而避免空指針異常。

Kotlin 中使用類中的屬性和 Java 中的一樣,通過類名來引用:

    var person = Person()//實例化person,在Kotlin中沒有new關鍵字
    view1.text = person.id //調用屬性

實際上上面定義的屬性是不完整的,在 Java 中的屬性定義還會涉及到 get() 和 set() 方法,那麼在 Kotlin 怎麼表示呢?

二、Getter()與Setter()
Kotlin 中 getter() 對應 Java 中的 get() 函數,setter() 對應 Java 中的 set() 函數,不過注意這僅僅是 Kotlin 的叫法而已,真正的寫法還是 get() 和 set()。

2.1 完整語法
在 Kotlin 中普通類中一般不提供 get() 和 set() 函數,因爲普通的類中基本用不到,這點和 Java 相同,但是 Java 在定義純粹的數據類時,會用到 get() 和 set() 函數,但是 Kotlin 這種情況定義了數據類,已經爲我們實現了 get() 和 set() 函數。

聲明屬性的完整語法如下:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

這是官方的標準語法,我們來翻譯一下:

var <屬性名> : <屬性類型> = 初始化值
    <getter>
    <setter>

其中,初始化器(property_initializer),getter 和 setter 都是可選的,如果可以從初始化器(或者 getter 返回類型)推斷出屬性的類型,那麼屬性類型(PropertyType)是可選的,如下所示:

//var weight: Int?//報錯,需要初始化,默認getter和setter方法是隱藏的

var height = 172 //根據初始化值推斷類型爲Int,屬性類型可以不需要顯示,默認實現了getter和setter方法

只讀屬性聲明與可變屬性聲明不同,它是 val 開始而不是 var,不允許設置 setter 函數,因爲它只是只讀。

val type: Int?//類型爲Int,必需初始化,默認實現了getter方法

val cat = 10//類型爲Int,默認實現getter方法

init {//初始化屬性,也可以在構造函數中初始化
    type = 0
}

Kotlin 中屬性的 getter 和 setter 函數是可以省略的,系統有默認實現,如下:

class Person {        
    //用var修飾時,必須爲其賦初始化值,即使有getter()也必須初始化,不過獲取的數值是getter()返回的值
    var name: String? = "Android"
        get() = field //默認實現方式,可省略
        set(value) { //默認實現方式,可省略
            field = value //value是setter()方法參數值,field是屬性本身
        }
}

其中,field 表示屬性本身,後端變量,下面會詳細講到,value 是 set() 的參數值,也可以修改你喜歡的名稱。set(value){field = value} 的意思是 set() 方法將設置的參數 value 賦值給屬性 field,上面的 getetter() 與 setter() 均爲默認實現方式,可以省略。

2.2 自定義
上面的屬性我們都省略了 getter 和 setter 方法。我們可以爲屬性定義訪問器,自定義 getetter() 與 setter() 可以根據自身的實際情況來制定方法值的規則。好比如 Java 中自定義 get() 和 set() 方法。

(1)val 修飾的屬性的 getter() 函數自定義

如果定義一個自定義 getter,那麼 getter() 方法會在屬性每次被訪問時調用,下面是自定義 getter 的例子:

    //用val修飾時,用getter()函數屬性指定其他值時可以不賦默認值,但是不能有setter()函數,等價 val id: String = "0"
    val id: String 
        get() = "0"   //爲屬性定義get方法

如果屬性的類型可以從 getter 方法中推斷出來,那麼類型可以省略:

class Person {
    //color根據條件返回值
    val color = 0
        get() = if (field > 0) 100 else -1  //定義get方法
    
    //isEmpty屬性是判斷 color是否等於0
    val isEmpty get() = this.color == 0 //Boolean類型,getter方法中推斷出來,可省
}

    //調用
    var person = Person()
    Log.e(TAG, "get()和set(): color == ${person.color} | isEmpty == ${person.isEmpty}")

color 默認爲0,get() 的數據爲-1,isEmpty 爲 false,打印數據如下:

get()和set(): color == -1 | isEmpty == false
1
(2)var 修飾的屬性的 getter() 和 setter() 函數自定義

自定義一個setter,將在每次爲屬性賦值的時候被調用,如下:

class Person {
    var hair: String = ""
        get() = field //定義get方法
        set(value) {//定義set方法
            field = if (value.isNotEmpty()) value else "" //如果爲不爲空則返回其值,否則返回""
        }

    var nose: String = ""
        get() = "Kotlin"//值一直是Kotlin,不會改變
        set(value) {
            field = if (value.isNotEmpty()) value else "" //如果爲不爲空則返回其值,否則返回""
        }
}

    var person = Person()
    person.hair = "Android"
    person.nose = "Android"
    Log.e(TAG, "get()和set(): hair == ${person.hair} | nose == ${person.nose}")

nose 中的 getter() 函數值已經固定了,不會再改變,打印數據如下:

get()和set(): hair == Android | nose == Kotlin
1
總結一下:
1.使用了 val 修飾的屬性不能有 setter() 方法;
2.屬性默認實現了 getter() 和 setter() 方法,如果不重寫則可以省略。

2.3 可見性
如果你需要改變訪問器的可見性或者註釋它,但不需要改變默認實現,你可以定義訪問器而不定義它的主體:

class Person {
    val tea: Int = 0
        //private set  報錯,val修飾的屬性不能有setter

    var soup: String = "Java"
        //@Inject set   用Inject註解去實現setter()

    var dish: String = "Android"
        //private get   報錯,不能有getter()訪問器的可見性

    var meal = "Kotlin"
        private set   //setter訪問器私有化,並且它擁有kotlin的默認實現
}

    var person = Person()
    //person.meal = "HelloWord"    報錯,setter已經聲明爲私有,不能重新賦值

如果屬性訪問器的可見性修改爲 private 或者該屬性直接使用 private 修飾時,只能手動提供一個公有的函數去改其屬性,類似 Java 中的 Bean.setXXX()。

三、後備字段與屬性
3.1 後備字段(Backing Fields)
後備字段相對 Java 來說是一種新的定義,不能在 Kotlin 類中直接聲明字段,但是當屬性需要後備字段時,Kotlin 有後端變量機制(Backing Fields)會自動提供,可以在訪問器中使用後備字段標識符 field 來引用此字段,即 Kotlin 中的後備字段用 field 來表示。

爲什麼提供後備字段?

class Person {
    var name: String = ""
        get() = "HelloWord"//值一直是Kotlin,不會改變
}

    var person = Person()
    person.name = "HelloUniverse"
    Log.e(TAG, "後備字段: goose == ${person.name}")

注意,我們明明通過 person.name= "HelloUniverse" 賦值,但是打印的依然是默認的 “HelloWord” 值,打印數據如下:

後備字段: name == HelloUniverse
1
上面的問題顯而易見,因爲我們定義了 Person 中的 name 的 getter() 方法,每次讀取 name 的值都會執行 get,而 get 只是返回了 “HelloWord”,那麼是不是直接用 name 替換掉 “HelloWord” 就可以了呢?我們來改造一下:

class Person {
    var name: String = ""
        //get() = "HelloWord" 
        get() = name //爲name定義了get方法,這裏返回name
}

    var person = Person()
    person.name = "HelloUniverse"
    Log.e(TAG, "後備字段: name == ${person.name}")

那麼上面代碼執行後打印什麼? “HelloUniverse”? 正確答案:不是。上面的寫法是錯誤的,在運行時會造成無限遞歸,直到java.lang.StackOverflowError棧溢出異常,爲什麼?

因爲在我們獲取 person.name 這個值的時候,都會調用 get() 方法,而 get()方法訪問了name 屬性(即 return name),這又會去調用 name 屬性的 get() 方法,如此反覆直到棧溢出。同樣,set() 方法也是如此,通過自定義改變 name 的值:

 class Person {
    var name: String = ""
        set(value) {//爲name定義了set方法
            name = value//爲name賦值一個新值,即value
        }
}

    var person = Person()
    person.name = "HelloUniverse"
    Log.e(TAG, "後備字段: name == ${person.name}")

同理:上面的代碼會拋出棧溢出異常,因爲 name = value 會無限觸發 name 的 set() 方法。

那麼我們怎麼在自定義屬性的get和set方法的時候在外部修改其值呢?

這就是後備字段的作用了,通過 field 可以有效解決上面的問題,代碼如下:

    var name: String? = "" //注意:初始化器直接分配後備字段
        get() = field//直接返回field
        set(value) {
            field = value//直接將value賦值給field
        }

    var person = Person()
    person.name = "HelloUniverse"
    Log.e(TAG, "後備字段: name == ${person.name}")

打印數據如下:

後備字段: name == HelloUniverse
1
如果屬性至少使用一個訪問器的默認實現,或者自定義訪問器通過 field 標識符引用該屬性,則將生成該屬性支持的字段。也就是說只有使用了默認的 getter() 或 setter() 以及顯示使用 field 字段的時候,後備字段 field 纔會存在。下面這段代碼就不存在後備字段:

    val isEmpty: Boolean
        get() = this.color == 0

這裏定義了 get() 方法,但是沒有通過後備字段 field 去引用。

注意:後備字段 field 只能用於屬性的訪問器。

3.2 後備屬性(Backing Properties)
如果你想做一些不適合後備字段來操作的事情,那麼你可以使用後備屬性來操作:

    private var _table: Map<String, Int>? = null//後備屬性
    public val table: Map<String, Int>
        get() {
            if (_table == null) {
                _table = HashMap()//初始化
            }
            //如果_table不爲空則返回_table,否則拋出異常
            return _table ?: throw AssertionError("Set to null by another thread")
        }

_table 屬性是私有的 private,我們不能直接使用,所以提供一個公有的後備屬性 table 去初始化 _table 屬性。這和 Java 定義 bean 屬性的方式是一樣的,因爲訪問私有屬性的 get() 和 set() 方法,會被編譯器優化成直接訪問其實際字段,不會引入函數調用的開銷。

四、編譯時常量
所謂編譯時常量,就是在編譯時就能確定值的常量。

4.1 編譯時常量與運行時常量的區別
與編譯時常量對應的還有運行時常量,在運行時才能確定值,編譯時無法確定其值,並放入運行常量池中。針對運行時常量,編譯器只能確定其他代碼段無法對其進行修改賦值。關於二者的區別,看下 Java 代碼:

    private static final String mark = "HelloWord";
    private static final String mark2 = String.valueOf("HelloWord");

定義了兩個常量:mark 和 mark2,那麼你覺得他們有區別嗎?大部分人認爲沒啥區別,都是常量。但是實際上是不一樣的,來看看它們的字節碼:

private final static Ljava/lang/String; mark = "HelloWord"
private final static Ljava/lang/String; mark2

我們發現,編譯後的 mark 直接賦值了 “HelloWord”,而 mark2 卻沒有賦值,實際上 mark2 在類構造方法初始化的時候才進行賦值,也就是運行時才進行賦值。這就是編譯時常量(mark)和運行時常量 (mark2)的區別!

4.2 編譯時常量
在 Kotlin 中,編譯時常量使用 const 修飾符修飾,它必須滿足以下要求:

必須屬於頂層Top-level,或對象聲明或伴生對象的成員;
被基本數據類型或者String類型修飾的初始化變量;
沒有自定義 getter() 方法;
只有 val 修飾的才能用 const 修飾。
//頂層top-level
const val CONST_STR = "" //正確,屬於top-level級別的成員
//const val CONST_USER = User() //錯誤,const只能修飾基本類型以及String類型的值,Point是對象

class Person {
    //const val CONST_TYPE_A = 0  //編譯錯誤,這裏沒有違背只能使用val修飾,但是這裏的屬性不屬於top-level級別的,也不屬於object裏面的

    object Instance { //這裏的Instance使用了object關鍵字修飾,表示Instance是個單例,Kotlin其實已經爲我們內置了單例,無需向 Java那樣手寫單例
        //const var CONST_TYPE_B = 0 //編譯錯誤,var修飾的變量無法使用const修飾
        const val CONST_TYPE_C = 0 //正確,屬於object類
    }
}

這些屬性還可以在註釋中使用:

const val CONST_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(CONST_DEPRECATED) fun foo() {
    //TODO
}

這裏基本包括了 const 的應用場景。但是有人會問,Kotlin 既然提供了 val 修飾符爲什麼還要提供 const 修飾符? 按理來說 val 已經可以表示常量了,爲什麼提供 const ?

4.3 const 與 val 的區別
下面代碼屬於 Kotlin 代碼,頂層的常量位於位於 kotlin 文件 Person.kt 中,屬於 top-level 級別:

const val NAME = "HelloWord"
val age = 20

class Person {
}

下面這段代碼是 Java 代碼,用於測試,建立一個類,在 main 函數數據:

public class ConstCompare {
    public static void main(String[] args) {
        //注意下面兩種的調用方式
        System.out.println(PersonKt.NAME);//這裏注意:kotlin文件會默認生成kotlin文件名+Kt的java類文件
        System.out.println(PersonKt.getAge());

        //編譯報錯,PersonKt.age的調用方式是錯誤的
        //System.out.println(PersonKt.age);
    }
}

上面的代碼證明了 const 修飾的字段和 val 修飾的字段的區別:使用 const 修飾的字段可以直接使用 類名+字段名來調用,類似於 Java 的 private static final 修飾,而 val 修飾的字段只能用get方法的形式調用。

那麼 const 的作用僅僅是爲了標識公有靜態字段?

不是,實際是 const 修飾字段 NAME 纔會變成公有字段(即public),這是 Kotlin 的實現機制,但不是因爲 const 才產生的 static 變量,我們來查看 Person 類的字節碼:

//PersonKt是 Kotlin 生成與之對應 的 Java 類文件
public final class com/suming/kotlindemo/blog/PersonKt {
  //注意下面兩個字段 NAME 和 age 的字節碼
  
  // access flags 0x19
  //Kotlin實際上爲 NAME 生成了public final static修飾的 Java 字段
  public final static Ljava/lang/String; NAME = "HelloWord"
  @Lorg/jetbrains/annotations/NotNull;() // invisible

  // access flags 0x1A
  //Kotlin 實際上爲 age 生成了private final static修飾的 Java 字段
  private final static I age = 20
    
  //注意:這裏生成getAge()的方法
  // access flags 0x19
  public final static getAge()I
   L0
    LINENUMBER 14 L0
    GETSTATIC com/suming/kotlindemo/blog/PersonKt.age : I
    IRETURN
   L1
    MAXSTACK = 1
    MAXLOCALS = 0

  // access flags 0x8
  static <clinit>()V  //Kotlin生成靜態構造方法
   L0
    LINENUMBER 14 L0
    BIPUSH 20
    PUTSTATIC com/suming/kotlindemo/blog/PersonKt.age : I
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0
  // compiled from: Person.kt
}

從上面的字節碼中可以看出:

1.Kotlin爲 NAME 和 age 兩個字段都生成了 final static 標識,只不過NAME 是 public 的,age 是 private 的,所以可以通過類名來直接訪問 NAME 而不能通過類名訪問 age ;
2.Kotlin 爲 age 生成了一個 public final static 修飾的 getAge() 方法,所以可以通過 getAge() 來訪問 age 。
總之, const val 與 val 的總結如下:
const val與 val 都會生成對應 Java 的static final修飾的字段,而 const val 會以 public 修飾,而 val 會以 private 修飾。同時,編譯器還會爲 val 字段生成 get 方法,以便外部訪問。

注意:通過 Android studio >Tools > Kotlin > Show Kotlin ByteCode 來查看字節碼。

五、延遲初始化的屬性和變量
  通常,聲明爲非空類型的屬性必須(在構造函數中)初始化。然而,這通常很不方便。例如:在單元測試中,一般在setUp方法中初始化屬性;在依賴注入框架時,只需要使用到定義的字段不需要立刻初始化等。

在這種情況下,不能在構造函數中提供一個非空初始化器,但是希望在引用類體中的屬性時避免null檢查。kotlin 針對這種場景設計了延遲初始化的機制,你可以使用 lateinit 修飾符來標記屬性,延遲初始化,即不必立即進行初始化,也不必在構造方法中初始化,可以在後面某個適合的實際初始化。使用 lateinit 關鍵字修飾的變量需要滿足以下幾點:

不能修飾 val 類型的變量;
不能聲明於可空變量,即類型後面加?,如String?;
修飾後,該變量必須在使用前初始化,否則會拋 UninitializedPropertyAccessException 異常;
不能修飾基本數據類型變量,例如:Int,Float,Double 等數據類型,String 類型是可以的;
不需額外進行空判斷處理,訪問時如果該屬性還沒初始化,則會拋出空指針異常;
只能修飾位於class body中的屬性,不能修飾位於構造方法中的屬性。
public class MyTest {
    lateinit var subject: TestSubject //非空類型

    @SetUp fun setup() {
        subject = TestSubject()//初始化TestSubject
    }

    @Test fun test() {
        subject.method()  //構造方法調用
    }
}

lateinit 修飾符可以用於類主體內聲明的 var 屬性(不在主構造函數中,並且只有當屬性沒有自定義getter和setter時才用)。自 Kotlin 1.2以來,可以用於頂級屬性和局部變量。屬性或變量的類型必須是非空的,而且不能是原始類型。在 lateinit 修飾的屬性被初始化之前訪問它會拋出異常,該異常清楚地標識被訪問的屬性以及沒有被初始化的事實。

自 Kotlin 1.2以來,可以檢查lateinit 修飾的變量是否被初始化,在屬性引用使用 this::變量名.isInitialized,this可省:

    lateinit var person: Person //lateinit 表示延遲初始化,必須是非空

    fun method() {
        person = Person()
        if (this::person.isInitialized) {//如果已經賦值返回true,否則返回false
            //TODO
        }
        Log.e(TAG, "延遲初始化: person.isInitialized == ${::person.isInitialized}")
    }

打印數據如下:

延遲初始化: person.isInitialized == true
1
注意:這種檢查只能對詞法上可訪問的屬性可用,例如:在相同類型或外部類型聲明的屬性,或在同一個文件的頂層聲明的屬性。但是不能用於內聯函數,爲了避免二進制兼容性問題。

至此,本文結束!常量和延遲初始化相關知識在前面的文章也有講解到。


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章