開發與維運

Kotlin for Java Developers 學習筆記

Kotlin for Java Developers 學習筆記

Coursera 課程 Kotlin for Java Developers(由 JetBrains 提供)的學習筆記

From Java to Kotlin

Java 和 Kotlin 代碼可以相互轉化

public class Person {
    private final String name;
    private final int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
}

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

Kotlin 被編譯為 Java 字節碼,所以從 Java 代碼的層面看,這兩者是一樣的,都有一個 Constructor 和兩個 Getter

也可以加上 data 修飾符,表示自動生成 equalshashCodetoString 這三個函數

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

多個變量可以以 Pair 的形式賦值

val (description: String, color: Color) = Pair("hot", RED)

如果數據的類型在上下文中可以很明確地被推導出來,那麼可以不用聲明變量的類型

val (description: String, color: Color)
val (description, color)

多於 2 個 if … else … 時可以使用 when 關鍵字,類似於 switch,但又有細微區別

val (description, color) = when {
    degrees < 10 -> Pair("cold", BLUE)
    degrees < 25 -> Pair("mild", ORANGE)
    else -> Pair("hot", RED)
}

基本語法

Kotlin 的代碼也是從 main 函數開始

package intro
fun main() {
    val name = "Kotlin"
    println("Hello, $name!")
}

從 Kotlin 1.3 開始

fun main(args: Array<String>)

可以只寫

fun main()

變量、常量與字符串模板

字符串模板 $variable${args.getOrNull(0)}

“變量”分為 valvarval 是隻讀的

Kotlin 是靜態類型的語言,每一個變量都會有自己的類型,但是我們可以在代碼中省略基本類型,編譯器會自動推斷

var s = "abc" // var s: String = "abc"
var v = 123 // var v: Int = 123

我們不能給一個類型的變量賦值另一個類型的數據,例如:字符串常量賦值給一個 Int 類型的變量 string,這是一個編譯時錯誤

var string = 1
string = "abc" // NOT ALLOWED是不允許的,我們不能把

val 不對數據做任何強加的限制,仍然可以改變其引用的數據,例如通過 list.add() 去修改一個被 val 修飾的列表,只要這個列表本身是允許被修改的

