9012年,鐵汁你為什麼還不上手Kotlin?

LiuXi0314發表於2019-04-21

What is Kotlin?

科特林島(Котлин)是一座俄羅斯的島嶼,位於聖彼得堡以西約30公里處,形狀狹長,東西長度約14公里,南北寬度約2公里,面積有16平方公里,扼守俄國進入芬蘭灣的水道。科特林島上建有喀琅施塔得市,為聖彼得堡下轄的城市。

Kotlin是一門以kotlin島名命名的現代化程式語言。它是一種針對Java平臺的靜態型別的新程式語言。專注於與Java程式碼的互操作性,幾乎可以應用於現今使用Java的任何地方:服務端開發、Android應用等等。Kotlin可以很好的和所有現存的java庫和框架一起工作,且效能與Java旗鼓相當。

谷歌開發者社群做過一個問卷調查,大概有40%的Android開發者已使用過Kotlin。


Kotlin簡史

  • 2011年7月,JetBrains推出Kotlin專案。
  • 2012年2月,JetBrains以Apache 2許可證開源此專案。
  • 2016年2月15日,Kotlin v1.0(第一個官方穩定版本)釋出。
  • 2017 Google I/O 大會,Kotlin “轉正”。
  • 現在 Kotlin 已釋出至 V1.3。 kotlinlang.org/

1. Kotlin的主要特徵

  • 目標平臺:服務端、Android及任何Java執行的地方。
  • 靜態型別。
  • 函數語言程式設計與物件導向
  • 免費且開源

1.1 靜態型別

Kotlin與Java一樣是一門靜態型別程式語言,在編譯期就已經確定所有表示式的型別。但是與Java不同的一點是Kotlin不需要在原始碼中顯示的每個宣告變數的型別,其所擁有的 型別推導 特性使編譯器通過上下文推斷變數的型別。 如下

var xOld: Int = 1 
var x = 1
var yOld: String = "string"
var y = "string"
複製程式碼

1.2 函數語言程式設計與物件導向

在討論Kotlin重的函數語言程式設計前,我們先了解一下函數語言程式設計的核心概念:

  1. 頭等函式:把函式當作值來使用和傳遞,可以使用變數儲存它,也可以將其作為其他函式的引數來進行傳遞,或者將其作為函式的返回值。
  2. 不可變性:使用不可變物件,保證該物件的狀態在建立之後不會再變化。
  3. 無副作用:使用純函式,此類函式在輸入相同引數時會輸出同樣的結果,且不會改變其他變數的狀態,也不會與外界有任何互動。

由於Kotlin的首要目標是提供一種更簡介、高效、安全的替代java的語言,所以其擁有java的物件導向特性。同時其豐富的特性集也讓其支援函數語言程式設計的風格。主要特性如下:

  • 函式型別:允許函式接受其他函式作為引數,或者返回其他函式。
  • lambad表示式,使用最少的樣板程式碼傳遞程式碼塊,同時節省效能開銷。
  • 資料類,提供了建立不可變值物件的簡明語法。
  • 標準庫提供了豐富的API集合,使你可以用函數語言程式設計風格操作物件和集合。

示例程式碼:

fun main(args: Array<String>) {
    var hello: () -> Unit = { print("Hello world") }
    test(hello)
}

fun test(f: () -> Unit) {
    f.invoke()
}

>>>
Hello world
複製程式碼
fun main(args: Array<String>) {
    var square: (Int) -> Int = { it * it }
    test(10, square)
}

fun test(i: Int, f: (Int) -> Int) {
    print(f(i))
}

>>>
100
複製程式碼
//資料類
data class User(val name:String,val age:Int)
複製程式碼

1.3 免費且開源

Kotlin完全開源,可以自由使用,採用Apache2許可證;開發過程完全公開在Github上。

推薦的開發工具:IntelliJ IDEA、Android Studio、Eclipse。

2. Kotlin的設計哲學

務實

Kotlin 不是一門哲學性、研究性語言而是一門實用語言,它的的誕生是為了解決當前程式設計世界的許多問題,它的特性也是依據與許多開發者遇到的場景而選擇的。Kotlin開發團隊對於Kotlin能幫助解決實際專案問題的特性很有自信。

Kotlin也沒有強制使用某種特定的程式設計風格和正規化。由於其設計者是JetBrains,Kotlin的開發工具IntelliJ IDEA的外掛和編譯器乃是同步開發,且在設計語言特性時就考慮到了對工具的支援,所以毋庸置疑,Kotlin的開發工具對於開發者來說是及其有好的。

