Kotlin入門(四)——類和對象的進階

本章內容包括:

  • 可空性
  • 數據類
  • 密封類
  • 枚舉類

0. 前言

在上一篇《Kotlin入門(三)——類、對象、接口》

我們只聊到了Kotlin中基本類的寫法以及繼承,但是我們說過,Kotlin的本質就是解決Java的繁瑣,如果Kotlin只有這麼簡單的話怎麼還能被稱爲Kotlin。

首先我們思考在Java中的幾個場景:

  • 在方法中每次都得對傳進來的對象進行判空,並且很多時候都會忘記判空或者不知道別人在調用你這個方法的時候到底會不會給空,然後就導致程序空指針異常了
void nullTest(Obj obj) {
    if (obj == null) {
        return
    }
    ...
}
  • 每次在Java中寫JavaBean的時候,一旦數據變多,就得寫一大堆的getter、setter、toString、equals等等方法
public class Person {
    private String firstName;
    private String lastName;
    private String telephone;
    private String address;

    public Person(String firstName, String lastName, String telephone, String address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.telephone = telephone;
        this.address = address;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getTelephone() {
        return telephone;
    }

    public void setTelephone(String telephone) {
        this.telephone = telephone;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

但是如果在Kotlin中,上面兩個問題還剛好可以通過可空性和數據類去解決。

那可能有的同學會問,你上面不還有一個密封類嗎,那他有啥方便之處呢?這個我們先賣個關子,我們放到後面來談這個。

1. 可空性

在Kotlin中,可空性是Kotlin和Java最顯著的區別之一,他能非常高效的幫助我們開發者去避免NullPointerException

Kotlin對於可空性的處理就是把這個運行時的錯誤轉成了編譯期的錯誤,這樣我們在編譯時就能發現很多存在的錯誤,從而減少運行時拋出異常的可能性。

1.1 可空類型

首先,Kotlin支持對可空類型的顯式。這句話可能讀起來覺得莫名其妙,其實簡單點說就是:這是一種可以直接指出你的程序中哪些變量和屬性允許爲null的方式。

我們還是從同樣的功能的代碼的Java版入手。

我們先來看下最常見的一種Java的代碼:

int strLen(String s) {
    return s.length();
}

恐怕這個代碼一寫出來,很多哪怕是新手的Java程序員都能指出他的問題:如果傳入的snull,這個程序就崩潰了。

那我們現在來試着用Kotlin去重寫這個函數,但是在重寫之前,我們首先得考慮我們調用這個函數的時候,傳入的實參,是否可以爲null。

如果我們不希望傳入的s爲null,我們就可以直接使用最基本的Kotlin函數的寫法:

fun strLen(s: String): Int {
    return s.length
}

這個時候如果我們在調用strLen的地方給他傳入一個null進去,我們甚至都不用編譯這段代碼,IDEA就會自動幫我們把這塊代碼給標註出來(報錯),不能傳入一個null進去:
Kotlin入門-四-1

在這個函數中,由於函數的形參被聲明爲了String(請注意:這個String只是String),所以Kotlin就會認爲你傳入的這個String類型的參數必須爲String的對象,而不可以爲null。

但是如果我們想讓它可以傳入null呢?這個時候我們就需要顯式的在類型名稱後面加上問號了:

fun strLen(s: String?): Int {
    return s.length
}

這個時候我們就可以直接像上圖中的那種方式去調用這個函數。

問號可以加在任何類型的後面,表示這個類型的變量可以爲null。

但是其實你像我上面說的那樣改了之後,其實IDEA也還是會報錯:
Kotlin入門-四-2

這是因爲如果你讓一個變量可空了之後,你就沒辦法直接對他進行操作,也不能把它賦值給非空類型的變量,也不能把可空類型的值傳給擁有非空類型參數的函數。

但是Kotlin和Java一樣,你只要在外面對s判斷不等於null了之後,就可以在if的函數體中對他直接進行操作了:

fun strLen(s: String?) = if (s != null) s.length else 0

但是這個時候,你一定會滿頭問號,因爲你一定會吐槽,這個代碼和Java有啥區別,Java甚至都不需要加問號(?)。

其實我講了這麼多,只是爲了引出Kotlin對於空的一大堆好用的操作,接下來,我們就先來說一下安全調用運算符。

1.2 安全調用運算符:?.

迴歸到剛纔那個問題,Kotlin是如何解決if (s != null)的。

其實要解決那個if(s != null)很簡單,就用?.就行了:

fun strLen(s: String?) = s?.length

其實?.他就是把null檢查和執行代碼合成了一個操作:
Kotlin入門-四-3

值得注意的是,圖裏面後面的兩個表達式其實是返回值,也就是說當snull的時候,s?.length返回值其實是null

安全調用不止可以調用方法,也可以用來訪問屬性。

但是這個時候你可能會說,這不對啊,Java的代碼的作用是當snull的時候返回0啊,但是你上面的那個Kotlin代碼當snull的時候,卻返回了null

我只能說你圖樣圖森破,其實這個套路和剛纔過度到?.的時候一樣,我們可以繼續用Kotlin給定的特殊語句(也就是Elvis運算符?:)去解決這個問題。

1.3 Elvis運算符:?:

fun strLen(s: String?) = s?.length ?: 0

同樣,也會有一個流程圖去讓你更容易理解這個代碼的流程,只不過這個時候我們需要將s?.length看做是一個整體:
Kotlin入門-四-4

我們可以簡化?:的用法,其實就是a ?: b,也就是說,當a的值不爲null的時候,就返回a,但是當a的值爲null的時候,就返回b

也就是說,當s?.length的值不爲null的時候,就返回s.length(因爲此處s?.length的值不爲null,所以就相當於s.length),但是如果當s?.length的值爲null的時候,就返回0

並且對於?:,我們其實還有一個非常方便的操作,就是當我們需要返回null或者需要拋異常的時候:

fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("name expected")
}

1.4 安全轉換:as?

我們在之前說到過,Kotlin主要通過as運算符來進行類型轉換。

但是和Java的類型轉換一樣,如果被轉換的值不是你試圖轉換的類型時,就會拋出ClassCastException異常。雖說可以結合is檢查來確定這個值擁有合適的類型,但是Kotlin一定會有更加優雅的方式。

as?就可以將值轉換成指定的類型,如果不是合適的類型就返回null
Kotlin入門-四-5

一種常見的模式就是可以用於重寫equals()方法:

class Person(val name: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person ?: return false
        return otherPerson.name == this.name
    }
}

1.5 非空斷言:!!

非空斷言是Kotlin提供的最簡單粗暴的一個處理可空類型的工具。他的作用就像他的樣子一樣,表示我就讓這個類型轉換成非空類型。
Kotlin入門-四-6

這種和Java一樣,所以也就沒啥好說的。

1.6 let函數

其實這個let函數屬於後面標準函數的內容,但是由於標準函數中的每一個函數都是服務於具體某個功能的,所以就放在功能這塊來說這個函數。

let函數天生就是爲?.服務的。

我們回到上面那個例子,如果我們想在返回slength之前先讓他刪除調最前面或者最後面的空格:

fun strLen(s: String?) = s?.let { str ->
    {
        str.trim()
        str.length
    }
}

一般情況,Kotlin的lambda表達式都會將語句的最後一句作爲return。

但是在Kotlin的lambda表達式中,我們可以用自動生成的名字it

fun strLen(s: String?) = s?.let {
    it.trim()
    it.length
}

至於爲啥是it,這個是Kotlin的lambda表達式的特殊字符,就類似於setter和getter中的field字段一樣的。

1.7 可空類型的集合

Kotlin也可以創建值爲null的集合,比如:

val nullsArray = arrayOfNulls<Int>(1) // 元素類型爲Int,容量爲1的初始值全爲null的數組
val nullsArrayList = ArrayList<Int?>() // 泛型爲Int?的ArrayList

如果你有一個可空類型元素的集合,並且想要過濾非空元素,你可以使用filterNotNull來實現:

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()

2. 數據類

我們在前言舉的例子說到過,Kotlin對於之前的飽受詬病的JavaBean做了一個非常好的處理。

對於這類只需要保存數據的容器,往往你都需要去重寫他們的一些方法,比如toString()equals()hashCode()等方法,這些方法的寫法又特別的機械,像Idea和eclipse都提供了自動生成的方法。

但是在Kotlin裏面,你就不必再去手動去操作這些方法了。在Kotlin中,這些類叫做數據類,並且只需要在class的前面添加data修飾符:

data class Person(val name: String, val age: Int)

在聲明爲數據類後,Kotlin就能自動的幫你重寫以下方法:

