[Kotlin] 一文速通Kotlin
Adrian Chen

Kotlin是那家以做IDE起家的大名鼎鼎的Jetbrains公司開發的一門程式設計語言,旨在Java放棄免費之後,成為Java的替代品。Google在2019年的Google I/O大會上宣告了Kotlin First戰略,使得Kotlin成為Android開發的“御用程式設計語言”。Kotlin對於Java開發人員來講應當會十分熟悉。

函式定義

Kotlin的函式定義有幾種方式。開始之前,先讓我插一個關於函式引數的小tip:變長引數。使用vararg來定義變長引數。使用vararg修飾的引數,可以傳入任意長度的該類型引數。

傳統寫法

1
2
3
fun sum(a: Int, b: Int): Int {
return a + b
}

無回傳值,可指定回傳值類型為Uint,類似於voidUint也可以省略不寫。

簡化寫法

當一個函式不是public函式,且只有一行表達式作為回傳值,那麼可以使用簡化寫法:

1
fun sum(a: Int, b: Int) = a + b

回傳值的類型會自動推斷。如果是public函式的話,則必須指定回傳值類型:

1
public func sum(a: Int, b: Int) : Int = a + b

Lambda

直接看例子,也比較簡單。Lambda講到底就是所謂的匿名函式罷了。

1
2
3
4
val plusFunc: (Int, Int) -> Int = { x, y ->
println("Hello, world.")
x + y
}

這段怎麼理解呢?首先,我定義了一個lambda:

1
2
3
4
{ x, y -> 
println("Hello, world.")
x + y
}

然後將它交付給了常數plusFunc,常數plusFunc的類型是函式類型,具體是(Int, Int) -> Int

樣板字面值

以下的例子會很好理解:

1
2
3
4
5
6
7
var a = 1
val s1 = "a is $a"
// a is 1

a = 2
val s2 = "${s1.replace("is", "was")}, but now is $a"
// a was 1, but now is 2

空安全

類似於Java和Swift中的Optional。以下的例子有對可空值的處理:

1
2
3
4
5
6
7
8
// ?表示可空
var age: String? = "23"
// 丟擲空異常
val ages = age!!.toInt()
// 不做處理,回傳null
val ages1 = age?.toInt()
// 為空時回傳-1
val ages2 = age?.toInt() ?: -1

陣列

陣列用Array實現:

1
2
3
4
var arr: Array<Int> = arrayOf(1, 2, 3)

// 3個元素,執行後面的函式生成,結果是[0, 2, 4]
var arr_2: Array<Int> = Array(3, { i -> { i * 2 } })

條件控制

if-else

和Java最大的一點不同是,if-else是有回傳值的

1
var result = if (a > b) a else b

這可以被方便地用來替代Java中的三元運算子?:

when-else

類似於Java中的switch-case,只是不再需要break

1
2
3
4
5
6
7
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> {
print("x 不是 1 ,也不是 2")
}
}

或者直接進行大規模的判斷:

1
2
3
4
5
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}

迴圈控制

for迴圈

Kotlin中的for迴圈是for-in迴圈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Only value
for(item in 1..4) {
...
}

// Only index
for (i in array.indices) {
...
}

// Value with index
for ((index, value) in array.withIndex()) {
...
}

while迴圈和do-while迴圈

與C家族語言相同。

迴圈控制標籤

之前在用Java的時候,當我們碰到巢狀迴圈,想要從內層迴圈直接break掉外層迴圈,我們需要在內外層各寫一個break。然而在Kotlin中,我們可以透過標籤方便地達成這一點。

1
2
3
4
5
loop@ for (i in 1..100) {
for (j in 1..100) {
if (……) break@loop
}
}

即如果滿足條件,跳出loop@所標示的迴圈。

型別和物件

作為一個物件導向程式設計語言,型別和物件自然是重中之重。

使用class來定義型別。

1
2
3
class MyClass {
...
}

可以使用預設建構子來初始化物件。

1
val myClass = MyClass()

不需要new

建構子

Kotlin中的建構子和Java有非常大的不同。每一個Kotlin型別可以包含有一個主建構子和若干次建構子。