在我們編寫Kotlin過程中,良好的IDE會發現那些可以用更簡潔的結構來替換的通用程式碼模式,我們同時也可以通過研究IDE使用的語言特性,將其應用到自己的程式碼中。

簡潔

Java中常見的程式碼在gettersetter以及將建構函式的引數賦值給變數的操作都被設定為隱式的。

var name:String = ""
var name: String
    get() {
        return name
    }
    set(value) {
        name = value
    }
複製程式碼
class Person(name: String){
    init {
        print(name)
    }
}

class Student{
    constructor(name: String){
        print(name)
    }
}
複製程式碼

Kotlin豐富的標準庫也可以代替很多不必要的冗長的程式碼。

例:如下所示的list,需求為:輸出其中第一個包含字母 b 的item,我們來看看通過Java和Kotlin的分別是如何實現的。

List<String> list = new ArrayList<>();
        list.add("a");
        ...
        list.add("a");
        list.add("ab");
複製程式碼

Java

 for (String s : list) {
            if (s.contains("b")) {
                System.out.print(s);
                break;
            }
        }
複製程式碼

Kotlin

 print(list.find { it.contains("b")})
複製程式碼

僅需一行程式碼就輕鬆搞定了。

還有一個細節需要提醒大家,與很多現代語言一樣,Kotlin中 沒有;號。

越簡潔的程式碼寫起來花的時間越短,更重要的是,讀起來耗費的時間更短,便於提高你的生產力, 使你更快的達到目標。

安全

通常,我們聲稱一門語言是安全的,安全的含義是它的設計可以防止程式出現某些型別的錯誤。Kotlin借鑑於Java,提供了一些設計,使其使用起來更加安全。

  • 在Kotlin中,不必要去指定所有型別宣告,編譯器會自動推斷出來,也降低了程式執行時的出錯機率。
  • Kotlin中定義了可空符號?,將其用來定義變數是否為 null
   val s: String= ""  //不可為空
   val s1: String? = null  //可為空
複製程式碼
  • 有效避免ClassCastException。在Java中,我們把一個物件轉換成某一個型別時,需要先對其進行型別檢查,檢查成功後再將其轉換成該型別。一旦忘了檢查操作,就有可能會發生上述異常。在Kotlin中,檢查和轉換被融合成了一個操作,當你檢查符合標準時,不再需要額外的轉換,就可以呼叫屬於該型別的成員。
    if (value is String){
          print(value.toUpperCase())
    }
複製程式碼

互操作性

在Java專案中新增Kotlin程式碼時,不會影響我們的任何Java程式碼。同時,我們可以在Kotlin中使用Java的方法、庫。可以繼承Java的類,實現Java的介面,在Kotlin上使用Java的註解。

另外,Kotlin的互操作性使Java程式碼像呼叫其他Java類和方法一樣輕鬆的呼叫Kotlin程式碼,在專案任何地方都可以混用Java和Kotlin。

Kotlin最大程度的使用Java庫,使其互操作性更強。比如,Kotlin沒有自己的集合庫,它完全依賴的Java標準庫,並使用額外的函式來擴充套件集合功能,使它們在Kotlin中可以更加便捷的使用。

Kotlin的工具也對互操作性提供了全面支援。可以編譯任意混合的Java和Kotlin原始碼。兩種原始檔自由切換,重構某一種語言時,在另外一種語言中也會得到正確的更新。

兩種語言可以隨意拼接,就像呼吸一樣自然。

3. Kotlin基礎

3.1 變數

在Java中宣告變數時:

int a = 10
複製程式碼

在Kotlin中宣告變數時,引數的名稱和型別使用 分隔,型別在之後,稍後我們會看到函式宣告也是如此。

val a: Int = 10 //顯示指定變數型別
val a = 10 //型別宣告省略
複製程式碼

在Kotlin中所有的變數必須有初始值,如果沒有也必須通過延遲初始化或者懶載入方式,在呼叫變數之前完成賦值操作。

lateinit var name: String
複製程式碼
val name by lazy { "liuxi" }
複製程式碼