  • hashCode():這個方法不用多介紹了,和Java中一樣
  • equals():這個方法也不用多介紹了,和Java中一樣
  • toString():這個方法仍然不用多介紹了,和Java中一樣
  • componentN():這個函數是用來解構聲明的,見下文
  • copy():見下文

我們可以看一下上面那個類的字節碼(看字節碼的方法在此):

public final class Person {
   @NotNull
   private final String name;
   private final int age;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final int getAge() {
      return this.age;
   }

   public Person(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.age = age;
   }

   @NotNull
   public final String component1() {
      return this.name;
   }

   public final int component2() {
      return this.age;
   }

   @NotNull
   public final Person copy(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      return new Person(name, age);
   }

   // $FF: synthetic method
   public static Person copy$default(Person var0, String var1, int var2, int var3, Object var4) {
      if ((var3 & 1) != 0) {
         var1 = var0.name;
      }

      if ((var3 & 2) != 0) {
         var2 = var0.age;
      }

      return var0.copy(var1, var2);
   }

   @NotNull
   public String toString() {
      return "Person(name=" + this.name + ", age=" + this.age + ")";
   }

   public int hashCode() {
      String var10000 = this.name;
      return (var10000 != null ? var10000.hashCode() : 0) * 31 + this.age;
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof Person) {
            Person var2 = (Person)var1;
            if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

需要注意的是,如果上面的方法中任何一個已經有了顯式的實現,那麼數據類在生成的時候,就不會再去重新生成這個函數,而是會直接使用顯式的這個函數。

2.1 無參構造

如果數據類需要一個無參構造,那麼就需要對每個屬性都指定默認值:

data class Person(val name: String = "", val age: Int = 0)

這樣就會有一個無參構造。

2.2 在類中聲明的屬性

數據類也可以在類的裏面去聲明屬性:

data class Person(val name: String, val age: Int) {
    var address: String = ""
}

但是需要注意的是,這樣的話,數據類幫你生成的那些方法(equals()toString()等),都不會帶上address這個屬性。除非你自己顯式重寫對應的方法。

2.3 copy()

我們上面說到,數據類會自動幫我們生成copy()方法,那麼這個copy到底是幹嘛的呢?

其實說句實話,我覺得這個方法的話,也有點雞肋,也就是那種食之無味,但是又棄之可惜的東西(這麼說存在一定的絕對),但是也無所謂,能多點功能,能讓我們少寫點代碼當然是好的了。

好了,說回來,這個方法到底是幹嘛的呢?其實單看名字就能看出來,肯定與複製有關,但是他到底是複製啥呢。

其實在有些時候,如果我們需要生成這個類的另外一個對象,但是很多屬性都和這個類的原本的對象都是一樣的,我們只需要修改他其中的某一個屬性,那麼這個時候copy()就很有用了:

val jack = Person("Jack", 1)
val oldJack = jack.copy(age = 28)

我們就可以通過去調用jackcopy()方法,在參數中指定我們需要修改的屬性,這樣就可以返回一個除了指定的屬性外,其它屬性都和原對象一樣的一個新對象。

其實這個方法也還是非常有用的,但是我爲什麼又在上面說食之無味棄之可惜?因爲說實話,我覺得這個東西,我們日常使用的着實少,可以說少之又少,但是單看概念又挺有用,並且如果真的讓我們自己去重寫這個方法,雖然說在技術層面,實現這個方法着實簡單,但是一旦我們屬性多了起來之後,重寫起來還真的得花點功夫。

2.4 解構聲明

對於Java來說,解構聲明是一個新的東西,而這個,說實話,在我看來和上面那個copy()差不多,也是一個食之無味棄之可惜的東西。但是,這是相對於數據類來說的。我爲啥這麼說呢,繼續往下看就知道了。

首先我們直接上代碼,看下解構聲明到底是什麼:

val jack = Person("Jack", 1)
val (name, age) = jack
println("$name's age is $age")

其中第二行那就是解構聲明,也就是說,我們可以將某個對象的所有屬性給單獨拎出來。

看到這,是不是也會和我一樣產生一個感覺,這個東西着實意義不大,我們想去獲取某個屬性的話,直接調用這個類的屬性的getter不就行了嗎。

但是我剛剛說了,我覺得這個東西很雞肋,是針對於數據類來說的,下面我給你看個代碼你就會覺得這個東西非常有用了:

val map = HashMap<String, Person>()
for ((name, person) in map) {
    println("$name to (${person.name}, ${person.age})")
}

3. 密封類

其實密封類很簡單,沒啥特別的東西,

我們先來看一個例子:

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else ->
            throw IllegalArgumentException("Unknown expression")
    }

我們定義了一個父類(接口)Expr,以及他的兩個子類:代表數字的Num和代表和的Sum,然後我們在when中去處理所有的操作。

目前來說這樣很方便,但是其實有一點很多餘,就是我們完全沒有必要去寫when中的else分支,因爲他完全不可能是其他類型。並且如果我們新增了一個Expr的子類,萬一忘記了在when添加對應的分支,那麼程序就存在bug。

這個時候,我們的密封類就派上用場了。在Kotlin官方文檔,對密封類的定義很簡單:“密封類用來表示受限的類繼承結構:當一個值爲有限幾種的類型、而不能有任何其他類型時。”。也就是說上面這種情況,我們非常明確Expr不可能會再有其它的子類的,所以就沒有必要再去寫else分支。

而實現密封類也很簡單,在class前面加上sealed。接下來我們使用密封類改寫一下上面那個例子:

sealed class Expr
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        // 不再需要else,
        // 並且如果你寫了else,編譯器會提示你'when' is exhaustive so 'else' is redundant here
        // else ->
        //     throw IllegalArgumentException("Unknown expression")
    }

4. 枚舉類

對於枚舉類,Kotlin和Java的用法沒啥區別。

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

4.1 初始化

enum class Color(val rgb: Int) {
        RED(0xFF0000),
        GREEN(0x00FF00),
        BLUE(0x0000FF)
}

5. 嵌套類

5.1 嵌套類

在Java中,我們很多時候都會在一個類裏面再去聲明一個類,這種類就叫做內部類。

而Kotlin把這種類叫做嵌套類。簡單來說就是把這個類嵌套在了另一個類裏面:

class Person {
    private var age: Int = 0
    private var name: Name = Name("William", "Shakespeare")

