面向對象:屬性

Kotlin 中的屬性(field)比 Java 更加複雜,主要因爲以下幾點:

  1. Kotlin 中允許 包級屬性 的存在,即屬性不一定在類裏;

  2. 所有非抽象屬性都強制要求初始化,沒有初始化的屬性無法通過編譯(除標記爲 lateinit var 的屬性外);

  3. 標準化的 getter 和 setter;

  4. 大量的高級屬性修飾符。

1. 類內聲明屬性

首先看一下在類裏面聲明屬性的完整格式吧:

[修飾符] val|var 屬性名[: 屬性類型][= 初始化語句]
   [get() = getter 語句]
   [set() = setter 語句]

這裏有幾個小細節:

  1. 屬性不能缺少初始化語句,要麼寫在定義屬性的地方,要麼寫在 init 語句裏,否則就會編譯錯誤

  2. 標記爲 val 的屬性不能有自定義的 setter,這個很容易理解,因爲 Kotlin 強制要求屬性初始化,標記爲 val 的屬性已經在創建對象的時候初始化了,不能再次賦值。

我們用幾個例子說明一下:

// 初始化語句class Person(val name: String) {
 val age: Int
 val id: Long = 0
 val nationality: String
 
 init {
   age = 0
 }}

這個 Person 類定義了 4 個屬性,各有不同的初始化方式:

  • name 屬性在主構造方法裏定義,使用轉入的參數初始化;

  • age 屬性在 init 語句裏初始化;

  • id 屬性在定義屬性時初始化;

  • nationality 屬性不是抽象屬性,沒有在定義時初始化,也沒有在 init 裏初始化,編譯錯誤。

2. getter 和 setter

Kotlin 把 Java 中沒有固定標準的 getter 和 setter 方法標準化,並且規定調用 Kotlin 類的屬性時強制使用 setter 和 getter 方法,不會直接操作類的屬性。

需要注意,Kotlin 和 Java 訪問 name 屬性的寫法是不一樣的:

// Kotlin 訪問屬性val p = Person("Alex")println(p.name)p.name = "Bob"
// Java 訪問屬性Person p = new Person("Alex");// System.out.println(p.name); 編譯錯誤,無法訪問 private 屬性System.out.println(p.getName());p.setName("Bob");

不對吧,不是說 Kotlin 不允許直接操作類屬性嗎,爲什麼 Kotlin 中可以用“對象.屬性”的寫法呢?這是因爲 Kotlin 中的“對象.屬性”會視情況自動編譯爲調用 getter 或 setter 方法

同時,Kotlin 允許屬性添加自定義的 getter 和 setter:

class Person(name: String) {
   var name = name
       set(value) {
           field = if (value.isEmpty()) "" else value[0].toUpperCase() + value.substring(1)
       }

   val isValidName
       get() = !name.isEmpty()}

這裏有幾個細節:

  1. 需要自定義 setter 或 getter 的屬性,不能放在類頭裏定義,必須在類體內定義,要不然 Kotlin 怎麼知道你自定義了哪個屬性的 getter 和 setter 呢?所以這裏我們把原本在類頭裏定義的 name 屬性挪到了類體內定義,並使用傳入的 String 類型參數初始化 name 屬性,name 屬性被自動推導爲 String 類型;

  2. getter 是一個沒有參數、返回類型與屬性類型相同的函數。完整的寫法應該是這樣的:

    get(): 屬性類型 {
     //……}

    但是一般 getter 的方法比較短,而且可以自動推導類型,所以如果只有一句的話可以寫成“get() = 函數語句”的形式。需要注意一點,不能在 getter 裏再調用本屬性,因爲 Kotlin 代碼裏所有對屬性的訪問都會被編譯爲 getter 方法,這樣寫就會出現無限迭代和 StackOverFlowError

  3. setter 的參數列表一般有一個與屬性類型相同的參數,沒有返回值

    Kotlin 中一般用 value 表示這個參數,當然也可以用其他關鍵字;

    這裏的 field 是表示 幕後字段 的關鍵字,它在使用時相當於 this.name,但是隻能用在 setter 方法內。

3. 類外屬性

在 Kotlin 類外定義的屬性有兩種,一是直接寫在類外並初始化的包級屬性,二是使用 const val 定義的 編譯期常量

  1. 在類外定義的包級屬性,會被編譯爲一個“文件類”的靜態變量

    // oop.Person.ktclass Person(val name: String)val maxID = Int.MAX_VALUE

    編譯這個文件,實際上會形成兩個類,一個是我們定義的 Person 類,另一個是“文件類”PersonKt。反編譯這個類的字節碼,我們發現它是這樣的:

    public final class PersonKt {
     private static final int maxID = Integer.MAX_VALUE;
     public static final int getMaxID() {
       return maxID;
     }}

    maxID 被編譯爲這個類私有的靜態字段,並擁有一個默認的 getter 方法。要調用這個屬性,Kotlin 和 Java 代碼有所不同,如下:

    // Kotlin 調用包級屬性import oop.maxID // 因爲是包級變量,所以使用“包名.屬性名”的方式導入val a = maxID // 直接調用,不需要標明變量所在的文件
    // Java 調用包級屬性import oop.PersonKt; // 導入自動生成的“文件類”int a = PersonKt.getMaxID(); // 使用默認的 getter 方法調用靜態屬性

  2. 編譯期常量使用 const val 定義在類外,它與包級屬性有一定的相似之處。我們把上面的 maxID 聲明爲編譯期常量:

    // oop.Person.ktclass Person(val name: String)const val maxID = Int.MAX_VALUE // 將 maxID 聲明爲編譯期常量

    再看一下反編譯的 PersonKt 類:

    public final class PersonKt {
      public static final int maxID = Integer.MAX_VALUE;}

    可以看到,編譯期常量其實就是 Java 中常用的常量。但與 Java 中的常量相比,Kotlin 的編譯期常量有以下幾點限制:

    還有,Kotlin 中,使用在註解參數中的屬性,只能是編譯期常量(其他形式的屬性都不能使用在註解的參數裏)

    const val DEPRECATED_MESSAGE = "This is deprecated."@Deprecated(DEPRECATED_MESSAGE) fun foo() {……}

    4. lateinit 修飾符

    Kotlin 強制要求類內定義的非抽象屬性都要初始化,但是有些屬性不需要在新建實例時初始化,或可能需要外部注入來初始化,這些時候我們都無法按照 Kotlin 的要求初始化屬性。爲了避免編譯錯誤,可以使用 lateinit 關鍵字修飾屬性。

    看下面的例子:

    class Person(val name: String) {
       lateinit var hello: String
       fun initHello() {
           hello = "Hello, my name is $name"
       }}

    我們使用 lateinit 關鍵字修飾 hello 屬性,定義了 initHello() 函數來初始化 hello 屬性。

    // Test.ktfun main(args: Array<String>) {
     val p = Person("Alex")
     println(p.hello)}

    如果調用了未初始化的 lateinit 屬性,就會拋出 UninitializedPropertyAccessException。我們修改下測試函數,在調用 hello 屬性前初始化屬性:

    // Test.ktfun main(args: Array<String>) {
     val p = Person("Alex")
     p.initHello()
     println(p.hello)}

    這樣,就能輸出正確的結果了:

    Hello, my name is Alex
  • 只能定義在類外或對象(Object)內;

  • 只能使用 String 或原生類型(Int、Double 等)初始化;

  • 不能自定義 getter(直接調用,不需要 getter)


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