如果你細心的話,會發現在上述的例子中出現了兩種宣告變數的關鍵字:

  • var ——(來自variable)可變引用,對應Java普通變數(非final),值可以任意改變,型別不可再變。
  • val —— (來自value)不可變引用,對應Java中final變數,val宣告的變數在初始化之後不能再次賦值。 Kotlin推薦我們在預設情況下使用 val 宣告變數,在需要的時候再使用 var 宣告變數

3.1.1 便捷的字串模版

Kotlin為我們提供了更便捷的字串拼接方式供我們使用

val name = "liuxi"
fun age() = 12
val log = "name: $name age: ${age()}"

>>>
name: liuxi age: 12
複製程式碼

3.2 函式的定義和呼叫

我們先來看一下Kotlin中的Hello world。

fun main(args: Array<String>){ //main 是函式名,()內部是引數列表, {}內部是函式體
    print("Hello world")
}
複製程式碼

Kotlin使用fun關鍵字來定義函式。

接下來我們在看一下帶返回型別的函式:

fun max(a: Int,b: Int): Int{
    retun if (a > b) a else b 
}
複製程式碼

函式的返回型別被定義在 之後

Kotlin中 if 是有結果值的表示式,不是語句,同時被用來替換掉Java中三元運算子 (boolean值) ? (表示式1) :(表示式2)。

表示式和語句的區別是表示式有值,且可以作為另一個表示式的一部分來使用。

3.2.1 表示式函式體

如果一個函式的函式體是由單個表示式構成的,那麼這個函式可以去掉外層的括號{}return,在函式宣告和表示式中間用=連線。

fun max(a: Int,b: Int) = if(a > b) a else b
//=後面的內容可以被稱作表示式體
複製程式碼

藉助於Kotlin優秀的型別推導特性,在上述範例中,我們也省略掉了對函式返回型別的宣告。


從Kotlin到Java,很多概念都是類似的。Kotlin 為了讓函式更簡潔易讀,借鑑Java並做出了很多改,基本如下:

  • 定義了命名引數、預設引數值以及中綴呼叫的語法。
  • 通過擴充套件函式和屬性去適配Java庫。
  • 使用了頂層函式,區域性函式和屬性架構程式碼。

我們先來看看在Kotlin 中命名引數,預設引數值的使用

3.2.2 命名引數、預設引數值

命名引數指的是在呼叫Kotlin定義的函式是,可以顯示的表明一些引數的名稱。當然如果一個函式由多個引數組成,為了避免混淆,在你給其中一個引數之名名稱之後,其之後的所有引數都需要標明名稱。

fun createAccount(account: String,password: String,desc: String){
    ...
}

create("liuxi","123456","cool")//常規使用

create("liuxi",password = "123456", desc = "cool") //使用命名引數的方式呼叫

複製程式碼

預設引數值,顧名思義就是一個引數支援宣告預設值,用於避免過載的函式。

比如上述的createAccount()函式中的desc是一個非必要引數,這時我們就可以給其設定預設值。

fun createAccount(account: String,password: String,desc: String = ""){
    ...
}

createAccount("liuxi","123456","cool")

createAccount("liuxi","123456")//當使用者不想輸入這個引數時,我們可以直接最後一個引數省略掉。
複製程式碼

當然,預設引數值的寫法在Java中是不被支援的,呼叫此方法時仍需顯式的指定所有引數。為了Java也可以體會到此方法的便捷性,我們可以使用@JvmOverloads註解此方法。這樣在Java中會自動生成此方法的過載方法。

3.2.3 頂層函式和屬性

頂層函式和屬性的誕生是為了消除Java中靜態工具類的寫法。即Java中各式各樣的Util工具類。

在Kotlin中,我們可以將一些函式和屬性(它們的功能型別使得它們很難歸屬到某一個具體的類中)直接寫在程式碼檔案(.kt)的頂層,不屬於任何的類。這些函式和屬性依舊是包內的成員。如果從包外想訪問它,則需要Import。 我們來建立一個Log.kt檔案

package com.liuxi.fun

val KEY = "123456"

fun log(msg:String){  //不需要放置在類體中
    Log.d("liuxi_test", msg) 
}
複製程式碼

在Kotlin中使用此方法就比較便捷:

log("測試頂層函式")
複製程式碼

而在Java中,這個檔案會被編譯成一個以檔名命名的類,頂層函式會被編譯成這個類的靜態函式。我們也可以利用@JvmName註解去自定義類的名稱。

/*Java*/
package com.liuxi.fun

public class LogKt{
    
