原文章地址:[Kotlin 里那些「更方便的」](https://kaixue.io/kotlin-basic-3/)
在上期內容當中,我們介紹了 Kotlin 的那些與 Java 寫法不同的地方。這一期我們再進階一點,講一講 Kotlin 中那些「更方便的」用法。這些知識點在不知道之前,你也可以正常寫 Kotlin,但是在熟悉之后會讓你寫得更爽。
[TOC]
## 構造器
### 主構造器
我們之前已經了解了 Kotlin 中 constructor 的寫法:
~~~kotlin
???
class User {
var name: String
constructor(name: String) {
this.name = name
}
}
~~~
其實 Kotlin 中還有更簡單的方法來寫構造器:
~~~kotlin
???
??
class User constructor(name: String) {
// ?? 這里與構造器中的 name 是同一個
var name: String = name
}
~~~
這里有幾處不同點:
* `constructor`構造器移到了類名之后
* **類的屬性`name`可以直接引用構造器中的參數`name`**
這個寫法叫「主構造器 primary constructor」。與之相對的在第二篇中,寫在類中的構造器被稱為「次構造器」。**在 Kotlin 中一個類最多只能有 1 個主構造器(也可以沒有),而次構造器是沒有個數限制**。
**主構造器中的參數除了可以在類的屬性中使用,還可以在`init`代碼塊中使用**:
~~~kotlin
???
class User constructor(name: String) {
var name: String
init {
this.name = name
}
}
~~~
其中`init`代碼塊是緊跟在主構造器之后執行的,這是**因為主構造器本身沒有代碼體,`init`代碼塊就充當了主構造器代碼體的功能**。
另外,**如果類中有主構造器,那么其他的次構造器都需要通過`this`關鍵字調用主構造器(硬性要求),可以直接調用或者通過別的次構造器間接調用。如果不調用 IDE 就會報錯**:
~~~kotlin
???
class User constructor(var name: String) {
constructor(name: String, id: Int) {
// ??這樣寫會報錯,Primary constructor call expected
}
}
~~~
為什么當類中有主構造器的時候就強制要求次構造器調用主構造器呢?
我們**從主構造器的特性出發,一旦在類中聲明了主構造器,就包含兩點**:
* 必須性:創建類的對象時,不管使用哪個構造器,都需要主構造器的參與
* 第一性:在類的初始化過程中,首先執行的就是主構造器
這也就是主構造器的命名由來。
當一個類中同時有主構造器與次構造器的時候,需要這樣寫:
~~~kotlin
???
class User constructor(var name: String) {
// ?? ?? 直接調用主構造器
constructor(name: String, id: Int) : this(name) {
}
// ?? 通過上一個次構造器,間接調用主構造器
constructor(name: String, id: Int, age: Int) : this(name, id) {
}
}
~~~
**在使用次構造器創建對象時,`init`代碼塊是先于次構造器執行的。如果把主構造器看成身體的頭部,那么`init`代碼塊就是頸部,次構造器就相當于身體其余部分**。
細心的你也許會發現這里**又出現了`:`符號**,它還在其他場合出現過,例如:
* 變量的聲明:`var id: Int`
* 類的繼承:`class MainActivity : AppCompatActivity() {}`
* 接口的實現:`class User : Impl {}`
* 匿名類的創建:`object: ViewPager.SimpleOnPageChangeListener() {}`
* 函數的返回值:`fun sum(a: Int, b: Int): Int`
可以看出`:`符號在 Kotlin 中非常高頻出現,它**其實表示了一種依賴關系,在這里表示依賴于主構造器**。
**通常情況下,主構造器中的`constructor`關鍵字可以省略**:
~~~kotlin
???
class User(name: String) {
var name: String = name
}
~~~
**但有些場景,`constructor`是不可以省略的,例如在主構造器上使用「可見性修飾符」或者「注解」**:
* 可見性修飾符我們之前已經講過,它修飾普通函數與修飾構造器的用法是一樣的,這里不再詳述:
~~~kotlin
???
class User private constructor(name: String) {
// ?? 主構造器被修飾為私有的,外部就無法調用該構造器
}
~~~
* 關于注解的知識點,我們之后會講,這里就不展開了
既然主構造器可以簡化類的初始化過程,那我們就幫人幫到底,送佛送到西,**用主構造器把屬性的初始化也一并給簡化了**。
### 主構造器里聲明屬性
之前我們講了主構造器中的參數可以在屬性中進行賦值,其實還可以在主構造器中直接聲明屬性:
~~~kotlin
???
??
class User(var name: String) {
}
// 等價于:
class User(name: String) {
var name: String = name
}
~~~
**如果在主構造器的參數聲明時加上`var`或者`val`,就等價于在類中創建了該名稱的屬性(property),并且初始值就是主構造器中該參數的值**。
以上講了所有關于主構造器相關的知識,讓我們總結一下類的初始化寫法:
* 首先創建一個`User`類:
~~~kotlin
???
class User {
}
~~~
* 添加一個參數為`name`與`id`的主構造器:
~~~kotlin
???
class User(name: String, id: String) {
}
~~~
* 將主構造器中的`name`與`id`聲明為類的屬性:
~~~kotlin
???
class User(val name: String, val id: String) {
}
~~~
* 然后在`init`代碼塊中添加一些初始化邏輯:
~~~kotlin
???
class User(val name: String, val id: String) {
init {
...
}
}
~~~
* 最后再添加其他次構造器:
~~~kotlin
???
class User(val name: String, val id: String) {
init {
...
}
constructor(person: Person) : this(person.name, person.id) {
}
}
~~~
**當一個類有多個構造器時,只需要把最基本、最通用的那個寫成主構造器就行了**。這里我們選擇將參數為`name`與`id`的構造器作為主構造器。
到這里,整個類的初始化就完成了,類的初始化順序就和上面的步驟一樣。
除了構造器,普通函數也是有很多簡化寫法的。
## 函數簡化
### 使用`=`連接返回值
我們已經知道了 Kotlin 中函數的寫法:
~~~kotlin
???
fun area(width: Int, height: Int): Int {
return width * height
}
~~~
其實,這種只有一行代碼的函數,還可以這么寫:
~~~kotlin
???
??
fun area(width: Int, height: Int): Int = width * height
~~~
`{}`和`return`沒有了,使用`=`符號連接返回值。
我們之前講過,Kotlin 有「類型推斷」的特性,那么這里函數的返回類型還可以隱藏掉:
~~~kotlin
???
// ??省略了返回類型
fun area(width: Int, height: Int) = width * height
~~~
**不過,在實際開發中,還是推薦顯式地將返回類型寫出來,增加代碼可讀性**。
以上是函數有返回值時的情況,**對于沒有返回值的情況,可以理解為返回值是`Unit`**:
~~~kotlin
???
fun sayHi(name: String) {
println("Hi " + name)
}
~~~
因此也可以簡化成下面這樣:
~~~kotlin
???
??
fun sayHi(name: String) = println("Hi " + name)
~~~
簡化完函數體,我們再來看看前面的參數部分。
對于 Java 中的方法重載,我們都不陌生,那 Kolin 中是否有更方便的重載方式呢?接下來我們看看 Kotlin 中的「參數默認值」的用法。
### 參數默認值
Java 中,允許在一個類中定義多個名稱相同的方法,但是參數的類型或個數必須不同,這就是方法的重載:
~~~java
??
public void sayHi(String name) {
System.out.println("Hi " + name);
}
public void sayHi() {
sayHi("world");
}
~~~
在 Kotlin 中,也可以使用這樣的方式進行函數的重載,不過還有一種更簡單的方式,那就是「**參數默認值**」:
~~~kotlin
???
??
fun sayHi(name: String = "world") = println("Hi " + name)
~~~
這里的`world`是參數`name`的默認值,當調用該函數時不傳參數,就會使用該默認值。
這就等價于上面 Java 寫的重載方法,當調用`sayHi`函數時,參數是可選的:
~~~kotlin
???
sayHi("kaixue.io")
sayHi() // 使用了默認值 "world"
~~~
既然與重載函數的效果相同,那 Kotlin 中的參數默認值有什么好處呢?僅僅只是少寫了一些代碼嗎?
其實在 Java 中,每個重載方法的內部實現可以各不相同,這就無法保證重載方法內部設計上的一致性,而 Kotlin 的參數默認值就解決了這個問題。
不過參數默認值在調用時也不是完全可以放飛自我的。
來看下面這段代碼,這里函數中有默認值的參數在無默認值參數的前面:
~~~kotlin
???
fun sayHi(name: String = "world", age: Int) {
...
}
sayHi(10)
// ?? 這時想使用默認值進行調用,IDE 會報以下兩個錯誤
// The integer literal does not conform to the expected type String
// No value passed for parameter 'age'
~~~
這個錯誤就是告訴你參數不匹配,說明我們的「打開方式」不對,其實 Kotlin 里是通過「命名參數」來解決這個問題的。
### 命名參數
具體用法如下:
~~~kotlin
???
fun sayHi(name: String = "world", age: Int) {
...
}
??
sayHi(age = 21)
~~~
**在調用函數時,顯式地指定了參數`age`的名稱,這就是「命名參數」**。Kotlin 中的每一個函數參數都可以作為命名參數。
再來看一個有非常多參數的函數的例子:
~~~kotlin
???
fun sayHi(name: String = "world", age: Int, isStudent: Boolean = true, isFat: Boolean = true, isTall: Boolean = true) {
...
}
~~~
當函數中有非常多的參數時,調用該函數就會寫成這樣:
~~~kotlin
???
sayHi("world", 21, false, true, false)//一長串的布爾值,很難分清楚每個參數的用處,可讀性很差
~~~
**當看到后面一長串的布爾值時,我們很難分清楚每個參數的用處,可讀性很差**。通過命名參數,我們就可以這么寫:
~~~kotlin
???
sayHi(name = "wo", age = 21, isStudent = false, isFat = true, isTall = false)
~~~
與命名參數相對的一個概念被稱為「**位置參數**」,也就是**按位置順序進行參數填寫**。
當一個函數被調用時,**如果混用位置參數與命名參數,那么所有的位置參數都應該放在第一個命名參數之前**:
**如果一個默認參數在一個無默認值的參數之前,那么該默認值只能通過使用[命名參數](http://www.kotlincn.net/docs/reference/functions.html#%E5%91%BD%E5%90%8D%E5%8F%82%E6%95%B0)調用該函數來使用**
~~~kotlin
???
fun sayHi(name: String = "world", age: Int) {
...
}
sayHi(name = "wo", 21) // ?? IDE 會報錯,Mixing named and positioned arguments is not allowed
sayHi("wo", age = 21) // ?? 這是正確的寫法
~~~
講完了命名參數,我們再看看 Kotlin 中的另一種常見函數:嵌套函數。
### 本地函數(嵌套函數)
首先來看下這段代碼,這是一個簡單的登錄的函數:
~~~kotlin
???
fun login(user: String, password: String, illegalStr: String) {
// 驗證 user 是否為空
if (user.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
// 驗證 password 是否為空
if (password.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}
~~~
該函數中,**檢查參數這個部分有些冗余**,我們又不想將這段邏輯作為一個單獨的函數對外暴露。這時可以使用嵌套函數,在`login`函數內部聲明一個函數:
~~~kotlin
???
fun login(user: String, password: String, illegalStr: String) {
??
fun validate(value: String, illegalStr: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}
??
validate(user, illegalStr)
validate(password, illegalStr)
}
~~~
這里我們**將共同的驗證邏輯放進了嵌套函數`validate`中,并且`login`函數之外的其他地方無法訪問這個嵌套函數**。
這里的`illegalStr`是通過參數的方式傳進嵌套函數中的,其實完全沒有這個必要,因為**嵌套函數中可以訪問在它外部的所有變量或常量,例如類中的屬性、當前函數中的參數與變量等**。
我們稍加改進:
~~~kotlin
???
fun login(user: String, password: String, illegalStr: String) {
fun validate(value: String) {
if (value.isEmpty()) {
??
throw IllegalArgumentException(illegalStr)
}
}
...
}
~~~
這里省去了嵌套函數中的`illegalStr`參數,在該嵌套函數內直接使用外層函數`login`的參數`illegalStr`。
上面`login`函數中的驗證邏輯,其實還有另一種更簡單的方式:
~~~kotlin
???
fun login(user: String, password: String, illegalStr: String) {
require(user.isNotEmpty()) { illegalStr }
require(password.isNotEmpty()) { illegalStr }
}
~~~
其中**用到了 lambda 表達式以及 Kotlin 內置的`require`函數**,這里先不做展開,之后的文章會介紹。
## 字符串
講完了普通函數的簡化寫法,Kotlin 中字符串也有很多方便寫法。
### 字符串模板
在 Java 中,字符串與變量之間是使用`+`符號進行拼接的,Kotlin 中也是如此:
~~~kotlin
???
val name = "world"
println("Hi " + name)
~~~
但是當變量比較多的時候,可讀性會變差,寫起來也比較麻煩。
Java 給出的解決方案是`String.format`:
~~~java
??
System.out.print(String.format("Hi %s", name));
~~~
Kotlin 為我們提供了一種更加方便的寫法:
~~~kotlin
???
val name = "world"
// ?? 用 '$' 符號加參數的方式
println("Hi $name")
~~~
這種方式就是把`name`從后置改為前置,簡化代碼的同時增加了字符串的可讀性。
**除了變量,`$`后還可以跟表達式,但表達式是一個整體,所以我們要用`{}`給它包起來**:
~~~kotlin
???
val name = "world"
println("Hi ${name.length}")
~~~
其實就跟四則運算的括號一樣,提高語法上的優先級,而單個變量的場景可以省略`{}`。
字符串模板還支持轉義字符,比如使用轉義字符`\n`進行換行操作:
~~~kotlin
???
val name = "world!\n"
println("Hi $name") // ?? 會多打一個空行
~~~
字符串模板的用法對于我們 Android 工程師來說,其實一點都不陌生。
首先,Gradle 所用的 Groovy 語言就已經有了這種支持:
~~~groovy
def name = "world"
println "Hi ${name}"
~~~
在 Android 的資源文件里,定義字符串也有類似用法:
~~~xml
<string name="hi">Hi %s</string>
~~~
~~~java
??
getString(R.id.hi, "world");
~~~
### raw string (原生字符串)
有時候我們不希望寫過多的轉義字符,這種情況 Kotlin 通過「**原生字符串**」來實現。
用法就是**使用一對`"""`將字符串括起來**:
~~~kotlin
???
val name = "world"
val myName = "kotlin"
??
val text = """
Hi $name!
My name is $myName.\n
"""
println(text)
~~~
這里有幾個注意點:
* `\n`并不會被轉義
* 最后輸出的內容與寫的內容完全一致,包括實際的換行
* `$`符號引用變量仍然生效
這就是「原生字符串」。輸出結果如下:
~~~
Hi world!
My name is kotlin.\n
~~~
但對齊方式看起來不太優雅,**原生字符串還可以通過`trimMargin()`函數去除每行前面的空格**:
~~~kotlin
???
val text = """
??
|Hi world!
|My name is kotlin.
""".trimMargin()
println(text)
~~~
輸出結果如下:
~~~
Hi world!
My name is kotlin.
~~~
這里的`trimMargin()`函數有以下幾個注意點:
* **`|`符號為默認的邊界前綴,前面只能有空格,否則不會生效**
* 輸出時`|`符號以及它前面的空格都會被刪除
* 邊界前綴還可以使用其他字符,比如`trimMargin("/")`,只不過上方的代碼使用的是參數默認值的調用方式
字符串的部分就先到這里,下面來看看數組與集合有哪些更方便的操作。
## 數組和集合
### 數組與集合的操作符
在之前的文章中,我們已經知道了數組和集合的基本概念,其實 Kotlin 中,還為我們提供了許多使數組與集合操作起來更加方便的函數。
首先聲明如下`IntArray`和`List`:
~~~kotlin
???
val intArray = intArrayOf(1, 2, 3)
val strList = listOf("a", "b", "c")
~~~
接下來,對它們的操作函數進行講解:
* `forEach`:遍歷每一個元素
~~~kotlin
???
// ?? lambda 表達式,i 表示數組的每個元素
intArray.forEach { i ->
print(i + " ")
}
// 輸出:1 2 3
~~~
除了「lambda」表達式,這里也用到了「閉包」的概念,這又是另一個話題了,這里先不展開。
* **`filter`:對每個元素進行過濾操作,如果 lambda 表達式中的條件成立則留下該元素,否則剔除,最終生成新的集合**
~~~kotlin
???
// [1, 2, 3]
??
// {2, 3}
// ?? 注意,這里變成了 List
val newList: List = intArray.filter { i ->
i != 1 // ?? 過濾掉數組中等于 1 的元素
}
~~~
* **`map`:遍歷每個元素并執行給定表達式,最終形成新的集合**
~~~kotlin
???
// [1, 2, 3]
??
// {2, 3, 4}
val newList: List = intArray.map { i ->
i + 1 // ?? 每個元素加 1
}
~~~
* `flatMap`:遍歷每個元素,并為每個元素創建新的集合,最后合并到一個集合中
~~~kotlin
???
// [1, 2, 3]
??
// {"2", "a" , "3", "a", "4", "a"}
intArray.flatMap { i ->
listOf("${i + 1}", "a") // ?? 生成新集合
}
~~~
關于為什么數組的`filter`之后變成`List`,就留作思考題吧~
這**里是以數組`intArray`為例,集合`strList`也同樣有這些操作函數**。Kotlin 中還有許多類似的操作函數,這里就不一一列舉了。
**除了數組和集合,Kotlin 中還有另一種常用的數據類型:`Range`(區間)**。
### `Range`
在 Java 語言中并沒有`Range`的概念,**Kotlin 中的`Range`表示區間的意思,也就是范圍**。區間的常見寫法如下:
~~~kotlin
???
?? ??
val range: IntRange = 0..1000
~~~
這里的`0..1000`就表示從 0 到 1000 的范圍,**包括 1000**,數學上稱為閉區間 \[0, 1000\]。除了這里的`IntRange`,還有`CharRange`以及`LongRange`。
Kotlin 中沒有純的開區間的定義,不過有**半開區間**的定義:
~~~kotlin
???
??
val range: IntRange = 0 until 1000
~~~
這里的`0 until 1000`表示從 0 到 1000,但**不包括 1000**,這就是半開區間 \[0, 1000) 。
`Range`這個東西,天生就是用來遍歷的:
~~~kotlin
???
val range = 0..1000
// ?? 默認步長為 1,輸出:0, 1, 2, 3, 4, 5, 6, 7....1000,
for (i in range) {
print("$i, ")
}
~~~
這里的`in`關鍵字可以與`for`循環結合使用,表示挨個遍歷`range`中的值。關于`for`循環控制的使用,在本期文章的后面會做具體講解。
除了使用默認的步長 1,**還可以通過`step`設置步長**:
~~~kotlin
???
val range = 0..1000
// ?? 步長為 2,輸出:0, 2, 4, 6, 8, 10,....1000,
for (i in range step 2) {
print("$i, ")
}
~~~
以上是遞增區間,Kotlin 還提供了**遞減區間`downTo`**,不過**遞減沒有半開區間的用法:**
~~~kotlin
???
// ?? 輸出:4, 3, 2, 1,
for (i in 4 downTo 1) {
print("$i, ")
}
~~~
其中`4 downTo 1`就表示遞減的閉區間 \[4, 1\]。這里的`downTo`以及上面的`step`都叫做「**中綴表達式**」,之后的文章會做介紹。
### `Sequence(序列)`
官方序列介紹——[序列](http://www.kotlincn.net/docs/reference/sequences.html#%E5%BA%8F%E5%88%97)
除了集合之外,Kotlin 標準庫還包含另一種容器類型——*序列*([`Sequence<T>`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/-sequence/index.html))。 序列提供與[`Iterable`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-iterable/index.html)相同的函數,但實現另一種方法來進行多步驟集合處理。序列的多步處理在可能的情況下會延遲執?:僅當請求整個處理
鏈的結果時才進?實際計算。
操作執?的順序也不同:**Sequence 對每個元素逐個執?所有處理步驟**。 反過來,Iterable 完成整個集合的每個步驟,然后進?下?步。
在上一期中我們已經熟悉了`Sequence`的基本概念,這次讓我們更加深入地了解`Sequence`序列的使用方式。
**序列`Sequence`又被稱為「惰性集合操作」**,關于什么是惰性,我們通過下面的例子來理解:
~~~kotlin
???
val sequence = sequenceOf(1, 2, 3, 4)
val result: Sequence<Int> = sequence
.map { i ->
println("Map $i")
i * 2
}
.filter { i ->
println("Filter $i")
i % 3 == 0
}
??
println(result.first()) // ?? 只取集合的第一個元素
~~~
輸出結果
```
Map 1
Filter 2
Map 2
Filter 4
Map 3
Filter 6
6
```
* **惰性的概念首先就是說在「??」標注之前的代碼運行時不會立即執行,它只是定義了一個執行流程,只有`result`被使用到的時候才會執行**
* 當「??」的`println`執行時數據處理流程是這樣的:
* 取出元素 1 -> map 為 2 -> filter 判斷 2 是否能被 3 整除
* 取出元素 2 -> map 為 4 -> filter 判斷 4 是否能被 3 整除
* ...
惰性指當出現滿足條件的第一個元素的時候,`Sequence`就不會執行后面的元素遍歷了,即跳過了`4`的遍歷。
而`List`是沒有惰性的特性的(先執行整個集合):
~~~kotlin
???
val list = listOf(1, 2, 3, 4)
val result: List = list
.map { i ->
println("Map $i")
i * 2
}
.filter { i ->
println("Filter $i")
i % 3 == 0
}
??
println(result.first()) // ?? 只取集合的第一個元素
~~~
輸出結果
```
Map 1
Map 2
Map 3
Map 4
Filter 2
Filter 4
Filter 6
Filter 8
6
```
包括兩點:
* 聲明之后立即執行
* 數據處理流程如下:
* {1, 2, 3, 4} -> {2, 4, 6, 8}
* 遍歷判斷是否能被 3 整除
`Sequence`這種類似懶加載的實現有下面這些優點:
* 一旦滿足遍歷退出的條件,就可以省略后續不必要的遍歷過程。
* 像`List`這種實現`Iterable`接口的集合類,每調用一次函數就會生成一個新的`Iterable`,下一個函數再基于新的`Iterable`執行,每次函數調用產生的臨時`Iterable`會導致額外的內存消耗,**而`Sequence`在整個流程中只有一個**。
因此,**`Sequence`這種數據類型可以在數據量比較大或者數據量未知的時候,作為流式處理的解決方案**。
## 條件控制
相比 Java 的條件控制,Kotlin 中對條件控制進行了許多的優化及改進。
### `if/else`
首先來看下 Java 中的`if/else`寫法:
~~~java
??
int max;
if (a > b) {
max = a;
} else {
max = b;
}
~~~
在 Kotlin 中,這么寫當然也可以,不過,**Kotlin 中`if`語句還可以作為一個表達式賦值給變量**:
~~~kotlin
???
??
val max = if (a > b) a else b
~~~
另外,Kotlin 中棄用了三元運算符(條件 ? 然后 : 否則),不過我們可以使用`if/else`來代替它。
**上面的`if/else`的分支中是一個變量,其實還可以是一個代碼塊,代碼塊的最后一行會作為結果返回**:
~~~kotlin
???
val max = if (a > b) {
println("max:a")
a // ?? 返回 a
} else {
println("max:b")
b // ?? 返回 b
}
~~~
### `when`
在 Java 中,用`switch`語句來判斷一個變量與一系列值中某個值是否相等:
~~~java
??
switch (x) {
case 1: {
System.out.println("1");
break;
}
case 2: {
System.out.println("2");
break;
}
default: {
System.out.println("default");
}
}
~~~
在 Kotlin 中變成了`when`:
~~~kotlin
???
??
when (x) {
??
1 -> { println("1") }
2 -> { println("2") }
??
else -> { println("else") }
}
~~~
這里與 Java 相比的不同點有:
* 省略了`case`和`break`,前者比較好理解,后者的意思是 **Kotlin 自動為每個分支加上了`break`的功能**,防止我們像 Java 那樣寫錯
* **Java 中的默認分支使用的是`default`關鍵字,Kotlin 中使用的是`else`**
與`if/else`一樣,**`when`也可以作為表達式進行使用,分支中最后一行的結果作為返回值。需要注意的是,這時就必須要有`else`分支,使得無論怎樣都會有結果返回,除非已經列出了所有情況**:
~~~kotlin
???
val value: Int = when (x) {
1 -> { x + 1 }
2 -> { x * 2 }
else -> { x + 5 }
}
~~~
在 Java 中,當多種情況執行同一份代碼時,可以這么寫:
~~~kotlin
??
switch (x) {
case 1:
case 2: {
System.out.println("x == 1 or x == 2");
break;
}
default: {
System.out.println("default");
}
}
~~~
而 **Kotlin 中多種情況執行同一份代碼時,可以將多個分支條件放在一起,用`,`符號隔開,表示這些情況都會執行后面的代碼**:
~~~kotlin
???
when (x) {
??
1, 2 -> print("x == 1 or x == 2")
else -> print("else")
}
~~~
**在`when`語句中,我們還可以使用表達式作為分支的判斷條件**:
* 使用`in`檢測是否在一個區間或者集合中:
~~~kotlin
???
when (x) {
??
in 1..10 -> print("x 在區間 1..10 中")
??
in listOf(1,2) -> print("x 在集合中")
?? // not in
!in 10..20 -> print("x 不在區間 10..20 中")
else -> print("不在任何區間上")
}
~~~
* 或者**使用`is`進行特定類型的檢測**:
~~~kotlin
???
val isString = when(x) {
??
is String -> true
else -> false
}
~~~
* 還可以**省略`when`后面的參數,每一個分支條件都可以是一個布爾表達式**:
~~~kotlin
???
when {
??
str1.contains("a") -> print("字符串 str1 包含 a")
??
str2.length == 3 -> print("字符串 str2 的長度為 3")
}
~~~
**當分支的判斷條件為表達式時,哪一個條件先為`true`就執行哪個分支的代碼塊**。
### `for`
我們知道 Java 對一個集合或數組可以這樣遍歷:
~~~kotlin
??
int[] array = {1, 2, 3, 4};
for (int item : array) {
...
}
~~~
而 Kotlin 中 對數組的遍歷是這么寫的:
~~~kotlin
???
val array = intArrayOf(1, 2, 3, 4)
??
for (item in array) {
...
}
~~~
這里與 Java 有幾處不同:
* 在 Kotlin 中,**表示單個元素的`item`,不用顯式的聲明類型**
* Kotlin 使用的是`in`關鍵字,表示`item`是`array`里面的一個元素
另外,**Kotlin 的`in`后面的變量可以是任何實現`Iterable`接口的對象**。
在 Java 中,我們還可以這么寫`for`循環:
~~~kotlin
??
for (int i = 0; i <= 10; i++) {
// 遍歷從 0 到 10
}
~~~
但 Kotlin 中沒有這樣的寫法,那應該怎樣實現一個 0 到 10 的遍歷呢?
其實使用上面講過的區間就可以實現啦,代碼如下:
~~~kotlin
???
for (i in 0..10) {
println(i)
}
~~~
### `try-catch`
關于`try-catch`我們都不陌生,在平時開發中難免都會遇到異常需要處理,那么在 Kotlin 中是怎樣處理的呢,先來看下 Kotlin 中捕獲異常的代碼:
~~~kotlin
???
try {
...
}
catch (e: Exception) {
...
}
finally {
...
}
~~~
可以發現 Kotlin 異常處理與 Java 的異常處理基本相同,但也有幾個不同點:
* 我們知道在 Java 中,調用一個拋出異常的方法時,我們需要對異常進行處理,否則就會報錯:
~~~java
??
public class User{
void sayHi() throws IOException {
}
void test() {
sayHi();
// ?? IDE 報錯,Unhandled exception: java.io.IOException
}
}
~~~
但在 Kotlin 中,調用上方`User`類的`sayHi`方法時:
~~~kotlin
???
val user = User()
user.sayHi() // ?? 正常調用,IDE 不會報錯,但運行時會出錯
~~~
**為什么這里不會報錯呢?因為 Kotlin 中的異常是不會被檢查的,只有在運行時如果`sayHi`拋出異常,才會出錯**。
* **Kotlin 中`try-catch`語句也可以是一個表達式,允許代碼塊的最后一行作為返回值**:
~~~kotlin
???
??
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
~~~
### `?.`和`?:`
我們在之前的文章中已經講過 Kotlin 的空安全,其實還有另外一個常用的復合符號可以讓你在判空時更加方便,那就是 Elvis 操作符`?:`。
我們知道空安全調用`?.`,在對象非空時會執行后面的調用,對象為空時就會返回`null`。如果**這時將該表達式賦值給一個不可空的變量(下面的length)**:
~~~kotlin
???
val str: String? = "Hello"
var length: Int = str?.length
// ?? ,IDE 報錯,Type mismatch. Required:Int. Found:Int?
~~~
報錯的原因就是`str`為 null 時我們沒有值可以返回給`length`
這時就可以使用 Kotlin 中的 Elvis 操作符`?:`來兜底:
~~~kotlin
???
val str: String? = "Hello"
??
val length: Int = str?.length ?: -1
~~~
它的意思是**如果左側表達式`str?.length` 結果為空,則返回右側的值`-1`**。
Elvis 操作符還有另外一種常見用法,如下:
~~~kotlin
???
fun validate(user: User) {
val id = user.id ?: return // ?? 驗證 user.id 是否為空,為空時 return
}
// 等同于
fun validate(user: User) {
if (user.id == null) {
return
}
val id = user.id
}
~~~
看到這里,想必你對 Kotlin 的空安全有了更深入的了解了,下面我們再看看 Kotlin 的相等比較符。
### `==`和`===`
我們知道在 Java 中,`==`比較的如果是基本數據類型則判斷值是否相等,如果比較的是`String`則表示引用地址是否相等,`String`字符串的內容比較使用的是`equals()`:
~~~java
??
String str1 = "123", str2 = "123";
System.out.println(str1.equals(str2));
System.out.println(str1 == str2);
~~~
Kotlin 中也有兩種相等比較方式:
* `==`:可以**對基本數據類型以及`String`等類型進行內容比較**,相當于 Java 中的`equals`
* `===`:對**引用的內存地址進行比較**,相當于 Java 中的`==`
可以發現,Java 中的`equals`,在 Kotlin 中與之相對應的是`==`,這樣可以使我們的代碼更加簡潔。
下面再來看看代碼示例:
~~~kotlin
???
val str1 = "123"
val str2 = "123"
println(str1 == str2) // ?? 內容相等,輸出:true
val str1= "字符串"
val str2 = str1
val str3 = str1
print(str2 === str3) // ?? 引用地址相等,輸出:true
~~~
其實 **Kotlin 中的`equals`函數是`==`的操作符重載**,關于操作符重載,這里先不講,之后的文章會講到。
## 練習題
1. 請按照以下要求實現一個`Student`類:
* 寫出三個構造器,其中一個必須是主構造器
* 主構造器中的參數作為屬性
* 寫一個普通函數`show`,要求通過字符串模板輸出類中的屬性
2. 編寫程序,使用今天所講的操作符,找出集合 {21, 40, 11, 33, 78} 中能夠被 3 整除的所有元素,并輸出。
- 前言
- Kotlin簡介
- IntelliJ IDEA技巧總結
- idea設置類注釋和方法注釋模板
- 像Android Studion一樣創建工程
- Gradle
- Gradle入門
- Gradle進階
- 使用Gradle創建一個Kotlin工程
- 環境搭建
- Androidstudio平臺搭建
- Eclipse的Kotlin環境配置
- 使用IntelliJ IDEA
- Kotlin學習路線
- Kotlin官方中文版文檔教程
- 概述
- kotlin用于服務器端開發
- kotlin用于Android開發
- kotlin用于JavaScript開發
- kotlin用于原生開發
- Kotlin 用于數據科學
- 協程
- 多平臺
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 開始
- 基本語法
- 習慣用法
- 編碼規范
- 基礎
- 基本類型
- 包與導入
- 控制流
- 返回與跳轉
- 類與對象
- 類與繼承
- 屬性與字段
- 接口
- 可見性修飾符
- 擴展
- 數據類
- 密封類
- 泛型
- 嵌套類
- 枚舉類
- 對象
- 類型別名
- 內嵌類
- 委托
- 委托屬性
- 函數與Lambda表達式
- 函數
- Lambda表達式
- 內聯函數
- 集合
- 集合概述
- 構造集合
- 迭代器
- 區間與數列
- 序列
- 操作概述
- 轉換
- 過濾
- 加減操作符
- 分組
- 取集合的一部分
- 取單個元素
- 排序
- 聚合操作
- 集合寫操作
- List相關操作
- Set相關操作
- Map相關操作
- 多平臺程序設計
- 平臺相關聲明
- 以Gradle創建
- 更多語言結構
- 解構聲明
- 類型檢測與轉換
- This表達式
- 相等性
- 操作符重載
- 空安全
- 異常
- 注解
- 反射
- 作用域函數
- 類型安全的構造器
- Opt-in Requirements
- 核心庫
- 標準庫
- kotlin.test
- 參考
- 關鍵字與操作符
- 語法
- 編碼風格約定
- Java互操作
- Kotlin中調用Java
- Java中調用Kotlin
- JavaScript
- 動態類型
- kotlin中調用JavaScript
- JavaScript中調用kotlin
- JavaScript模塊
- JavaScript反射
- JavaScript DCE
- 原生
- 并發
- 不可變性
- kotlin庫
- 平臺庫
- 與C語言互操作
- 與Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 調試
- FAQ
- 協程
- 協程指南
- 基礎
- 取消與超時
- 組合掛起函數
- 協程上下文與調度器
- 異步流
- 通道
- 異常處理與監督
- 共享的可變狀態與并發
- Select表達式(實驗性)
- 工具
- 編寫kotlin代碼文檔
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin與OSGI
- 編譯器插件
- 編碼規范
- 演進
- kotlin語言演進
- 不同組件的穩定性
- kotlin1.3的兼容性指南
- 常見問題
- FAQ
- 與Java比較
- 與Scala比較(官方已刪除)
- Google開發者官網簡介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin樣品
- Kotlin零基礎到進階
- 第一階段興趣入門
- kotlin簡介和學習方法
- 數據類型和類型系統
- 入門
- 分類
- val和var
- 二進制基礎
- 基礎
- 基本語法
- 包
- 示例
- 編碼規范
- 代碼注釋
- 異常
- 根類型“Any”
- Any? 可空類型
- 可空性的實現原理
- kotlin.Unit類型
- kotlin.Nothing類型
- 基本數據類型
- 數值類型
- 布爾類型
- 字符型
- 位運算符
- 變量和常量
- 語法和運算符
- 關鍵字
- 硬關鍵字
- 軟關鍵字
- 修飾符關鍵字
- 特殊標識符
- 操作符和特殊符號
- 算術運算符
- 賦值運算符
- 比較運算符
- 邏輯運算符
- this關鍵字
- super關鍵字
- 操作符重載
- 一元操作符
- 二元操作符
- 字符串
- 字符串介紹和屬性
- 字符串常見方法操作
- 字符串模板
- 數組
- 數組介紹創建及遍歷
- 數組常見方法和屬性
- 數組變化以及下標越界問題
- 原生數組類型
- 區間
- 正向區間
- 逆向區間
- 步長
- 類型檢測與類型轉換
- is、!is、as、as-運算符
- 空安全
- 可空類型變量
- 安全調用符
- 非空斷言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函數
- 函數式編程概述
- OOP和FOP
- 函數式編程基本特性
- 組合與范疇
- 在Kotlin中使用函數式編程
- 函數入門
- 函數作用域
- 函數加強
- 命名參數
- 默認參數
- 可變參數
- 表達式函數體
- 頂層、嵌套、中綴函數
- 尾遞歸函數優化
- 函數重載
- 控制流
- if表達式
- when表達式
- for循環
- while循環
- 循環中的 Break 與 continue
- return返回
- 標簽處返回
- 集合
- list集合
- list集合介紹和操作
- list常見方法和屬性
- list集合變化和下標越界
- set集合
- set集合介紹和常見操作
- set集合常見方法和屬性
- set集合變換和下標越界
- map集合
- map集合介紹和常見操作
- map集合常見方法和屬性
- map集合變換
- 集合的函數式API
- map函數
- filter函數
- “ all ”“ any ”“ count ”和“ find ”:對集合應用判斷式
- 別樣的求和方式:sumBy、sum、fold、reduce
- 根據人的性別進行分組:groupBy
- 扁平化——處理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 區間、數組、集合之間轉換
- 面向對象
- 面向對象-封裝
- 類的創建及屬性方法訪問
- 類屬性和字段
- 構造器
- 嵌套類(內部類)
- 枚舉類
- 枚舉類遍歷&枚舉常量常用屬性
- 數據類
- 密封類
- 印章類(密封類)
- 面向對象-繼承
- 類的繼承
- 面向對象-多態
- 抽象類
- 接口
- 接口和抽象類的區別
- 面向對象-深入
- 擴展
- 擴展:為別的類添加方法、屬性
- Android中的擴展應用
- 優化Snackbar
- 用擴展函數封裝Utils
- 解決煩人的findViewById
- 擴展不是萬能的
- 調度方式對擴展函數的影響
- 被濫用的擴展函數
- 委托
- 委托類
- 委托屬性
- Kotlin5大內置委托
- Kotlin-Object關鍵字
- 單例模式
- 匿名類對象
- 伴生對象
- 作用域函數
- let函數
- run函數
- with函數
- apply函數
- also函數
- 標準庫函數
- takeIf 與 takeUnless
- 第二階段重點深入
- Lambda編程
- Lambda成員引用高階函數
- 高階函數
- 內聯函數
- 泛型
- 泛型的分類
- 泛型約束
- 子類和子類型
- 協變與逆變
- 泛型擦除與實化類型
- 泛型類型參數
- 泛型的背后:類型擦除
- Java為什么無法聲明一個泛型數組
- 向后兼容的罪
- 類型擦除的矛盾
- 使用內聯函數獲取泛型
- 打破泛型不變
- 一個支持協變的List
- 一個支持逆變的Comparator
- 協變和逆變
- 第三階段難點突破
- 注解和反射
- 聲明并應用注解
- DSL
- 協程
- 協程簡介
- 協程的基本操作
- 協程取消
- 管道
- 慕課霍丙乾協程筆記
- Kotlin與Java互操作
- 在Kotlin中調用Java
- 在Java中調用Kotlin
- Kotlin與Java中的操作對比
- 第四階段專題練習
- 朱凱Kotlin知識點總結
- Kotlin 基礎
- Kotlin 的變量、函數和類型
- Kotlin 里那些「不是那么寫的」
- Kotlin 里那些「更方便的」
- Kotlin 進階
- Kotlin 的泛型
- Kotlin 的高階函數、匿名函數和 Lambda 表達式
- Kotlin協程
- 初識
- 進階
- 深入
- Kotlin 擴展
- 會寫「18.dp」只是個入門——Kotlin 的擴展函數和擴展屬性(Extension Functions / Properties)
- Kotlin實戰-開發Android