主建構子

主建構子的引數直接與型別名稱放在一起:

1
2
3
class MyClass constructor(name: String) {
...
}

如果主建構子沒有任何修飾,則可以省略constructor

主建構子的函式體放在init{}段中:

1
2
3
4
5
class MyClass(name: String) {
init {
println("The name is: $name.")
}
}

如果希望把主建構子的引數作為型別的成員變數,可以採用簡易寫法:

1
2
3
class MyClass(var name: String) {
...
}

透過給引數添加varval,可將它們變為成員變數。上述寫法等同於:

1
2
3
4
5
6
class MyClass(name: String) {
lateinit var name: String
init {
this.name = name
}
}

次建構子

次建構子在一個型別中可以有若干。使用constructor() {}段來定義次建構子:

1
2
3
4
5
class MyClass {
constructor(name: String) {
...
}
}

注意,如果型別有主建構子,則任何次建構子都需要直接或間接代理主建構子。要實現這一點,可以使用this()

1
2
3
4
5
6
7
8
9
class MyClass(name: String) {
init {
println("The name is: $name.")
}

constructor(name: String, age: Int) : this(name) {
println("The age is: $age.")
}
}

上述次建構子是說:使用this呼叫主建構子,並將次建構子的引數name作為主建構子的第一個引數傳送給主建構子,從而執行主建構子的建構過程。

也就是說,主建構子一旦定義,則必須被呼叫。

型別修飾元

修飾元

  • abstract:抽象型別。只有抽象型別中才可以定義抽象和抽象方法。有了該修飾元即無須open
  • final:型別不可繼承,預設值。
  • enum:枚舉型別。
  • open:型別可繼承。
  • annotation:標註型別。

    權限修飾元

  • public
  • private
  • protected
  • internal

巢狀型別

可以將一個型別放在另一個型別裡面,組成巢狀型別。

1
2
3
4
5
6
7
8
9
10
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}

...

var result = Outer.Nested().foo()

內部型別

內部型別也是將一個型別放到另一個型別中的,但是和巢狀型別不同的是,內部型別可以訪問到外部型別的成員和成員方法。

1
2
3
4
5
6
7
8
9
10
11
class Outer {
private val bar: Int = 1
var v = "成員"
inner class Inner {
fun foo() = bar // 訪問外部型別
fun innerTest() {
var o = this@Outer // 獲取外部型別的引用
println("內部型別可以訪問外部型別的成員:" + o.v)
}
}
}

匿名內部型別

匿名內部型別必須在初始化的時候複寫其中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Test {
var v = "成員"

fun setInterFace(test: TestInterFace) {
test.test()
}
}

/**
* 介面
*/
interface TestInterFace {
fun test()
}

fun main(args: Array<String>) {
var test = Test()

test.setInterFace(object : TestInterFace {
override fun test() {
println("創建匿名內部型別的實例")
}
})
}

Getter和Setter

這裡講的Getter和Setter應該區別於Java中的。在Java中,Getter和Setter是用來將private的成員變數開放給外界來訪問和修改的控制器。在Kotlin中你仍然可以透過這種方法來實現這一點。

我們在這裡講的Getter和Setter是不同的概念。在Kotlin中,型別中所定義的任何變數,都會自動生成一個get()和一個set(),每一個常數,都會自動生成一個get()。所以當我們訪問或修改它們的時候,實際上是透過了一層get()或一層set()來執行的訪問。因此,這兩個函式是可以客製化的。

1
2
3
var lastName: String = "chen"
get() = field.toUpperCase()
set

這樣子,當外界讀取lastName的時候,就會自動讀取到全部大寫的字串。

我們也可以客製化Setter:

1
2
3
4
5
6
7
8
9
var no: Int = 100
get() = field
set(value) {
if (value < 10) {
field = value
} else {
field = -1
}
}

這裡的field代表get到的的值。這個變數只能夠在Getter和Setter中使用,由Kotlin自動產生。

繼承與複寫

物件導向三大內容:封裝、繼承、多型。