val list = mutableListOf("Java"// list.add() 可以往 List 中加東西
val list = listOf("Java"// list.add() 是不存在的

函數

fun max(a: Int, b: Int)Int {
    return if (a > b) a else b
}

如果只有一句話(function wilth expression body),可以寫成

fun max(a: Int, b: Int) = if (a > b) a else b

void 類型的函數在 Kotlin 中會以 Unit 的形式返回

Kotlin 的函數可以定義在任何地方:頂層、類的成員、函數中定義另一個函數

調用頂層函數相當於 Java 中的 static 函數

// MyFile.kt
package intro
fun foo() = 0

//UsingFoo.java
package other;
import intro.MyFileKt;
public class UsingFoo {
    public static void main(String[] args) {
        MyFileKt.foo();
    }
}

為變量提供默認值,不再需要重載各種函數

fun displaySeparatpr(character: Char = '', size: Int = 10) {
    repeat(size) {
        print(character)
    }
}

displaySeparator() // 
displaySeparator(size = 5// **
displaySeparator(3'5'// WON'T COMPILE
displaySeparator(size = 3, character = '5'// 555

分支

在 Kotlin 中,if 是表達式

val max = if (a > b) a else b

沒有三元表達式

(a > b) ? a : b

注意與 Python 的區別

max = a if a > b else b

在 Kotlin 中,when 可以當作 switch 使用,不需要 break

switch (color) {
 case BLUE:
  System.out.println("cold")
  break;
 case ORANGE:
  System.out.println("mild")
  break;
 default:
  System.out.println("hot")
}

when (color) {
    BLUE -> println("cold")
    ORANGE -> println("mild")
    else -> println("hot")
}

可以使用任何類型,可以用來判斷多個條件

fun response(input: String) = when (input) {
 "y""yes" -> "Agree"
 "n""no" -> "Sorry"
 else -> "Not Understand"
}

可以做類型檢查

if (pet instanceof Cat) {
    ((Cat) pet).meow();
else if (pet instanceof Dog) {
 Dog dog = (Dog) pet;
 dog.woof();
}

when (pet) {
    is Cat -> pet.meow()
    is Dog -> pet.woof()
}

可以不需要參數

fun updateWeather(degrees: Int) {
 val (desc, color) = when {
        degrees < 5 -> "cold" to BLUE
        degrees < 23 -> "mild" to ORANGE
        else -> "hot" to RED
 }
}

循環

val map = mapOf(1 to "one"2 to "two"3 to "three")
for ((key, value) in map) {
    println("$key = $value")
}

val list = listOf("a""b""c")
for ((index, element) in list.withIndex()) {
    println("$index$element")
}

for (i in 1..9// 1 2 3 4 5 6 7 8 9

for (i in 1 until 9// 1 2 3 4 5 6 7 8

for (ch in "abc")

for (i in 9 downTo 1 step 2// 9 7 5 3 1

拓展函數

fun String.lastChar() = get(length - 1)
val c: Char = "abc".lastChar()

也可以直接在 Java 中使用 Kotlin 中定義的拓展函數

import lastChar from ExtensionFunctions.kt;
char c = lastChar("abc");

常用的有 withIndexgetOrNulljoinToStringuntiltoeqtoInt 等等

val set = hashSetOf(1753)
println(set.javaClass) // class java.util.HashSet

fun main(args: Array<String>) {
    println("Hello, ${args.getOrNull(0)}!")
}

val regex = """d{2}.d{2}.d{4}""".toRegex()
regex.matches("15.02.2016"// true
regex.matches("15.02.16"// false

特別的,untilto 這種,本身是需要通過 .() 調用的

1.until(10)
"Answer".to(42)

但是因為原型聲明的時候允許 infix

infix fun Int.until(to: Int) = IntRange
infix fun <A, B> A.to(that: B) = pair(this, that)

所以可以省略 .()

1 until 10
"Answer" to 42

成員函數比拓展函數的優先級高,例如下例會輸出 1,並得到一個警告,說 entension is shadowed by a member

class A {
    fun foo() = 1
}

fun A.foo() = 2 // Warning: Extension is shadowed by a member

A().foo() // 1

但是我們可以重載一個拓展函數

class A {
    fun foo() = "member"
}

fun A.foo(i: Int) = "extension($i)"

A().foo(2// extension(2)

標準庫

Kotlin 的標準庫包括 Java 標準庫和一些常用的拓展函數

沒有所謂的 Kotlin SDK,只有 Java 的 JDK 和一些 extensions

Nullability

現代的編程語言應該把 Null Pointer Exception 變成編譯時錯誤,而不是運行時錯誤

val s1: String = "always not null" // 不可以 = null
val s2: String? = null // 或者 = "a string"

對於一個可能為 null 的變量,我們應該始終用 if 語句檢查

if (s != null) {
    s.length
}

在 Kotlin 中。可以使用 ? 來簡化訪問,如果對象為 null,則運行結果為 null,返回類型是 nullable 的基本類型

val length: Int? = s?.length

如果只想要基本類型,可以使用 elvis 運算符( elvis 來自 Groove)

val length: Int = s?.length ?: 0

可以使用 !! 強制拋出 NPE

elvis 的優先級比加減法低

val x: Int? = 1
val y: Int = 2
val z1 = x ?: 0 + y // 1
val z2 = x ?: (0 + y) // 1
val z3 = (x ?: 0) + y // 3

? 的位置不同會決定具體什麼東西不可以為 null:List<Int?>List<Int>?

Kotlin 中使用 as 進行類型轉換,同樣可以對 as 進行 ? 修飾

if (any is String) {
    any.toUpperCase()
}

(any as? String)?.toUpperCase()

函數式編程

Lambda

與匿名類類似,在現代語言(例如 Kotlin)和 Java 8 中,都支持了 Lambda 使得語法更簡單

Kotlin 中的 Lambda 用 {} 包圍,為了與正則表達式區分,Lambda 的 {} 常加粗

list.any({i: Int -> i > 0})

  • 當 Lambda 是括號中最後一個參數時,我們可以把 Lambda 從括號中移出

  • 當括號為空時,可以省略空括號

  • 當類型可以被推斷時,可以省略類型

  • 當只有一個參數時,可以只用 it 而無需聲明參數

於是可以簡化為

list.any { it > 0 }

多行的 Lambda 的最後一個表達式為 Lambda 結果

list.any {
    println("processing $it")
    it > 0
}

可以使用解構聲明簡化 Lambda 表達式

對於沒有使用的參數,可以用 _ 替代

map.mapValues {entry -> "${entry.key} -> ${entry.value}!" }
map.mapValues {(key, value) -> "${key} -> ${value}!" }
map.mapValues {(_, value) -> "${value}!" }

常用的集合操作

  • filter 只保留滿足謂詞條件的元素
  • map 將每一個元素按指定規則變換
  • any 判斷列表中是否有滿足謂詞條件的元素
  • all 判斷列表中是否所有元素都滿足謂詞條件
  • find 找第一個滿足謂詞條件的元素,如果不存在則為 null,等價於將謂詞條件作為參數的 first 或者 firstOrNull
  • count 計算列表中滿足謂詞條件的元素個數
  • partition 按是否滿足謂詞條件,將列表分裂為 2 個列表
  • groupBy 按照指定字段將元素分類為若干個列表(例如按照 it.age 分類)
  • associatedBy 會將重複字段刪除
  • zip 將 2 個列表合併為一個列表,其中每一個元素分別由兩個列表各自對應位置元素組合,如果列表長度不同,則合併後的元素個數是較短列表的長度,其餘部分將被忽略
  • flatten 將嵌套的列表展開
  • flatMap 是 map 和 flatten 的組合
  • distinct 保留列表中互不相同的元素
  • maxBy 查找列表中給定字段最大的元素,如果列表為空則返回 null

組合這些操作,我們可以很容易進行復雜的運算,例如找年齡的眾數

val mapByAge: Map<Int, List<Hero>> = heros.groupBy {it.age }
val (age, group) = mapByAge.maxBy { (_, group) -> group.size }!!
println(age) // 找眾數

函數類型

Lambda 表達式是有函數類型的

val isEven: (Int) -> Boolean = { i: Int -> i % 2 == 0 }
val result: Boolean = isEven(2// true

對於沒有參數的函數,使用 () 調用看起來很奇怪,所以經常使用 run

{ println("hey!") }() // possible, but looks strange
run { println("hey!") }

() -> Int? 表示返回值可以為 null,而 (() -> Int)? 表示表達式可以為 null

成員引用

可以往變量中存儲 Lambda 表達式,但是不可以存儲一個函數,在 Kotlin 中,函數和 Lambda 是兩回事,如果一定要把函數保存到變量中,可以使用函數引用

val isEven: (Int) -> Boolean = { i % 2 == 0 } // OK

fun isEven(i: Int)Boolean = i % 2 == 0
val predicate = isEven // COMPILE ERROR

fun isEven(i: Int)Boolean = i % 2 == 0
val predicate = ::isEven // OK
list.any(::isEven) // OK

可以將函數綁定到特定的實例,也可以不綁定

class Person(val name: String, val age: Int) {
    fun isOlder(ageLimit: Int) = age > ageLimit
}
val alice = Person("alice"29)

val agePredicate = alice::isOlder
agePredicate(21// true

val agePredicate: (Person, Int) -> Boolean = { person, ageLimit -> person.isOlder(ageLimit) }
agePredicate(alice, 21// true

下面這個例子是 Bound Reference,Person 被存儲在了實例的內部,所以函數類型是 (Int) -> Boolean 而不是 (Person, Int) -> Boolean

class Person(val name: String, val age: Int) {
    fun isOlder(ageLimit: Int) = age > ageLimit
    fun getAgePredicate() = ::isOlder // this::isOlder
}

函數返回值

return 只會返回到函數 fun,而不會到返回 Lambda

fun containsZero(list: List<Int>)Boolean {
    list.forEach {
        if (it == 0return true
    }
    return false
}
// 這個 forEach 接受了一個 Lambda 表達式,但是 return 是返回到 fun containsZero 的

fun duplicateNonZero(list: List<Int>): List<Int> {
    return list.flatMap {
        if (it == 0return listOf()
        listOf(it, it)
    }
}
duplicateNonZero(listOf(305)) // 輸出 []
// 因為 return 只會返回到 fun duplicateNonZero,而不是先返回給 flatMap 接受的 Lambda 再經由 flatMap 返回

為了避免這種情況,我們應該避免使用 return 語句,利用 Lambda 將最後一行作為返回值的特性來實現 Lambda 中的返回

fun duplicateNonZero(list: List<Int>): List<Int> {
    return list.flatMap {
        if (it == 0) {
            listOf()
        } else {
            listOf(it, it)
        }
    }
}

如果確實需要將結果返回到 Lambda,可以使用 return@ 返回到指定的標籤

list.flatMap {
    if (it == 0return@flatMap listOf<Int>()
    listOf(it, it)
}

list.flatMap l@ { // 自定義標籤 l
    if (it == 0return@l listOf<Int>()
    listOf(it, it)
}

另外的解決方案是使用本地函數或者匿名函數

fun duplicateNonZeroLocalFunction(list: List<Int>): List<Int> {
    fun duplicateNonZeroElement(e: Int): List<Int> {
        if (e == 0return listOf()
        return listOf(e, e)
    }
    return list.flatMap(::duplicateNonZeroElement)
}

fun duplicateNonZero(list: List<Int>): List<Int> {
    return list.flatMap(fun (e): List<Int> {
        if (e == 0return listOf()
        return listOf(e, e)
    })
}

forEach 中的 return 不會像 break 一樣響應

fun foo(list: List<Int>) {
    list.forEach {
        if (it == 0return
        print(it)
    }
    print("##"// 如果 list 包含 0,則不會輸出 ##
}

fun bar(list: List<Int>) {
    for (element in list) {
        if (element == 0break
        print(element)
    }
    print("##"// 始終會輸出 ##
}

屬性

屬性和域成員變量

在 Kotlin 中,依然保持了 Java 中屬性的概念,但是不再需要顯式地聲明 getter 和 setter

  • property = field + accessor
  • val = field + getter
  • var = filed + getter + setter

例如在 Kotlin 的這段代碼中,如果將它轉化為 Java 代碼,則隱含了 3 個 accessor

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

// String getName()
// int getAge()
// void setAge(int newAge)

對屬性的訪問

下面這段代碼中,“Calculati……” 會輸出 3 次

對於 foo1 來說:

  • 代碼中使用了 run,所以運行了 Lambda 並且把最後一行的表達式作為了結果,因此 foo1 獲得了值 42,並在這個過程中輸出了 “Calculating……” 的信息

  • Lambda 表達式的值只在賦值時被計算一次,之後就會使用 property 的值,所以 “Calculating……” 只會輸出 1 次

對於 foo2 來說:

  • 我們寫了一個自定義的 getter,所以當訪問 foo2 時,會訪問自定義的 getter,因此輸出 2 次 “Calculating……”

val foo1 = run {
  println("Calculating the answer...")
  42
}

val foo2: Int 
  get() {
    println("Calculating the answer...")
    return 42 
  }

fun main(args: Array<String>) { 
  println("$foo1 $foo1 $foo2 $foo2")
}

class StateLogger {
  var state = false
  set(value) {
      println("state has changed: $field -> $value")
      field = value
  }
}

StateLogger.state = true // state has changed: false -> true

在 accessor(getter 和 setter)中,我們可以使用 field 來訪問域成員變量,但是也僅能在 accessor 中通過這種方式來訪問

如果重新定義了 accessor 但是沒有使用 field,編譯器會忽略並且不會生成對應的 accessor

如果沒有為屬性定義 accessor,那麼會有默認的 getter 和 setter

在類的內部,className.valueNale 的代碼將由編譯器決定是否對齊進行優化,如果訪問非常簡單,那麼編譯器會替換為直接訪問這個變量本身,注意這樣的優化對於類外部的訪問來說是不安全的,所以在類的外部,className.valueNale 會調用對應的 getter 作為字節碼,而不是直接訪問這個變量本身

使用 private set 來將一個成員變量設置為僅允許從內部被修改,而不會被外部的訪問所修改

interface User {
  val nickname: String
}

class FacebookUser(val accountId: Int) : User { 
  override val nickname = getFacebookName(accountId)
}

class SubscribingUser(val email: String) : User { 
  override val nickname: String
    get() = email.substringBefore('@')
}

FacebookUser.nickname 會把值存在 filed 中,而 SubscribingUser.nickname 用的是一個自定義的 getter,所以每一次都會訪問計算

接口中的屬性

接口中的屬性不是 final 的,它們可以被子類修改

如果任意一個子類中有自定義的 getter,那麼不可以使用智能類型轉換(即 if (session.user is FacebookUser) 會被編譯器報錯),因為自定義的 getter 可能每一次返回的是不同的值

可以通過引入一個本地變量來使用智能類型轉換

fun analyzeUserSession(session: Session) {
    if (session.user is FacebookUser) { // 這裡判斷的時候得到了一個值
        println(session.user.accountId) // 下一次 getter 得到的未必是同一個
    }
}
// Compiler error: Smart cast to 'FacebookUser' is impossible, because 'session.user' is a property that has open or custom getter

fun analyzeUserSession(session: Session) {
    val user = session.user // 只會在這裡有一次 getter
    if (user is FacebookUser) {
        println(user.accountId)
    }
}
// OK

同樣的,可變數據類型(mutable variables)也不可以使用智能類型轉換

屬性拓展

可以拓展已有的屬性

val String.lastIndex: Int
 get() = this.length - 1
val String.indices: IntRange
 get() = 0..lastIndex

拓展屬性和拓展函數很類似,沒有任何奇妙的優化,所以下面這段代碼依然會輸出 2 次 “Calculating……”

val String.medianChar 
  get(): Char? {
    println("Calculating...")
    return getOrNull(length / 2
  }

fun main(args: Array<String>) { 
  val s = "abc"
  println(s.medianChar) 
  println(s.medianChar)
}

延遲初始化

Lazy Initialization 或者叫 Late Initialization,以只在第一次被用到的時候才會計算

val lazyValue: String by lazy {
    println("Computed")
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}
// 只在聲明的時候計算(輸出)1 次 "Computed",main 函數中的訪問直接用的 property

fun main(args: Array<String>) {
    // no lazyValue usage
}
// 但是因為初始化是 lazy 的,所以只在第一次被用到的時候才會計算,於是不會輸出 "Computed"

如果對於一個類的成員,我們在構造函數中沒有辦法知道它的初始值,那麼只能將它初始化成了 null,之後就需要使用 myData?.foo 的形式來訪問

但是如果我們能確保在初始化完成後這個成員不可能再是 null,例如我們在 onCreate 函數中(或者別的手段)對其進行了初始化,處理 null 就會顯得冗餘

就可以使用 lateinit 對其修飾,這樣這個類型就不再需要是 nullable 的了

lateinit myData: MyData
// ...
myData.foo

如果因為某些原因,這個成員沒有被正確初始化,我們會得到一個運行時錯誤,但是這個錯誤不會顯示 NullPointerException,而是 UninitializedPropertyAccessException

注意 lateinit 修飾的只能是 var而不可以是 val,其類型不能是基本類型也不能是一個 nullable

可以個 .isInitialized 來判斷一個延遲初始化的變量有沒有被初始化

面向對象編程

訪問級別

  • Kotlin 中默認級別是 public 和 final 的,如果需要不是 final 的需要顯式說明 open
  • Java 中的默認級別是 package-level,同一個包內其他類可見,這個在 Kotlin 中叫做 internal
  • override 在 Kotlin 中是強制的,避免意外 override
  • protected 在 Java 中仍然對同一個包內的其他類可見,在 Kotlin 中只對子類可見
  • private 針對類來說就是私有類,對於 top-level declaration 是對同一個文件中的其他部分可見
  • internal 在 JVM 的層面 public + name mangled
  • Java 中每一個類需要是單獨的類,而 Kotlin 中可以把多個類放在一個文件裡
  • Kotlin 中的包名稱不必遵循 org.company.store 的形式,但仍做如此推薦

構造器

Kotlin 中不需要使用 new,直接像訪問函數一樣就可以構造一個對象

class A
val a = A()

如果構造器足夠簡單,不需要像 Java 一樣顯式地寫清楚 this.val = val 這樣的構造器,Kotlin 會自動賦值

// Kotlin
class Person(val name: String, val age: Int)

// Java
class Person(String name, int age) {
    this.name = name;
    this.age = age;
}

如果需要更復雜的構造器,可以使用 init

class Person(name: String) {
    val name: String // property declaration
    
    init {
        this.name = name
        // do something else
    }
}

注意,只有加上 var 或者 val 才會自動賦值作為域成員,否則就只是普通的構造器的參數

可以修改構造器的訪問級別

可以聲明二級構造器,例如在矩形的類中聲明一個二級的構造器(正方形),當接收一個參數(邊長)時,由正方形調用 this(side, side)

class Rectangle(val height: Intval width: Int) {
    constructor(side: Int): this(side, side) {
        // ...
    }
}

子類的構造器會先調用父類的構造器

open class Parent {
  init { print("parent ") } 
}

class Child : Parent() {
  init { print("child ") } 
}

fun main(args: Array<String>) {
  Child() 
}

// parent child

open class Parent {
    open val foo = 1
    init {
        println(foo)
    }
}

class ChildParent() {
    override val foo = 2
}

fun main() {
    Child()
}

// 0

這段代碼會輸出 0

override 一個 property 其實是 override 了它的 getter,而不是 filed

父類(應該)擁有 foo,初始化為 1,並且有一個平凡的 getter,叫做 getFoo(),這個 getter 返回了(父類的) foo

子類(應該)擁有 foo,初始化為 2,並且有一個平凡的 getter,叫做 getFoo(),這個 getter 返回了(子類的) foo,注意這個 getter 會 override 父類的 getter

當新建一個子類的時候,首先調用了父類的構造器,父類的 foo 為 1,並且擁有一個返回了(父類的)foo 的 getter,然後調用 init,在 init 中,會調用 getFoo,由於這是一個子類,那麼根據多態,應該調用子類的 getFoo,子類的 getFoo 會返回(子類的)foo 值,而此時子類還沒有完成初始化,所以 foo 值為 0

因此,上面這段代碼在 Java 中相當於

public class A {
    private final String value;
    
    public A(String value) {
        this.value = value;
        getValue().length(); // call value_B.length() -> call null.length()
    }
    
    public String getValue() {
        retrun value;
    }
}

public class B extends A {
    private final String value; // mark it as value_B
    
    public B(String value) {
        super(value);
        this.value = value; // mark it as value_B
    }
    
    @Override
    public String getValue() {
        retrun value; // mark it as value_B
    }
}

類修飾符

enum 是一個類修飾符,而不是一個特殊的關鍵字

enum class Color {
    BLUE, ORANGE, RED
}

Color.BLUE

import mypackage.Color.*
BLUE

enum class Color(val r: Intval g: Intval b: Int) {
    BLUE(00255), ORANGE(2551650), RED(25500);
    fun rgb() = (r  256 + g)  256 + b
}

BLUE.r
BLUE.rgb()

dataequalscopyhashCodetoString 等方法

data class Contact(val name: String, val address: String)
contact.copy(address = "new address")

在 Kotlin 中,== 默認比較它們的 equals,而 === 比較它們是不是同一個引用

在 Java 中,== 比較是否是同一個引用,需要使用 equals 來比較它們

class Foo(val first: Intval second: Int)
data class Bar(val first: Intval second: Int)

val f1 = Foo(12
val f2 = Foo(12
println(f1 == f2) // false

val b1 = Bar(12
val b2 = Bar(12
println(b1 == b2) // true

默認的實現都是比較引用的 equals,但是當類使用 data 修飾時,會自動實現一個比較域成員的 equals,於是就會得到 true

Kotlin 只會使用主構造器中的屬性來實現 equals,不會使用類在其他部分定義的變量

當明確知道自己的類考慮了所有考慮的情況時,可以用 sealed 來避免冗餘的代碼,注意這個是類修飾符,不能用於接口

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"// 要加上這句話,否則無法通過編譯:when 必須完備
}

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)
    // OK
}

在 Java 中,如果只寫 class A,則作為一個內部類,會默認保存外部類的一個引用,而在 Kotlin 中, class A這種寫法默認不會產生這樣的引用,即相當於 Java 中的 static class A

如果需要這樣一個對外部類的引用,可以使用 inner class A,並通過 @ 標籤訪問

class A {
    class B
    inner class C {
        this@A
    }
}

類委託可以委託一個類來實現一個接口

interface Repository {
    fun getById(id: Int): Customer
    fun getAll(): List<Customer>
}
interface Logger {
    fun logAll()
}

// 原本的寫法
class Controller(
 repository: Repository,
    logger: Logger
): Repository, Logger {
    override fun getById(id: Int) = repository.getById(id)
    override fun getAll(): List<Customer> = repository.getAll()
    override fun logAll() = logger.logAll()
}

// Class Delegation
class Controller(
 repository: Repository,
    logger: Logger
): Repository by repository, Logger by logger

fun use(controller: Controller) {
    controller.logAll()
}

對象

對象在 Kotlin 中,對象是單例的

object KSingleton {
    fun foo() {}
}

KSingleton.foo()

對象表達式代替了 Java 中的匿名類(如果只有簡單的方法,可以直接使用 Lambda 表達式,如果需要多個方法,那可以使用對象表達式)

對象表達式不是單例的,每一次調用都會新建新的實例,因為有可能會需要使用外部的類傳遞進來的參數,使用每一次都要實例化

Kotlin 中沒有 static 的方法,companion object 可以作為它的替代

Java 中的 static 方法不能重寫接口的方法,在 Kotlin 中,companion object 可以重寫接口的方法

class C {
  companion object {
    @JvmStatic fun foo() {}
    fun bar() {} 
  }
}

// Java
C.foo();           // OK
C.bar();           // NO,因為試圖將其作為 static 方法來調用
C.Companion.foo(); // OK
C.Companion.bar(); // OK

inner 只能修飾類,不能修飾對象,因為 object 是單例

可以把 object 放在 class 內部作為嵌套

常量

const 用來定義基本類型或者 string,這個常量會在編譯時被替換掉

const cal answer = 42

泛型

interface List<E{
    fun get(index: Int): E
}

fun foo(ints: List<Int>) { ... }

fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T>

fun <T> List<T>.firstOrNull(): T?

可以使用 Any 來確保元素不可以為 null

fun <T> foo (list: List<T>) {
    for (element in List) {
        
    }
}

foo(listOf(1null)) // OK

fun <T: Any> foo (list: List<T>) {
    for (element in List) {
        
    }
}

foo(listOf(1null)) // NO

可以使用 where 來進行多個 upper bounds

fun <T: Comparable<T>max(first: T, second: T): T {
    return if (first > second) first else second
}

fun <T> ensureTrailingPeriod(seq: T)
 where T: CharSequence, T: Appendable {
        if (!seq.endsWith('.') {
            seq.append('.')
        })
    }

使用了泛型的函數,可以用 JvmName 來指定不同的泛型函數名稱,這樣就可以在 Java 中使用 averageOfDouble,因為字節碼有這個函數了

fun List<Int>.average: Double { ... }

@JvmName("averageOfDouble")
fun List<Double>.average()Double { ... }

編碼約定

符號重載

使用 a + b 會自動調用 a.plus(b)

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

Point(12) + Point(23)

重載的運算符左右兩邊的數據類型可以不一樣

operator fun Point.times(scale: Int): Point {
    return Point(x  scale, y  scale)
}

Point(12) * 3

單目運算符也可以重載,例如 unaryMinusnotinc

注意對於 list 這樣的類型,+= 的操作會新建一個新的 list,例如下面這段代碼會輸出 [1, 2, 3, 4][1, 2, 3]

val list1 = listOf(123
var list2 = list1
list2 += 4
println(list1) 
println(list2)

如果需要,可以把 varlistOf 換成 valmutableListOf

在 Kotlin 中,可以使用 < 這些符號比較字符串之間的大小,會自動調用 compareTo() 並和 0 比較,也可以使用 == 比較相等,會調用 equals()

訪問鍵值對也可以使用 map[index] 操作,會調用 map.get(index)

Java 的 String 沒有實現 Iterable 接口,但是 Kotlin 中可以通過定義拓展函數的方法重載迭代運算符

operator fun CharSequence.iterator(): CharIterator
for (c in "abc") { ... }

解構式的定義,在本質上也是運算符的重載 argument.component1()

map.forEach { (key, value) -> { ... } }

list 也可以同時遍歷下標和元素

for ((inex, element) in list.withIndex()) {
 println("$index $element")
}

不需要的參數可以用 _ 跳過

如果一個類(例如 Point)實現了 Comparable 接口,那麼在任何其他地方都可以使用 < > 來比較大小,也可以再定義一個 private operator fun Point.compareTo,這樣就可以在自己的算法中用新的比較規則,這個規則在代碼的其他部分是不可見的

內聯函數

run 會運行一個 Lambda 代碼段,並把最後一個表達式作為結果

let 可以檢測一個參數是不是 nulll

fun getEmail(): Email?
val email = getEmail()

if (meial != null) sendEmailTo(email)

email?.let { e -> sendEmailTo(e) }
getEmail()?.let { sendEmailTo(it) }

如果任意一個子類中有自定義的 getter,那麼不可以使用智能類型轉換(即 if (session.user is FacebookUser) 會被編譯器報錯),因為自定義的 getter 可能每一次返回的是不同的值,可以通過引入一個本地變量來使用智能類型轉換,而 let 可以簡化這個寫法

interface Session {
    val user: User
}
fun analyzeUserSession(session: Session) {
    val user = session.user
    if (user is FacebookUser) {
        println(user.accountId)
    }
}

(session.user as? FacebookUser)?.let {
    println(it.accountId)
}

takeIf 返回條件滿足時的對象,否則 null

常與 ?.let 連用

issue.takeIf { it.status == FIXED }
person.patronymicName.takeIf(String::isNotEmpty)

takeUnlesstakeIf 相反

repeat 可以重複一個操作多次,注意這不是一個 built-in 的關鍵字,而是一個 inline function

repeat(10) {
    println("Hello")
}

inline fun repeat(times: Int, action (Int) -> Unit) {
    for (index in 0 until times) {
        action(index)
    }
}

沒有內聯的 Lambda 表達式會被當做一個類,會帶來額外的性能開銷,因為內聯會把函數題替換掉,而不是調用函數

fun myRun(f: () -> Unit) = f()
fun main(args: Array<String>) {
    val name = "Kotlin"
    myRun { println("Hi, $name!") }
}
// class Examplekt$main$1

像 filter 這樣的函數,都是內聯的

但 inline 是 Kotlin 的特性,如果從 Java 調用,那不會有內聯

序列

Lambda 是內聯的,但是鏈式調用的中間過程的數據集合都會被產生

val list = listOf(12, -3
val maxOddSquare = list // [1, 2, -3]
    .map { it * it }  // [1, 4, 9]
    .filter { it % 2 == 1 }  // [1, 9]
    .max() // 1

序列 .asSequence() 推遲了計算髮生的時間,從而避免了中間過程中不斷產生集合

val list = listOf(12, -3
val maxOddSquare = list
 .asSequence()
    .map { it * it } 
    .filter { it % 2 == 1 } 
    .max()

Collections 中,每一次鏈式調用都會完成計算,因此得到 [m1, m2, m3, m4, f1, f2, f3, f4]

Sequences 中,每次對一個值完成全部的計算,因此得到 [m1, f1, m2, f2, m3, f3, m4, f4]

注意在 Sequences 中,除非需要這個值,否則不會計算

另外,但 Sequences 發現前面的步驟已經不滿足時,不會進行後面的步驟

Collections 和 Sequences 的類不是父類子類關係

val seq = generateSequence {
    Random.nextInt(5).takeIf { it > 0 }
}
println(seq.toList())

Sequences 都是懶惰計算的,除非到了需要的時候,否則不會完成計算

例如下面這個例子,問的只是 .first(),而第一個元素已知,所以不會去計算後面的元素,因此輸出 “Generating” 0 次

val numbers = generateSequence(3) { n ->
  println("Generating element...")
  (n + 1).takeIf { it < 7 } 
}
println(numbers.first()) // 3

yield 在 Kotlin 中不是語言特性、不是關鍵字,只是一個函數

但它是懶惰的,只在需要時被調用

val numbers = sequence {
    var x = 0
    while (true) {
        yield(x++)
    }
}
numbers.take(5).toList() // [0, 1, 2, 3, 4]

fun mySequence() = buildSequence { 
  println("yield one element"
  yield(1)
  println("yield a range"
  yieldAll(3..5)
  println("yield a list")
  yieldAll(listOf(79)) 
}

println(mySequence() 
  .map { it  it }
  .filter { it > 10 } 
  .take(1))
// 不會輸出任何一條 yield ...
// 因為 take() 不是最終操作

println(mySequence() 
  .map { it 
 it }
  .filter { it > 10 } 
  .first())
// 只會輸出 "yield one element" 和 "yield a range"
// first() 是終端操作
// 首先計算 1,經過 map 得到 1,被過濾
// 然後計算 3,經過 map 得到 9,被過濾
// 再計算 4,經過 map 得到 16,找到答案,程序結束,不會繼續後面的計算

帶接收器的 Lambda

拓展函數和 Lambda 結合,可以看作帶接收器的 Lambda,又叫拓展的 Lambda

val sb = StringBuilder()
sb.appendln("Alphabet: ")
for (c in 'a'..'z') {
    sb.append(c)
}
sb.toString()

這樣的代碼需要重複多次變量名,可以使用 with 簡化

val sb = StringBuilder()
with (sb) {
    appendln("Alphabet: ")
    for (c in 'a'..'z') {
        append(c)
    }
    toString()
}

事實上,with 是一個函數,sb 作為第一個參數,而這個 Lambda 表達式是第二個參數,即

with (sb, { this.toString() } )

val isEven: (Int) -> Boolean = { it % 2 == 0 }
val isOdd: Int.() -> Boolean = { this % 2 == 1 }

isEven(0)
1.isOdd()

使用庫函數簡化一些計算

people.filter { it.age < 21 }.size


people,count { it.age < 21 }

people.sortedBy { it.age }.reversed()


people.sortedByDesending { it.age }

people
  .map { person ->
    person.takeIf { it.isPublicProfile }?.name 
  }
  .filterNotNull()


people.mapNotNull { person ->
    person.takeIf { it.isPublicProfile }?.name
}

if (person.age !in map) {
  map[person.age] = mutableListOf() 
}
map.getValue(person.age) += person


val group = map.getOrPut(person.age) { mutableListOf() }
group += person

val map = mutableMapOf<Int, MutableList<Person>>() 
for (person in people) {
  if (person.age !in map) {
    map[person.age] = mutableListOf() 
  }
  map.getValue(person.age) += person 
}


people.groupBy { it.age }

groupBy()
// Write the name of the function that performs groupBy for Sequences in a lazy way.


groupingBy()

eachCount() // counts elements in each group

Kotlin 和 Java 中的數據類型

使用 Int 時,Kotlin 將其轉換為 int 字節碼,當使用 Int? 時,Kotlin將其轉換為 Integer 字節碼

List<Int> 仍然會被當成 List<Integer>

Array<Int>Integer[]IntArrayint[]

Kotlin 中的 String 就是 Java 中的 String,但隱藏了一些容易混淆的方法,例如 replaceAll 接收正則表達式

AnyObject,也是 Int 這些基本類型(在 Kotlin 中)的基類

除非是內聯的 Lambda 表達式,否則會被變成 Function0Function1 這樣,內聯的表達式會直接替換

可以顯式地在 Kotlin 中調用 invoke()

println(arrayOf(12) == arrayOf(12))

Kotin 中的數組和 Java 中的數組是一樣的,沒有魔法,所以上面的比較結果是 false,可以使用 contentEquals 來比較它們的內容

當只使用 Kotlin(而不需要從字節碼層面被 Java 使用)時,那麼沒有理由使用 Array,應該始終使用 List

Nothing 是 Kotlin 中的底層類型,Nothing 可以看做是任何類型的子類,但在字節碼層面,仍然會被轉化為 void,因為 Java 中沒有可以表示 Nothing 的類型

Unit 表示函數返回時沒有有意義的返回值,用來替代 Java 的 void,其在字節碼層面就是 void,完全等價

Nothing 表示函數永遠不會返回,例如在 fail() 函數中拋出異常,這是一個永遠不會執行完成的函數

Kotlin 中,TODO() 是一個內聯的函數,可以接受一個參數 String 表示一些備註信息,它的類型也是 Nothing

直接使用 return 也可以獲得 Nothing 類型

fun greetPerson(person: Person) {
    val name = person.name ?: return
    println("Hello, $name!")
}

val answer = if (timeHasPassed()) {
    42
else {
    fail("Not ready")
}
fun fail(message: String) {
    throw IllegalStateException(message)
}

這裡 answer 會被認為是 Any,因為當條件成立時,42 是一個 Int,而 fail()Unit,這兩個類型的公共父類是 Any,這與期望不合

val answer = if (timeHasPassed()) {
    42
else {
    fail("Not ready")
}
fun fail(message: String)Nothing {
    throw IllegalStateException(message)
}

這裡 answer 會被認為是 Int,因為當條件成立時,42 是一個 Int,而 fail()NothingNothing 可以看做是任何類型的子類

最簡單的、也是唯一的一個 Nothing? 類型是 null 常量

類型後面加 ! 例如 String! 往往只會出現在錯誤信息中,例如數據類型不匹配的錯誤,來表示這個類型是來自 Java 的

// Java
public class Session {
  public String getDescription() {
    return null
  }
}
// Kotlin
val session = Session()
val description = session.description // description 的類型是 "String!"
println(description.length) // NullPointerException

這樣會使得 Kotlin 中的 Nullable 檢查毫無用處,因為依然可能出現 Null Pointer Exception,而不需要明確地檢查是不是為 null

這種情況可以在 Java 代碼中增加註解 @Nullable@NonNull 等,這樣 Kotlin 就可以強制檢查 Nullable 的數據

// Java
public class Session {
  @Nullable
  String getDescription() {
    return null;
  }
}
// Kotlin
val session = Session()
val description = session.description
println(description.length) // 無法通過編譯

可以將 @NotNull 設置為默認(由 JSR-305 支持的 @ParametersAreNonnullByDefault@MyNonnullByDefault),這樣只需要註釋 @Nullable 的類型即可

也可以根據自己的需要指定另一個默認值

但注意 Kotlin 將默認 NotNull 的數據類型、卻接收了 null 這樣的問題,只是看作警告,需要添加 -Xjsr305=strict 編譯選項,Kotlin 才會把它們看作錯誤

預防 Null Pointer Exception,除了使用 Java 註解,還可以在 Kotlin 代碼中明確數據類型,例如 String? 或者 String,而不要讓編譯器自己猜測

明確數據類型可以得到以下不同的結果:

// Java
public class Session {
  String getDescription() {
    return null;
  }
}
// Kotlin
val session = Session()
val description: String? = session.description // 這是 String? 類型
println(description?.length) // 輸出 null

// Java
public class Session {
  String getDescription() {
    return null;
  }
}
// Kotlin
val session = Session()
val description: String = session.description // 這是 String 類型,不能為空
println(description.length) // 拋出 IllegalStateException,不是 NUllPointerException

kotlin.Listjava.util.List 是一樣的,MutableList 繼承自 List

注意只讀和不可變是不一樣的,不能對 List 使用 add,因為它沒有 mutating 方法,但可以通過 MutableList 來修改

val mutableList = mutableListOf(123)  //#1 
val list: List<Int> = mutableList         //#2 
mutableList.add(4)                        //#3 
println(list)                             //#4 

這依然會輸出 [1, 2, 3, 4]

在底層,kotlin.List 有一個子類 kotlin.MutableList,而 kotlin.MutableList 會用 java.util.ArrayList 來實現

使用只讀類型,例如 List,可以防止自己意外地調用 .add() 這樣的方法,除非把它明確地交給 Mutable,那就可以修改

Leave a Reply

Your email address will not be published. Required fields are marked *