Kotlin 語言學習筆記
像 Java, Objective-C 這些上個世紀的開發語言越來越沒法滿足現代移動開發的需要,所以要有一些更現代化的語言和編譯器,它能夠夠更像人類的語言,更符合人類的表達習慣,更加簡潔,更加富有語義性,能夠根據上下文環境判斷我們想表達東西並理解我們的簡述,能夠察覺或者糾正我們的語法錯誤,能夠有一些語法之內或之外的一些約定俗成的表達習慣,能夠很好地支援面向物件程式設計、函數語言程式設計以及響應式程式設計,最重要的還是在簡潔高效的同時依然能保證很高的可讀性。這些要求其實對於高階語言來說大多隻是針對編譯器的,而編譯器作為橋樑也是高階語言的一部分,所以我們迫切地需要更現代化的高階語言和更現代化的編譯器。
JetBrains 2011 年開發了更現代化的語言ofollow,noindex">Kotlin 用來彌補 Java 的不足。
Apple 2014 年開發了更現代化的語言Swift ,用來彌補 Objective-C 的不足。
Google 2011 年開發了更現代化的語言Dart ,一種可以構建Web, 伺服器和移動應用的通用開發語言。
這些語言都很大程度上滿足了上面所說的一些要求,但 Kotlin 要與 Java 互操作,所以有很多限制,Swift 也同樣會受 Objective-C 相關的限制,而 Dart 可以更自由地成長,語法風格也是我比較喜歡的風格。Kotlin 雖然目前的使用率很低,但是畢竟相對於 Java 來說還是有特別多的優點的,所以即使現在不使用或者之後也不想使用,那至少能讀懂 Kotlin 程式碼也是很有用處的。
Kotlin 語法
Elvis 運算子
在 Java 中,三目運算子a > b ? a : b
可以簡化這樣的 if 語句:
if (a > b) { return a; } else { return b; }
但是如果用來判斷並設定預設值就顯得乏力了,如:
person.getAvater() != null ? person.getAvatar() : DEFAULT_AVATAR;
如果你不想函式執行兩次或者執行兩次後會有副作用那就無法使用這個運算子了,只能通過判斷語句實現,但是這種預設賦值的情況還是有很多的,我們為什麼不能通過一個運算子就指定為空時的預設值呢?類似這樣:
person.getAvater() ?: DEFAULT_AVATAR;
這種?:
二目運算子也被稱為 Elvis operator,因為和Elvis Presley 的表情符號
一樣。在 Kotlin 中利用?:
可以簡化大部分情況下的預設賦值或者條件控制語句,而且?:
右邊也可以選擇直接丟擲異常。而三目運算子可以使用if else
表示式替代。
空型別檢查
在 Java 中,規定某個引數是否可以為空只能通過額外的註解標註,而在 Kotlin 中只需要用一個?
修飾可以為空的程式元素即可,如val s: String? = null
,Kotlin 的型別系統會檢測程式中可以為空或不可以為空的值以在編譯時消除NullPointerException
,也就是說如果不顯式地使用?
標記那麼常見型別預設就是不能為空的。
安全呼叫運算子?.
可以將空檢查和方法呼叫合併成一個操作,如s?.toUpperCase()
等同於if(s != null) s.toUpperCase() else null
,而這個表示式的結果是String?
型別的。這個操作符還可以用來訪問屬性。
!!
可以對值進行非空斷言val sNotNull: String = s!!
如果 s 為空會顯式地在這一行丟擲異常。
let
只在表示式不為空時執行 lambdaemail?.let { sendEmailTo(it) }
可以定義一些擴充套件函式簡化一些空型別檢查,如fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()
可以這樣被使用input.isNullOrBlank()
,而input
是String?
型別的。
型別轉化
在 Java 中,物件轉化時不要求先判斷物件型別再強制轉化,所以很難避免ClassCastException
,而 Kotlin 將檢查和轉化合併成一次操作,這樣一旦檢查通過就不需要額外的轉化操作,也就不會出現未經檢查的型別轉換了:
if (value is String) println(value.toUpperCase())
as?
運算子也可以嘗試把值轉換成指定的型別,不合適的話就返回null
,可以結合 Elvis 運算子一起使用val otherPerson = o as? Person ?: return false
。
表示式體
在 Java 中,所有控制結構都是語句,賦值操作是表示式,而在 Kotlin 中,除了for
、while
迴圈外的大多數控制結構都是表示式,也就是說即便是if
結構也是表示式,而賦值操作則是語句。表示式是有值的,可以作為另一個表示式的一部分使用,所以區分 expression 和 statement 依舊很重要。
如果函式體寫在花括號中,我們說這個函式有程式碼塊體
(block body),如果函式直接返回了一個表示式,那麼我們說這個函式有表示式體
(expression body),而在 Kotlin 中表達式體函式可以簡化,省略花括號和return
,改為賦值語句:
fun max(a: Int, b: Int): Int = if (a > b) a else b
型別推導
在 Java 中,每個變數必須顯式並精確地指定型別,而 Kotlin 使用了型別推導機制,很多情況下你不需要顯式或精確地指定型別。比如表示式體函式的返回型別可以忽略,因為編譯器會分析表示式體並把它的型別作為函式的返回型別:
fun max(a: Int, b: Int) = if (a > b) a else b
也正是因為省略,宣告變數時必須以val
或var
關鍵字開始,val
是 value 的縮寫,用來宣告不可變引用,也就是說val
宣告的變數不能在初始化後再次賦值,即只讀/只能賦值一次。var
是 variable 的縮寫,用來宣告可變引用。應該儘可能使用val
,這樣有利於函數語言程式設計。val
引用自身是不可變的,但它指向的物件可能是可變的。即使var
允許變數改變自己的值,但是型別卻是改變不了的。如果變數沒有在宣告時初始化,就必須在宣告時顯式地指定型別:
val answer: Int answer = 42
屬性
在 Java 中,private 欄位和對應的getter/setter
方法(也叫訪問器)的組合通常被稱為屬性
,而 Kotlin 簡化了屬性的宣告,只需要使用val
或var
關鍵字像宣告普通變數一樣即可:
class Person { val name: String, var isMarried: Boolean }
在 Java 中使用的話可以使用person.getName()
,person.isMarried()
,person.setMarried()
方法,而在 Kotlin 中使用的話就可以簡化為person.name
,person.isMarried
,雖然本質還是呼叫getter/setter
方法。
when
在 Java 中,使用switch
結構進行條件判斷處理有很多限制,在 Java 7 之前只支援byte
、short
、char
、int
、enum
型別,之後才支援String
,而且case
子句只能是常量,default
子句雖然可省但一般為了安全必須要寫。而在 Kotlin 中when
表示式比switch
結構更強大。when
表示式中多個值合併到一個分支時用逗號隔開,when
可以沒有引數,分支條件就是任意布林表示式。使用when
加is
進行型別判斷轉換,when
加in
進行區間檢查更加方便。
命名引數和引數預設值
常規呼叫函式時必須根據引數順序賦值,可以省略的只有排在末尾的有預設值的引數,但命名引數呼叫時就可以隨便省略了。
頂層函式與頂層屬性,擴充套件函式與擴充套件屬性
在 Kotlin 中,函式和屬性可以在類外面定義,也就不需要去寫只包含靜態方法的 util 工具類了。join.kt
檔案對應於 Java 中的JoinKt
類和其靜態方法,可以使用註解改變這個預設類名:@file:JvmName("StringFunctions")
。頂層屬性使用const
修飾等同於 Java 中的public static final
。
擴充套件函式可以在類外擴充套件某個類的功能,是一種特殊的頂層函式,只需要在函式名前加上接收者型別.
即可,如fun String.lastChar(): Char = this.get(this.length - 1)
,String
就是接收者型別,可以隨便使用它的可訪問的屬性和方法,this
就是接收者物件,可以省略不寫。使用import
匯入擴充套件函式,使用as
重新命名匯入後的類名或函式名。如果擴充套件函式和成員函式簽名一樣,成員函式會被優先使用,而且擴充套件函式是不能被繼承的,也就不能被重寫。擴充套件屬性必須定義getter
函式,因為沒有欄位支援。
可變引數
在 Java 中,不確定數量的引數宣告用...
表示,而在 Kotlin 中要使用vararg
關鍵字,而且賦值時要使用展開運算子*
主動將陣列展開,如listOf("args: ", *args)
。
中綴呼叫
使用infix
關鍵字修飾的函式可以使用中綴符號進行函式呼叫,也就是說可以省略.
和括號,函式名稱直接放在目標物件和引數之間,如val (number, name) = 1 to "one"
。這就要求可以進行中綴呼叫的函式必須只能有一個引數且不能是vararg
可變引數,必須是成員函式或擴充套件函式。
類與繼承
介面和抽象類
Kotlin 中的繼承採用的是單繼承的方式,一個類只能繼承一個類,但可以同時實現多個介面,介面不但可以包含抽象方法,還可以包含非抽象方法的預設實現,介面和抽象類的區別在於介面不能儲存狀態,介面的屬性必須是抽象的或者提供訪問器的實現。實現介面和繼承父類只需要冒號:
即可,多個介面使用逗號,
分隔。
如果一個類同時實現了帶預設實現的相同方法的多個介面,那麼它就必須顯示地實現這個方法,並可以通過類似super<Clickable>.showOff()
的形式呼叫某個父類的實現。
對於介面中宣告的屬性,要麼是抽象的val email: String
,要麼雖然提供了自定義getter/setter
方法但是沒有引用支援欄位,在自定義訪問器中可以使用field
來訪問支援欄位的值,如果你顯式地引用或使用預設的訪問器實現,編譯器會為屬性生成支援欄位。訪問器的可見性和屬性一樣,不過可以在get
或set
關鍵字前加可見性修飾符修改。
繼承限制和可見性限制
Kotlin 中的類和方法預設是final
的,也就是說禁止被繼承,可以通過open
修飾符修改。
抽象類的成員預設是open
的,因為抽象類肯定是希望被繼承重寫的,所以不需要顯式地寫open
修飾符。在介面中不能使用final
、open
或者abstract
。
Kotlin 可見性預設是public
的,protected
成員只在類和它的子類中可見,類的擴充套件函式不能訪問它的private
和protected
成員。
內部類,巢狀類,密封類
對於類內部定義的類,在 Java 中 非靜態的內部類是會隱式持有外部類的引用的,可以直接訪問外部類的任何成員方法和變數,而靜態內部類就不會持有外部類的引用,也無法直接訪問外部類的成員。在 Kotlin 中將非靜態的內部類叫做內部類(Inner classes),並必須使用inner
修飾符修飾,否則的話預設就是不持有外部類引用的靜態的內部類,也叫巢狀類(Nested classes)。內部類由於持有外部類的引用,會有記憶體洩漏的風險,而且內部類序列化也是個問題。內部類訪問外部類需要使用this@Outer
。
不管還是 Java 中的switch
還是 Kotlin 中的when
,我們通常都會提供一個預設的分支,以便能夠處理任何其它分支都不匹配的情況,但是如果邏輯發生了更改,比如新增了一個分支,編譯器無法察覺,我們自己也可能忘了處理,那麼程式就會走到預設的分支,產生無法預料的 Bug,尤其是當when
表示式是檢測類的子型別的時候,而在 Kotlin 中可以通過sealed
關鍵字修飾一個類以限制類的繼承。這種類也被叫做密封類,其實也是列舉類的拓展,密封類的所有子類必須在同個檔案中宣告,由於密封類肯定是希望被繼承的,所以open
是不需要的。
構造器
如果主構造器沒有註解或可見性修飾,那麼constructor
關鍵詞可以省略。如果子類沒有提供任何構造器,那麼必須顯式呼叫父類構造器。
讓編譯器自動生成模板程式碼
對於只用來儲存資料的資料類,在 Kotlin 中成為資料類(Data Classes),可以使用data
修飾,data class User(val name: String, val age: Int)
,然後 IDE/編譯器 就會幫你自動生成equals()/hashCode()
函式比較主構造器中的值並生成唯一的 hashCode,自動生成toString()
函式格式類似於"User(name=John, age=42)"
,自動生成componentN()
函式用於解構宣告,自動生成copy()
函式用於建立副本。
對於final
類,通常使用裝飾者模式進行擴充套件,而這些模板程式碼也可以讓 IDE/編譯器 自動生成,只需要通過by
關鍵字委託物件即可。
單例
建立類和該類的唯一例項可以放在一起宣告,使用object
關鍵字,和宣告普通類不同的是不能宣告構造器。也可以在類裡面宣告一個單例物件。雖然這種單例類的宣告方式更簡潔,但是卻無法控制構造的引數和過程,所以依賴注入還是最好的選擇。
伴生物件
在 Kotlin 中沒有static
關鍵字也沒有靜態成員的概念,雖然 Kotlin 的包級別函式和頂層函式可以滿足大部分需求,但是它們還是無法訪問private
成員,所以需要一個工廠方法,也就是使用companion
關鍵字在類中定義物件,這樣使用的時候就不需要顯式地指定物件的名字,而是直接通過類名加方法名就可以呼叫了,與此同時,這個物件完全可以訪問類中的private
成員,包括private
構造器。伴生物件可以不指定名字,預設名字是Companion
,伴生物件可以實現一個介面,可以有擴充套件函式和屬性,也就是說,宣告一個空的伴生物件並在其他地方宣告這個伴生物件的擴充套件函式有時候是個不錯的選擇。
物件表示式
可以利用object
關鍵字宣告匿名物件,匿名物件可以不實現介面也可以實現多個介面,匿名物件不是單例的,每次使用物件表示式都會建立新的物件,可以把表示式的值賦值給一個變數,那麼匿名物件也就有名字了。
Lambda 表示式
λ演算 是一個數學邏輯中的形式系統,以變數繫結和替換的規則,來研究函式如何抽象化定義,函式如何被應用及遞迴。它是一種通用的計算模型,可用於模擬任何圖靈機。Lambda 表示式是函數語言程式設計中非常重要的一個概念,通常是指使用特殊的語法所書寫的匿名函式,函式可以像普通引數一樣賦值給其它變數,也可以作為其它函式的返回值。
Kotlin 中宣告 Lambda 表示式的語法類似於{ x: Int, y: Int -> x+y }
,用花括號包裹,用箭頭把實參列表和 Lambda 函式體隔開。像函式內宣告的匿名內部類一樣,函式內使用的 lambda 表示式也可以訪問函式的引數以及在 lambda 之前定義的區域性變數,而且可以是非final
的變數,非final
的變數的值是被封裝在一個特殊的包裝器中的,這個包裝器的引用會和 lambda 程式碼一起儲存,也就是說,如果 lambda 被用作事件處理或者其他非同步執行的情況,對區域性變數的修改只會在 lambda 中的程式碼真正執行時發生,這個時候不使用區域性變數而是使用類成員變數是個不錯的選擇。
成員引用
把一個已經定義好的函式作為值傳遞,只需要使用::
運算子來轉換val getAge = Person::age
,這種表示式成為成員引用,雙冒號把類名和成員(方法或屬性)名隔開。可以引用頂層函式,也就可以不寫類名。可以引用擴充套件函式就像例項成員一樣。可以引用構造器,只需要在雙冒號後指定類名val createPerson = ::Person
。
函式式介面
只有一個抽象方法的介面被稱為函式式介面,或 SAM(Single Abstract Method)介面,像OnClickListener
、Runnable
和Callable
等這些都是函式式介面,函式式介面作為引數的方法在呼叫時可以使用 lambda 簡化,而且如果 lambda 表示式沒有訪問任何來自定義它的函式的變數時,相應的匿名類例項可以在多次呼叫時重用:
button.setOnClickListener { view -> doSomething(view) }
編譯器可以幫助生成 lambda 轉換成函式式介面所需的 SAM 構造方法,這個方法只需要一個引數,就是一個被用作函式式介面中唯一的方法的方法體:
fun createAllDoneRunnable(): Runnable { return Runnable { println("All done!") } }
“with” 和 “apply” 函式
庫函式with
會把第一個引數轉換成作為第二個引數傳給它的 lambda 的接受者,返回值是執行 lambda 程式碼的結果,而apply
函式始終會返回作為實參傳給它的物件。
註解
宣告註解需要額外的關鍵字annotation
來修飾類annotation class Fancy
,元註解包括:
@Target
表明該註解型別可以註解哪些程式元素,如果註解型別不使用@Target
描述那麼表明可以註解所有程式元素,值是Array<out AnnotationTarget>
型別:
AnnotationTarget.CLASS AnnotationTarget.PROPERTY AnnotationTarget.FIELD AnnotationTarget.LOCAL_VARIABLE AnnotationTarget.VALUE_PARAMETER AnnotationTarget.CONSTRUCTOR AnnotationTarget.FUNCTION AnnotationTarget.PROPERTY_GETTER AnnotationTarget.PROPERTY_SETTER
@Retention
表明該註解是否保留到編譯完的 class 檔案中,是否可以在執行時通過反射訪問,預設都是可以的,也就是說AnnotationRetention.RUNTIME
:
AnnotationRetention.SOURCE AnnotationRetention.BINARY AnnotationRetention.RUNTIME
@Repeatable
表明該註解是否能同時註解同一個元素多次。
@MustBeDocumented
表明這個註解是公共 API 的一部分,應該包含在自動生成的 API 文件中類或方法簽名的地方。
註解可以有構造器和引數,只能擁有以下型別的引數: 基本資料型別、字串、列舉、類引用、其他的註解類,以及前面這些型別的陣列。
如果要把一個類指定為註解的實參,類名後要加::class
,如@MyAnnotataion(MyClass::class)
。
如果把另一個註解指定為註解的實參,那麼需要去掉@
。
如果把一個數組指定為註解的實參,那需要使用陣列字面值或者arrayOf
函式。
如果把屬性指定為註解的實參,那麼這個屬性必須是編譯時常量,也就是說用const
修飾。
由於 Kotlin 中一個簡單的程式元素可能會自動生成多個程式元素和其位元組碼,比如構造器宣告中的變數宣告可能還隱式包含了屬性宣告和相應的getter/setter
方法,所以為了精確地表示你想註解的程式元素,可以使用將目標放在@
和註解名之間,並用:
將目標和註解名隔開,如@get:Ann val bar
表明註解bar
的getter
方法。
如果多個註解同時作用於一個元素,可以使用方括號包裹:
class Example { @set:[Inject VisibleForTesting] var collaborator: Collaborator }
註解也可以用在 lambda 表示式中,它們將被應用於自動生成 lambda 體的invoke()
方法。
反射
Kotlin 反射主要依靠KClass
、KCallable
、KFunction
和KProperty
。
使用val kClass = person.javaClass.kotlin
可以返回一個KClass<Person>
的例項,然後就可以通過這個例項獲取kClass.memberProperties
所有屬性等資訊。
通過::
語法可以獲取KFunction
或KProperty
的例項。