Kotlin中的繼承有點子麻煩。首先,任何沒有openabstract修飾的東西都是不可以被繼承的。

1
2
3
4
5
6
7
8
class Animal {
...
}

class Tiger : Animal() {
...
}
// 報錯,無法繼承final的型別

其次,如果基本型別有主建構子,則衍生型別必須在繼承的時候立即實現主建構子。

當衍生類別有主建構子的時候,必須在繼承的時候呼叫基本類別的主建構子:

1
2
3
4
5
6
7
open class Animal(category: String) {
...
}

class Tiger(category: String, location: String) : Animal(category) {
...
}

而如果衍生型別沒有主建構子,則需要在每一個次建構子使用super來呼叫基本型別的主建構子:

1
2
3
4
5
6
7
8
9
open class Animal(category: String) {
...
}

class Tiger: Animal {
constructor(category: String, location: String): super(category) {
...
}
}

複寫

基底型別中帶有open的內容允許被複寫,abstract的抽象內容必須被複寫。或者在多繼承體系中,相同名稱的內容也必須被複寫。

1
2
3
4
5
6
7
8
9
10
11
12
open class Person{
open fun study(){
println("我畢業了")
}
}

class Student : Person() {
override fun study(){
println("我在讀大學")
super.study()
}
}

當呼叫study()函式的時候,呼到的是Student的函式。該函式透過super.study()呼叫了父型別的函式。

的複寫也是一樣的道理。需要注意的是,你可以用一個var複寫一個val,但是無法用一個val複寫一個var。

介面實作

和Java一樣,Kotlin可以實現對多個介面的實作。

介面中的只能是abstract的,被實作後必須複寫,方法則可以有預設的實作。如果方法沒有預設實作,也必須被複寫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface MyInterface {
abstract var prefix: String
fun world()
fun hello() {
println(prefix)
}
}
class Child : MyInterface {
override var prefix: String = "Hello, "
override fun world() {
println("world.")
}
}
fun main(args: Array<String>) {
val c = Child()
c.hello();
c.world();
}

Kotlin擴充函式

擴充函式可以算是Kotlin的一項標誌性的創舉。它可以在一個已經定義好的型別外部——注意喔,是外部——去添加成員函式。但是這個改變並不會影響到型別本身。

這樣子,我們的型別就可以變得更加靈活了。

我們來看一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Jump { 
fun test() {
println("jump test")
}
}

// 擴充函式
fun Jump.doubleJump(howLong: Float): Boolean {
println("jump:$howLong")
println("jump:$howLong")
return true
}

Jump().doubleJump(2f)

範型

和Java一樣,Kotlin也提供了範型支援。下面是宣告一個範型型別:

1
2
3
class Box<T>(t: T) {
var value = t
}

範型上界約束

在Java中,我們會有到對範型的上界約束。在Kotlin中也是一樣的支援。

比如,需要範型實作Comparable介面,可以這樣寫:

1
2
3
class MyClass<T : Comparable<T>>(t: T) {
...
}

資料型別、密封型別、枚舉型別

資料型別

只含有資料的類。

1
data class Person(var name: String, var age: Int)

須滿足以下條件:

  • 主建構子至少包含一個引數。
  • 所有的主建構子的引數必須標識為val或var
  • 不可以宣告為abstract, open, sealed或inner
  • 不能繼承其他型別(但是可以實作介面)

密封型別

給我的感覺就是,這個東西是一個空空的型別,用來代指機種特定的型別。

1
2
3
4
5
6
7
8
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()

fun eval(expr: Expr): Double = when (expr) {
is Const -> expr.number
is Sum -> eval(expr.e1) + eval(expr.e2)
}

枚舉型別

Kotlin中的枚舉型別和其他語言都不是很一樣。具體來講,枚舉型別中的每一個枚舉,都是這個枚舉型別的一個物件

比如:

1
2
3
4
5
enum class RGB(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}

甚至你可以:

1
2
3
4
5
6
7
8
9
10
11
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},

TALKING {
override fun signal() = WAITING
};

abstract fun signal(): ProtocolState
}