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
修飾符,表示自動生成 equals
、hashCode
和 toString
這三個函式
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)}
“變數”分為 val
和 var
,val
是隻讀的
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");
常用的有 withIndex
、getOrNull
、joinToString
、until
、to
、eq
、toInt
等等
val set = hashSetOf(1, 7, 53)
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
特別的,until
和 to
這種,本身是需要通過 .
和 ()
呼叫的
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 == 0) return true
}
return false
}
// 這個 forEach 接受了一個 Lambda 表示式,但是 return 是返回到 fun containsZero 的
fun duplicateNonZero(list: List<Int>): List<Int> {
return list.flatMap {
if (it == 0) return listOf()
listOf(it, it)
}
}
duplicateNonZero(listOf(3, 0, 5)) // 輸出 []
// 因為 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 == 0) return@flatMap listOf<Int>()
listOf(it, it)
}
list.flatMap l@ { // 自定義標籤 l
if (it == 0) return@l listOf<Int>()
listOf(it, it)
}
另外的解決方案是使用本地函式或者匿名函式
fun duplicateNonZeroLocalFunction(list: List<Int>): List<Int> {
fun duplicateNonZeroElement(e: Int): List<Int> {
if (e == 0) return 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 == 0) return listOf()
return listOf(e, e)
})
}
forEach
中的 return
不會像 break
一樣響應
fun foo(list: List<Int>) {
list.forEach {
if (it == 0) return
print(it)
}
print("##") // 如果 list 包含 0,則不會輸出 ##
}
fun bar(list: List<Int>) {
for (element in list) {
if (element == 0) break
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: Int, val 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 Child: Parent() {
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: Int, val g: Int, val b: Int) {
BLUE(0, 0, 255), ORANGE(255, 165, 0), RED(255, 0, 0);
fun rgb() = (r * 256 + g) * 256 + b
}
BLUE.r
BLUE.rgb()
data
有 equals
、copy
、hashCode
和 toString
等方法
data class Contact(val name: String, val address: String)
contact.copy(address = "new address")
在 Kotlin 中,==
預設比較它們的 equals
,而 ===
比較它們是不是同一個引用
在 Java 中,==
比較是否是同一個引用,需要使用 equals
來比較它們
class Foo(val first: Int, val second: Int)
data class Bar(val first: Int, val second: Int)
val f1 = Foo(1, 2)
val f2 = Foo(1, 2)
println(f1 == f2) // false
val b1 = Bar(1, 2)
val b2 = Bar(1, 2)
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(1, null)) // OK
fun <T: Any> foo (list: List<T>) {
for (element in List) {
}
}
foo(listOf(1, null)) // 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(1, 2) + Point(2, 3)
過載的運算子左右兩邊的資料型別可以不一樣
operator fun Point.times(scale: Int): Point {
return Point(x * scale, y * scale)
}
Point(1, 2) * 3
單目運算子也可以過載,例如 unaryMinus
、not
、inc
注意對於 list 這樣的型別,+= 的操作會新建一個新的 list,例如下面這段程式碼會輸出 [1, 2, 3, 4]
和 [1, 2, 3]
val list1 = listOf(1, 2, 3)
var list2 = list1
list2 += 4
println(list1)
println(list2)
如果需要,可以把 var
和 listOf
換成 val
和 mutableListOf
在 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)
takeUnless
與 takeIf
相反
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(1, 2, -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(1, 2, -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(7, 9))
}
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[]
,IntArray
是 int[]
Kotlin 中的 String
就是 Java 中的 String
,但隱藏了一些容易混淆的方法,例如 replaceAll
接收正規表示式
Any
是 Object
,也是 Int 這些基本型別(在 Kotlin 中)的基類
除非是內聯的 Lambda 表示式,否則會被變成 Function0
、Function1
這樣,內聯的表示式會直接替換
可以顯式地在 Kotlin 中呼叫 invoke()
println(arrayOf(1, 2) == arrayOf(1, 2))
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()
是 Nothing
,Nothing
可以看做是任何型別的子類
最簡單的、也是唯一的一個 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.List
和 java.util.List
是一樣的,MutableList
繼承自 List
注意只讀和不可變是不一樣的,不能對 List
使用 add
,因為它沒有 mutating 方法,但可以通過 MutableList
來修改
val mutableList = mutableListOf(1, 2, 3) //#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,那就可以修改