    public static final String KEY = "123456"
    
    public static void log(String msg){
        Log.d("liuxi_test", msg) 
    }
}
複製程式碼

3.2.4 擴充套件函式和屬性

Kotlin的一個特色就是可以平滑的與現有程式碼整合。當我們在專案中遇到Java和Kotlin混合使用的時候,為了便捷的使用一些基於Java的功能,我們可以利用擴充套件函式來對其進行優化。

理論上來說,擴充套件函式就是類的成員函式,只不過它定義在類的外面。我們來看一個TextView的擴充套件函式:結合SpannableStringTextView賦值包含一個Image的文字:

/*檔名SpanHelper.kt*/

/**
 * 將目標String的替換為Image
 *
 * @param mainBody
 * @param targetBody
 * @param drawableRes
 * @return
 */
fun TextView.setTextWithImageSpan(mainBody: String?, targetBody: String?, drawableRes: Int) {
    if (mainBody.isNullOrEmpty() || targetBody.isNullOrEmpty() || drawableRes <= 0) {
        return
    }
    val spannableStr = SpannableString(mainBody)
    val start = mainBody.indexOf(targetBody)
    val end = start + targetBody.length
    spannableStr.setSpan(ImageSpan(context, drawableRes), start, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
    text = spannableStr
}
複製程式碼

此擴充套件方法的呼叫示例:

textView.setTextWithImageSpan("酷酷的", "酷酷", R.drawable.icon_diamond_small)
複製程式碼

簡短的一行程式碼,我們就實現了給TextView設定一段包含圖片的文字。這個功能真的很酷~~~

接下來,我們來看一下在Java中如何呼叫擴充套件函式。

SpanHelperKt.setTextWithImageSpan(textView, "酷酷的", "酷酷", R.drawable.icon_diamond_small)
複製程式碼

因為這個函式被宣告為頂層函式,所以它被編譯成為靜態函式。textView被當做靜態函式的第一個引數來使用。

擴充套件函式不存在重寫,Kotlin會把其當作靜態函式來看待。

接下來我們來看一下擴充套件屬性:

//檔名IntUtil.Kt
val Int.square: Int
    get() = toInt() * toInt()

fun main(args:Array<String>){
    print(5.square)
}

>>> 25
複製程式碼

與擴充套件函式一樣,擴充套件屬性也像是接收者的一個普通成員屬性一樣。但是必須定義 getter 函式。因為沒有支援的欄位,所以沒有預設的getter 。同理因為沒有儲存的地方,所以也無法初始化。個人感覺擴充套件屬性和擴充套件函式類似,但是使用的便捷性比擴充套件函式略遜一籌。

//擴充套件屬性在Java中的呼叫
IntUtilKt.getSquare(5)
>>> 25
複製程式碼

3.2.5 區域性函式

一種支援在函式內部再定義一個函式的語法:

fun createAccount(name:String,password:String){

    fun lengthNotEnough(string:String) = string.length > 10
    
    if(lengthNotEnough(name) return
    if(lengthNotEnough(password) return
}
複製程式碼

這個功能的意義在於解決程式碼重複問題,儘可能的保持清晰的程式碼結構,提高可讀性。

小知識點:區域性函式可以訪問外層函式的引數。

3.3 對集合的支援:可變引數,中綴呼叫,庫的支援

3.3.1 可變引數

如下是Collections.kt中的一個方法

public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
複製程式碼

vararg修飾符在Kotlin中被用來宣告可以接受任意數量的引數。

3.3.2 中綴呼叫

一個很有趣的語法糖。 一下是我隨手寫的一個例子

infix fun TextView.copy(view: TextView) {
    this.text = view.text
} 
複製程式碼

我們使用infix 新增在只有一個引數的函式前面。上述例子是在一個擴充套件函式前面新增上了infix。 使用範例如下:

 val textView1 = ...
 val textView2 = ...
 textView1 copy textView2  //中綴呼叫式寫法,這樣textView2的內容就被複制到了textView1 中
 textView1.copy(textView2)//常規寫法,與中綴呼叫式寫法等價。
複製程式碼

中綴呼叫是一種特殊的函式呼叫,在使用過程中沒有新增額外的分隔符,函式名稱(copy)直接放置在目標物件和引數之間。

3.3.3 集合庫的支援

Kotlin中的集合與Java的類相同,但是對擴充套件函式、行內函數等對API做了大量的擴充套件,此處就不再一一闡述了。

4. 類

Kotlin中和Java中一樣,使用關鍵字 class 宣告類

class Person{
    
}
複製程式碼

若一個類沒有類體,可上述花括號可以省略

class Person
複製程式碼

另一個需要注意的地方是:Kotlin中類宣告中沒有了public。在Kotlin中,類預設為public和final的

4.1 構造方法

如你所知,在Java中,一個類可以宣告一個或者多個建構函式,在Kotlin中同樣如此。但是Kotlin對建構函式做了一些修改,引入的主構造方法(在類體外宣告,為了簡潔的初始化類的操作而被創造出來)和從構造方法(與Java構造方法類似,在類體中宣告)併為此新增了兩個關鍵字 initconstructor

關鍵字constructor用來宣告 構造方法

下面是一個用Java式思想和kotlin定義的新語法寫出來的Person類

class Person{
    val name: String 
    constructor(name: String){
        this.name = name
    }
}
複製程式碼

接下來我們對其引入主建構函式的概念和 init關鍵字:

class Person constructor(name: String){ //帶一個引數的主構造方法
    val name: String
    init {   //初始化語句塊
        this.name = name
    }
}
複製程式碼

如上程式碼所示,init用來標示初始化語句塊,在類被建立的時候被執行,也可搭配主構造h函式使用。一個類中也可以宣告多個初始化語句塊,它們按照出現在類體中的順序執行。

接下來,我們在對Person類進行簡化:

class Person constructor(name: String){
    val name: String = name
}
複製程式碼

主建構函式的引數一樣可以使用 varval 宣告(從建構函式不可以),我們可以使用如下方式繼續簡化我們類的屬性定義:

//val 表示相應的屬性會用構造方法的引數來初始化
class Person constructor(val name: String)
複製程式碼

如果主建構函式沒有註解或者可見性修飾符,則我們可以將constructor省略。

class Person (val name: String)
複製程式碼

在 Kotlin 中的一個類可以有一個主建構函式以及一個或多個從建構函式。當一個類有主建構函式時,其類體中宣告的從建構函式需要通過直接或者間接的方式委託給主建構函式,此時就需要使用到this關鍵字。

class Person(val name: String = "liuxi"){
    constructor(name: String,age: Int): this(name)
    constructor(name: String,age: Int,gender:String): this(name, age)
}
複製程式碼

如果你想你的類不被其他程式碼例項化,則可以給建構函式新增可見型修飾符private

class Person private constructor()
複製程式碼
  • 建立的例項
val p = Person("liuxi")
複製程式碼

注:Kotlin中宣告物件不需要使用 new 關鍵字

4.2 類的繼承結構

我們先來認識幾個基礎概念:

介面 : 使用 inerface 定義

interface OnClickListener{
    fun onClick()
    fun test(){ //帶有預設方法體的方法
        print("Hello")
    }
}
複製程式碼

抽象類: 使用abstract修飾

abstract class Game{
  abstract fun play()
  
  open fun record(){ //關鍵字open用來表示這個方法可以被重寫,open也可以被用來修飾類。
      print("score")
  }
  
  fun test(){
      print("test")
  }
}
複製程式碼

接下來我們來展示一下如何繼承一個抽象類

class A: Game(){
    override fun play() = print("Go play")
    
    override fun record(){
        super.record() //super 關鍵字用來標示對父類或者介面的預設方法體的引用,類似於Java
    }
}
複製程式碼

接下來我們在讓類A實現介面OnClickListener

class A: Game(), OnClickListener{
    override fun play() = print("Go play")
    
    override fun record(){
        super.record() //super 關鍵字用來標示對父類或者介面的預設方法體的引用,類似於Java
    }

   override fun onClick()
   
   override fun test(){
       super.test()
   }
}

複製程式碼

如上所示,無論是繼承還是實現介面,我們都統一通過 : 來實現,沒有使用Java中的 extendsimplements

其中還需要注意下類的繼承,和介面的實現有一個區別,被繼承的類名後帶了(),它值得是類的構造方法。如果你的類宣告瞭主構造方法且含有引數,則子類也被強制要求實現主構造方法。此定義類似於Java的子類必須實現父類構造方法,且將父類構造所需要的引數傳遞給父類。

open class A(name:String) //一個類如果非抽象類,則預設是final,不可以被繼承,但是用open修飾後就可以了。

class B(name: String): A(name)
複製程式碼

openfinal 是兩個互斥定義的關鍵字。

還有一個細節也需要我們注意,當你override父類或者介面的方法後,它們預設是open的,如果你不希望你的子類再去修改它,需要用final修飾

class A: Game(), OnClickListener{
    final override fun play() = print("Go play")
   ...
}

複製程式碼

4.3 資料類、內部類、巢狀類、密封類

資料類

data class Person(val name)
複製程式碼

內部類和巢狀類

Java:

class A{
        public String name = "liuxi";

       static class B{
            void test(){
                System.out.print(name);//此操作被拒絕
            }
        }

        class C{
            void test(){
               System.out.print(name); 
            }
        }
    }
複製程式碼

Kotlin:

class A{
    var name = "liuxi"
    
    class B{
        fun test(){ //為巢狀類
            //print(name),此操作被拒絕,不持有外部類的例項,相當於Java 中的 static class B
        }
    }
    
    inner class C{
        fun test(){ //為巢狀類
            print(name) //持有外部類的例項 
        }
    }
}

複製程式碼

密封類 : 定義受限的類繼承結構。這是為了解決when結構必須要實現一個else分支(類似於Java switch case中的default)而提出的解決方。給父類新增 sealed 修飾符且所有的直接子類必須巢狀在父類中。

sealed class Children{
    class Boy: Children()
    class Girl:Children()
}
複製程式碼

4.4 object關鍵字

object 關鍵字在Kotlin中有主要有三個使用場景,其核心理念是:object定義一個類同時建立一個例項。 接下來,看一下三個場景各是什麼:

  1. 在Kotlin中使用物件宣告去去建立單例:引入object 關鍵字,通過一句話去定義一個類和一個該類的變數。在編譯過程中,該變數的名字始終都為INSTANCE
object Utils{     //由於類在定義的時候就立即建立了該類的變數,所以該類不被允許擁有建構函式,除此之外,其他地方都和普通類一樣。
    fun test(){
        ...
    }
    val TEST = "test"
}
複製程式碼

沒有在Java中的各種樣式書寫的單例模式,Kotlin僅需一行程式碼,輕鬆搞定。

kotlin中呼叫如下:

Utils.test()
Utils.TEST
複製程式碼

在Java中呼叫如下:

Utils.INSTANCE.test()
Utils.INSTANCE.TEST
複製程式碼

需要通過.INSTANCE引用物件宣告中的方法,類似於我們在Java單例模式中習慣去建立的mInstance

  1. 伴生物件在Kotlin中的使用

Kotlin沒有保留Java中的static關鍵字,為了補足靜態函式和屬性的功能,Kotlin提出了伴生物件,頂層函式和屬性(也叫包級函式和屬性),我們先來講解伴生物件,後者在之後會被提到。

伴生物件是一個宣告在類中的普通物件,可以擁有名字,也可以實現介面或者有擴充套件函式或屬性。

class Person(val name: String){
    
    companion object Loader{
        fun test(){
           ... 
        }
        
        val type:String = "simple"
    }
}
複製程式碼

如上範例所示:companion關鍵字被用來標記伴生物件,伴生物件的名字可以被省略。簡易寫法如下:

class Person{
    companion object{
        ...
    }
}
複製程式碼

我們可以把伴生物件當作Java中的靜態物件來看待,被定義為伴生物件後,我們就可以通過容器類的名稱來獲取這個物件的呼叫了。示例如下:

Person.companion.test()
複製程式碼
  1. object 來實現匿名內部類

object 在類體內部,可以被用來宣告匿名物件 (替代了Java中的匿名內部類用法)。

常見使用場景有:將一個介面或者抽象類宣告為匿名物件。

如下是將我們常見的 OnClikListener 宣告為匿名物件:

view.setOnClickListener(object: OnClickListener{
    override onClick(v:View){
        
    }
})
複製程式碼

5. 一些很有特色的語法

5.1 when

Java中使用swich語句來完成對一個值可能出現的不同情況的作出對應處理。在Kotlin中我們使用when來處理。它比switch擁有更多的使用場景,功能更加強大,使用的也更加頻繁,且還可以和智慧轉換 完美的結合使用。

when和if一樣,都是表示式,可以擁有返回值。

舉個例子來說:當下有一個需求,我們需要根據不同vip等級返回不同的drawable,供給ImageView使用。

    val imageView: ImageView = ...
    val level: Int = ...
    var imageView = imageView.setImageResource(
    when (level) {
        1 -> R.drawable.icon_level_1
        2 -> R.drawable.icon_level_2
        3 -> R.drawable.icon_level_3
        4 -> R.drawable.icon_level_4
        5 -> R.drawable.icon_level_5
        else -> {
            R.drawable.icon_level_default
        }
    })
複製程式碼

when語句對於level的不同情況,when返回不同分支上的值交給setImageResource()去使用。

值得一提的是,when 支援在一個分支上合併多個選項,比如我們對上述例子做出修改,將1、2定義為上,3定義為中,4、5定義為下,我們使用 when寫出的程式碼如下:

   ...
    when (level) {
        1,2 -> R.drawable.icon_level_top
        3 -> R.drawable.icon_level_middle
        4,5 -> R.drawable.icon_level_bottom
        else -> {
            R.drawable.icon_level_default
        }
    }
複製程式碼

whenswitch強大的地方在於: 前者允許使用任何物件作為分支條件,甚至包括無參。但是後者必須使用列舉常量、字串或者數字字面值。

open class BaseActivity{
  ...  
}

class MainActivity: BaseActivity(){
    ...
    fun mainPrint(){
        print("main")
    }
}

class SettingActivity: BaseActivity(){
    ...
}

複製程式碼
fun getTitle(activity: BaseActivity):String = when(activity){
    is MainActivity -> {
        activity.mainPrint()
        "main"        //if 和 when 都可以使用程式碼塊作為分支體,在示例情況下,程式碼塊中最後一個表示式就是返回的結果
    }
    is SettingActivity -> "Setting"
     
     else -> {
         ""
     }
}
複製程式碼

is :用來檢查判斷一個變數是否是某種型別,檢查過後編譯器會自動幫你把該變數轉換成檢查的型別,你可以直接呼叫該型別下的方法和變數,此功能可以被稱作 智慧轉換 。它替我們省略掉了Java 中的 instanceOf 檢查後的型別強制轉換這一步,型別判斷和型別轉換二合一的操作也使我們的程式碼更加安全。當然,如果你仍然需要顯式的型別轉換,Kotlin提供了 as 來表示到特定型別的顯示轉換。

var a = activity as Maintivity
複製程式碼

接下來我們從一個例子來認識一下 when 的無參寫法。

我們需要根據兩人團中成員的性別分別給出型別定義,若是都為男性則稱作男團、都為女性則成為女團、男女混合則稱作二人轉

/*常量*/
const val BOY = 0
const val GIRL = 1
const val BOY_GROUP = "男團"
const val GIRL_GROUP = "女團"
const val MIX_GROUP = "二人轉"
複製程式碼

我們先給出 if 語句的寫法:

fun getGroupName(gender1: Int, gender2: Int) =
        if (gender1 != gender2) {
            MIX_GROUP
        } else if (gender1 == BOY) {
            BOY_GROUP
        } else {
            GIRL_GROUP
        }
複製程式碼

我們先給出 when 語句的寫法:

fun getGroupName2(gender1: Int, gender2: Int) =
        when {
            gender1 != gender2 -> MIX_GROUP

            gender1 == BOY -> BOY_GROUP

            else -> GIRL_GROUP
        }
複製程式碼

when語句的功能特別豐富,類似於Java中的switch但是功能更加強大。

5.2 Kotlin的可空性

可空性 是Kotlin型別系統中幫你避免NullPointerException錯誤的特性。

在Java中我們遇到過太多太多的 java.lang.NullPointerException,Kotlin解決這類問題的方法是將執行時的異常轉換為編譯時的異常。通過支援作為型別學系統的一部分的可控性,編譯器就能在編譯期間發現很多潛在的錯誤,從而減少執行時丟擲空指標異常的可能性。

如下是一個獲取字串長度的方法

fun getLength(str: String?){  //我們需要在那些可能為null的實參後面加上問號來標記它
    if(str == null){
        return 0
    }
    return str.length
}
複製程式碼

問號 ? 可以加在任何型別的後面,來表示這個型別的變數可以儲存null引用。相反沒有被 ?

標記的變數不能儲存null引用。如果你對一個不接收null引用的引數賦值為null,程式會直接丟擲異常 Null can not be a value of a non-null type

可空型別和非空型別在Kotlin中有著很嚴格的區分。一旦你的變數被定義為可空型別,對它的操作也將收到限制,你將無法呼叫它的方法,除非你能證明它不為空;也不能將它賦值給非空型別的變數。也不能將可空型別的引數傳給擁有非空型別引數的函式。

那麼我們可以對可空型別引數做什麼呢?我們對於可空型別引數,最重要的就是和null進行比較。一旦你進行了比較操作,編譯器就會記住。並且在此次比較發生的作用域內把這個值當作非空來對待(我們就可以呼叫非空型別下的引數和函式了),如上述例子。

可空的和非空的物件在執行時沒有什麼區別。可空型別並不是非空型別的包裝。所有的檢查都發生在編譯器。即使用Kotlin的可空型別在執行時不會帶來額外的開銷。

5.2.1 安全呼叫運算子:?.

安全呼叫運算子?.允許把一次null檢查和一次方法呼叫合併成一個操作。

str?.toUpperCase()

if(str != null) str.toUpperCase() else null     //等效於str?.toUpperCase()
複製程式碼

安全運算調符只會呼叫非空值的方法。 安全呼叫符的結果型別也是可空的。在上述例子中返回型別為String?

5.2.2 Elvis 運算子 ?:

在引入可空性概念後,就代表了Kotlin中會出現很多預設值為null的可空型別。為了避免預設值weinull帶來的麻煩,Kotlin提供了 Elvis運算子 ?: (null合併運算子)

下面展示了它是怎麼使用的:

fun toStr(s: String?){
    val str =  s ?: "" // 如果s為null,則賦值為""
    
    var length = s?.length ?: 0  //常見使用場景,和安全呼叫運算子搭配使用
}
複製程式碼

5.2.3 非空斷言!!

一個很暴力的語法!非空斷言是最簡單直率的處理可空型別的工具。它使用!!雙歎號表示。

var length = s!!.length 
複製程式碼

當一個使用非空斷言的變數為null時,會顯式的丟擲空指標異常。除非你對你的邏輯很有自信,不要輕易使用這個工具。 常見使用場景是你在一個函式使用一個值時對其進行了非空判斷,而在另一個函式中使用這個值時,不想重複進行非空判斷,這時你就可以使用非空斷言。

5.2.3 let函式

let 函式讓可空表示式的處理變得更加容易。它會把呼叫它的物件變成一個lambda表示式。

和安全呼叫運算子一起使用時,lambda中的it為該物件的非空值,如果該物件為null則什麼都不會發生,不為空時會執行lambda表示式

str?.let{ print(it.length)}

/* 以下是為了增加可讀性,簡單格式化後的寫法*/
str?.let{ it-> 
    print(it.length)
}

/*常規寫法的等效語句*/
if(str != null){
     print(it.length)
}
複製程式碼

let生成的lambda語句中的it代表著呼叫物件的非空值

6. 總結

本文到此也基本告一段落了,寫下這邊文章的主要目的是想讓更多的對Kotlin有興趣的開發人員、在Kotlin和Java中徘徊糾結的人們還有那些對Kotlin抱有各種觀念的人簡單瞭解一下Kotlin,激發起大家學習Kotlin的興趣,也拋開對Kotlin的偏見。看完此篇文章你應該也可以簡單上手Kotlin了,快快寫起來吧~

文章的定義是入門講解,因此很多地方講解的不夠完善,也不夠詳細。希望那些對Kotlin有興趣的人在學習Kotlin過程中最好還是能夠系統的學習一邊Kotlin的基礎知識以及進階語法。文章絕大部分知識點參照於《Kotlin實戰》,推薦入門學者翻閱。

一些想寫但沒加入到文章中的知識點(有些部分是個人掌握不熟練,怕描述不準確,還有一點是基於篇幅考慮,怕寫的太長嚇跑新人)

  1. Kotlin中的lambda表示式,這部分的內容十分豐富,還有很多簡單便捷的語法糖。
  2. Kotlin 基本資料型別,有點懶就沒寫,希望可以自行翻閱。
  3. Kotlin中的集合和陣列 以及迭代相關知識(for 、區間、數列、map迭代、in關鍵字)。
  4. Kotlin的高階函式和泛型。
  5. 協程(coroutines)是在Kotlin1.3版本中正式釋出,一定要學習~

歡迎大家多多討論,講的晦澀或描述的有問題的地方也懇請大家指正,一起進步~~~~///(^v^)\~~~

相關文章