    class Name(val firstName: String, val lastName: String)
}

但是,你如果把上面這段代碼翻譯成字節碼的話,你會發現其實這個嵌套類,他是static的,也就是說,他不持有外部類的引用,並且你在嵌套類中,沒法直接使用外部類的屬性或者方法。

所以,我們在Android中寫Handler的時候,我們就直接寫一個Handler的嵌套類就行了。

5.2 內部類

但是如果你就是想寫一個普通的內部類,就是一個沒有static修飾的內部類,那麼Kotlin就提供了一個inner關鍵字:

class Person {
    private var age: Int = 0
    private var name: Name = Name("William", "Shakespeare")

    inner class Name(val firstName: String, val lastName: String) {
        fun print() {
            println("$firstName $lastName's age is $age")
        }
    }
}

這樣,Name這個類,就是一個非static的內部類了,並且他會持有外部類Person的引用,所以我們可以直接訪問外部類的屬性,上面那個Name類中的print()方法纔可以去訪問Person的屬性age

5.3 匿名內部類

在Java中我們經常會使用到匿名內部類,比方說我們寫Callback回調的時候:

call.enqueue(new Callback<Translation>() {
    //請求成功時回調
    @Override
    public void onResponse(Call<Translation> call, Response<Translation> response) {
        // 對返回數據進行處理
        response.body().show();
    }

    //請求失敗時候的回調
    @Override
    public void onFailure(Call<Translation> call, Throwable throwable) {
        System.out.println("連接失敗");
    }
});

這個時候,enqueue傳入的就是一個繼承自Callback類的匿名內部類。

而在Kotlin中,也是差不多的形式,只不過我們需要借用下object關鍵字:

call.enqueue(object : Callback<T> {
    override fun onFailure(call: Call<T>, t: Throwable) {
        println("連接失敗")
    }

    override fun onResponse(call: Call<T>, response: Response<T>) {
        response?.body().show();
    }
})
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章