天天看點

第2章 Kotlin 文法基礎

第2章 Kotlin 文法基礎

人與人之間通過語言來交流溝通,互相協作。人與計算機之間怎樣“交流溝通”呢?答案是程式設計語言。一門語言有詞、短語、句子、文章等,對應到程式設計語言中就是關鍵字、辨別符、表達式、源代碼檔案等。通常一門程式設計語言的基本構成如下圖所示

第2章 Kotlin 文法基礎

程式設計語言的基本構成

本章我們學習 Kotlin語言的基礎文法。

2.1 變量和辨別符

變量(資料名稱)辨別一個對象的位址,我們稱之為辨別符。而具體存放的資料占用記憶體的大小和存放的形式則由其類型來決定。

在Kotlin中, 所有的變量類型都是引用類型。Kotlin的變量分為 ​

​val​

​​ (不可變的) 和​

​var​

​ (可變的) 。可以簡單了解為:

​val​

​​ 是隻讀的,僅能一次指派,後面就不能被重新指派。

​​

​var​

​ 是可寫的,在它生命周期中可以被多次指派;

使用關鍵字 val 聲明不可變變量

>>> val a:Int = 1
>>> a
1      

另外,我們可以省略後面的類型Int,直接聲明如下

>>> val a = 1 // 根據值 1 編譯器能夠自動推斷出 `Int` 類型
>>> a
1      

用val聲明的變量不能重新指派

>>> val a = 1
>>> a++
error: val cannot be reassigned
a++
^      

使用 var 聲明可變變量

>>> var b = 1
>>> b = b + 1
>>> b
2      

隻要可能,盡量在Kotlin中首選使用​

​val​

​不變值。因為事實上在程式中大部分地方隻需要使用不可變的變量。使用val變量可以帶來可預測的行為和線程安全等優點。

變量名就是辨別符。辨別符是由字母、數字、下劃線組成的字元序列,不能以數字開頭。下面是合法的變量名

>>> val _x = 1
>>> val y = 2
>>> val ip_addr = "127.0.0.1"
>>> _x
1
>>> y
2
>>> ip_addr
127.0.0.1      

跟Java一樣,變量名區分大小寫。命名遵循駝峰式規範。

2.2 關鍵字與修飾符

通常情況下,程式設計語言中都有一些具有特殊意義的辨別符是不能用作變量名的,這些具備特殊意義的辨別符叫做關鍵字(又稱保留字),編譯器需要針對這些關鍵字進行詞法分析,這是編譯器對源碼進行編譯的基礎步驟之一。

Kotlin中的修飾符關鍵字主要分為:

類修飾符、通路修飾符、型變修飾符、成員修飾符、參數修飾符、類型修飾符、函數修飾符、屬性修飾符等。這些修飾符如下表2-1所示

表2-1 Kotlin中的修飾符

類修飾符

類修飾符 說明
abstract 抽象類
final 不可被繼承final類
enum 枚舉類
open 可繼承open類
annotation 注解類
sealed 密封類
data 資料類

成員修飾符

成員修飾符 說明
override 重寫函數(方法)
open 聲明函數可被重寫
final 聲明函數不可被重寫
abstract 聲明函數為抽象函數
lateinit 延遲初始化

通路權限修飾符

通路權限修飾符 說明
private 私有,僅目前類可通路
protected 目前類以及繼承該類的可通路
public 預設值,對外可通路
internal 整個子產品内可通路(子產品是指一起編譯的一組 Kotlin 源代碼檔案。例如,一個 Maven 工程, 或 Gradle 工程,通過 Ant 任務的一次調用編譯的一組檔案等)

協變逆變修飾符

協變逆變修飾符 說明
in 消費者類型修飾符,out T 等價于 ? extends T
out 生産者類型修飾符,in T 等價于 ? super T

函數修飾符

函數修飾符 說明
tailrec 尾遞歸
operator 運算符重載函數
infix 中綴函數。例如,給Int定義擴充中綴函數 infix fun Int.shl(x: Int): Int
inline 内聯函數
external 外部函數
suspend 挂起協程函數

屬性修飾符

屬性修飾符 說明
const 常量修飾符

參數修飾符

參數修飾符 說明
vararg 變長參數修飾符
noinline 不内聯參數修飾符,有時,隻需要将内聯函數的部分參數使用内聯Lambda,其他的參數不需要内聯,可以使用“noinline”關鍵字修飾。例如:​

​inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit)​

crossinline 當内聯函數不是直接在函數體中使用lambda參數,而是通過其他執行上下文。這種情況下可以在參數前使用“crossinline”關鍵字修飾辨別。

代碼執行個體如下。

crossinline代碼執行個體:

inline fun f(crossinline body: () -> Unit) {
    val f = object: Runnable {
        override fun run() = body()
    }
}      
類型修飾符 說明
reified 具體化類型參數

除了上面的修飾符關鍵字之外,還有一些其他特殊語義的關鍵字如下表2-2所示

表2-2 Kotlin中的關鍵字

關鍵字 說明
package 包聲明
as 類型轉換
typealias 類型别名
class 聲明類
this 目前對象引用
super 父類對象引用
val 聲明不可變變量
var 聲明可變變量
fun 聲明函數
for for 循環
null 特殊值 null
true 真值
false 假值
is 類型判斷
throw 抛出異常
return 傳回值
break 跳出循環體
continue 繼續下一次循環
object 單例類聲明
if 邏輯判斷if
else 邏輯判斷, 結合if使用
while while 循環
do do 循環
when 條件判斷
interface 接口聲明
file 檔案
field 成員
property 屬性
receiver 接收者
param 參數
setparam 設定參數
delegate 委托
import 導入包
where where條件
by 委托類或屬性
get get函數
set set 函數
constructor 構造函數
init 初始化代碼塊
try 異常捕獲
catch 異常捕獲,結合try使用
finally 異常最終執行代碼塊
dynamic 動态的
typeof 類型定義,預留用

這些關鍵字定義在源碼 org.jetbrains.kotlin.lexer.KtTokens.java 中。

2.3 流程控制語句

流程控制語句是程式設計語言中的核心之一。可分為:

分支語句(​

​if​

​​ 、 ​

​when​

​​)

循環語句(​​

​for​

​​、​

​while​

​​ )

跳轉語句 (​​

​return​

​​ 、 ​

​break​

​​ 、​

​continue​

​​、​

​throw​

​)

2.3.1 if表達式

if-else語句是控制程式流程的最基本的形式,其中else是可選的。

在 Kotlin 中,if 是一個表達式,即它會傳回一個值(跟Scala一樣)。

代碼示例:

package com.easy.kotlin

fun main(args: Array<String>) {
    println(max(1, 2))
}

fun max(a: Int, b: Int): Int {
    // 表達式傳回值
    val max = if (a > b) a else b
    return max
}      

另外,if 的分支可以是代碼塊,最後的表達式作為該塊的值:

fun max3(a: Int, b: Int): Int {
    val max = if (a > b) {
        print("Max is a")
        a // 最後的表達式作為該代碼塊的值
    } else {
        print("Max is b")
        b // 同上
    }
    return max
}      

if作為代碼塊時,最後一行為其傳回值。

另外,在Kotlin中沒有類似​

​true? 1: 0​

​​這樣的三元表達式。對應的寫法是使用​

​if else​

​語句:

if(true) 1 else 0      

if-else語句規則:

  • if後的括号不能省略,括号裡表達式的值須是布爾型。

代碼反例:

>>> if("a") 1
error: type mismatch: inferred type is String but Boolean was expected
if("a") 1
   ^

>>> if(1) println(1)
error: the integer literal does not conform to the expected type Boolean
if(1)
   ^      
  • 如果條件體内隻有一條語句需要執行,那麼if後面的大括号可以省略。良好的程式設計風格建議加上大括号。
>>> if(true) println(1) else println(0)
1
>>> if(true) { println(1)}  else{ println(0)}
1      

程式設計執行個體:用 if - else 語句判斷某年份是否是閏年。

fun isLeapYear(year: Int): Boolean {
    var isLeapYear: Boolean
    if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
        isLeapYear = true
    } else {
        isLeapYear = false
    }
    return isLeapYear
}

fun main(args: Array<String>) {
    println(isLeapYear(2017)) // false
    println(isLeapYear(2020)) // true
}      

2.3.2 when表達式

when表達式類似于 switch-case 表達式。when會對所有的分支進行檢查直到有一個條件滿足。但相比switch而言,when語句要更加的強大,靈活。

Kotlin的極簡文法表達風格,使得我們對分支檢查的代碼寫起來更加簡單直接:

fun casesWhen(obj: Any?) {
    when (obj) {
        0,1,2,3,4,5,6,7,8,9 -> println("${obj} ===> 這是一個0-9之間的數字")
        "hello" -> println("${obj} ===> 這個是字元串hello")
        is Char -> println("${obj} ===> 這是一個 Char 類型資料")
        else -> println("${obj} ===> else類似于Java中的 case-switch 中的 default")
    }
}

fun main(args: Array<String>) {
    casesWhen(1)
    casesWhen("hello")
    casesWhen('X')
    casesWhen(null)
}      

輸出

1 ===> 這是一個0-9之間的數字
hello ===> 這個是字元串hello
X ===> 這是一個 Char 類型資料
null ===> else類似于Java中的 case-switch 中的 default      

像 if 一樣,when 的每一個分支也可以是一個代碼塊,它的值是塊中最後的表達式的值。

如果其他分支都不滿足條件會到 else 分支(類似default)。

如果我們有很多分支需要用相同的方式處理,則可以把多個分支條件放在一起,用逗号分隔:

0,1,2,3,4,5,6,7,8,9 -> println("${obj} ===> 這是一個0-9之間的數字")      

我們可以用任意表達式(而不隻是常量)作為分支條件

fun switch(x: Int) {
    val s = "123"
    when (x) {
        -1, 0 -> print("x == -1 or x == 0")
        1 -> print("x == 1")
        2 -> print("x == 2")
        8 -> print("x is 8")
        parseInt(s) -> println("x is 123")
        else -> { // 注意這個塊
            print("x is neither 1 nor 2")
        }
    }
}      

我們也可以檢測一個值在 in 或者不在 !in 一個區間或者集合中:

val x = 1
    val validNumbers = arrayOf(1, 2, 3)
    when (x) {
        in 1..10 -> print("x is in the range")
        in validNumbers -> print("x is valid")
        !in 10..20 -> print("x is outside the range")
        else -> print("none of the above")
    }      
程式設計執行個體: 用when語句寫一個階乘函數。
fun fact(n: Int): Int {
    var result = 1
    when (n) {
        0, 1 -> result = 1
        else -> result = n * fact(n - 1)
    }
    return result
}

fact(10) // 3628800      

2.3.3 for循環

for 循環可以對任何提供疊代器(iterator)的對象進行周遊,文法如下:

for (item in collection) {
    print(item)
}      

如果想要通過索引周遊一個數組或者一個 list,可以這麼做:

for (i in array.indices) {
    print(array[i])
}      

或者使用庫函數 ​

​withIndex​

​:

for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}      

另外,範圍(Ranges)表達式也可用于循環當中:

if (i in 1..10) { // 等同于1 <= i && i <= 10
    println(i) 
}      

簡寫

(1..10).forEach { print(it) }      

其中的操作符形式的 1..10 等價于 1.rangeTo(10) 函數調用 ,由in和!in進行連接配接。

程式設計執行個體: 編寫一個 Kotlin 程式在螢幕上輸出1!+2!+3!+……+10!的和。

我們使用上面的fact函數,代碼實作如下

fun sumFact(n: Int): Int {
    var sum = 0
    for (i in 1..n) {
        sum += fact(i)
    }
    return sum
}

sumFact(10) // 4037913      

2.3.4 while循環

while 和 do .. while使用方式跟C、Java語言基本一緻。

代碼示例

package com.easy.kotlin

fun main(args: Array<String>) {
    var x = 10
    while (x > 0) {
        x--
        println(x)
    }

    var y = 10
    do {
        y = y + 1
        println(y)
    } while (y < 20) // y的作用域包含此處
}      

2.3.5 break 和 continue

​break​

​​和​

​continue​

​都是用來控制循環結構的,主要是用來停止循環(中斷跳轉),但是有差別,下面我們分别介紹。

break

​break​

​用于完全結束一個循環,直接跳出循環體,然後執行循環後面的語句。

問題場景:

列印數字1-10,隻要遇到偶數就結束列印。

代碼示例:

for (i in 1..10) {
        println(i)
        if (i % 2 == 0) {
            break
        }
    } // break to here      

輸出:

1
2      

continue

​continue​

​​是隻終止本輪循環,但是還會繼續下一輪循環。可以簡單了解為,直接在目前語句處中斷,跳轉到循環入口,執行下一輪循環。而​

​break​

​則是完全終止循環,跳轉到循環出口。

問題場景:

列印1-10中的奇數。

代碼示例:

for (i in 1..10) {
        if (i % 2 == 0) {
            continue
        }
        println(i)
    }      

輸出

1
3
5
7
9      

2.3.6 return傳回

在Java、C語言中,return語句使我們再常見不過的了。雖然在Scala,Groovy這樣的語言中,函數的傳回值可以不需要顯示用return來指定,但是我們仍然認為,使用return的編碼風格更加容易閱讀了解 (尤其是在分支流代碼塊中)。

在Kotlin中,除了表達式的值,有傳回值的函數都要求顯式使用​

​return​

​來傳回其值。

代碼示例

fun sum(a: Int,b: Int): Int{
    return a+b // 這裡的return不能省略
}

fun max(a: Int, b: Int): Int {
 if (a > b){
 return a // return不能省略
} else{
 return b // return不能省略
}      

我們在Kotlin中,可以直接使用​

​=​

​符号來直接傳回一個函數的值,這樣的函數我們稱為函數字面量。

代碼示例

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

>>> sum(1,10)
11

>>> max(1,2)
2

>>> val sum=fun(a:Int, b:Int) = a+b
>>> sum
(kotlin.Int, kotlin.Int) -> kotlin.Int
>>> sum(1,1)
2      

後面的函數體語句有沒有大括号 ​

​{}​

​ 意思完全不同。加了大括号,意義就完全不一樣了。

>>> val sumf = fun(a:Int, b:Int) = {a+b}
>>> sumf
(kotlin.Int, kotlin.Int) -> () -> kotlin.Int
>>> sumf(1,1)
() -> kotlin.Int
>>> sumf(1,1).invoke()
2      

我們再通過下面的代碼示例清晰的看出:

>>> fun sumf(a:Int,b:Int) = {a+b}
>>> sumf(1,1)
() -> kotlin.Int
>>> sumf(1,1).invoke()
2
>>> fun maxf(a:Int, b:Int) = {if(a>b) a else b}
>>> maxf(1,2)
() -> kotlin.Int
>>> maxf(1,2).invoke()
2      

可以看出,​

​sumf​

​​,​

​maxf​

​的傳回值是函數類型:

() -> kotlin.Int
() -> kotlin.Int      

這點跟Scala是不同的。在Scala中,帶不帶大括号​

​{}​

​,意思一樣:

scala> def maxf(x:Int, y:Int) = { if(x>y) x else y }
maxf: (x: Int, y: Int)Int

scala> def maxv(x:Int, y:Int) = if(x>y) x else y
maxv: (x: Int, y: Int)Int

scala> maxf(1,2)
res4: Int = 2

scala> maxv(1,2)
res6: Int = 2      

我們可以看出​

​maxf: (x: Int, y: Int)Int​

​​跟​

​maxv: (x: Int, y: Int)Int​

​簽名是一樣的。在這裡,Kotlin跟Scala在大括号的使用上,是完全不同的。

然後,調用函數方式是直接調用​

​invoke()​

​函數:sumf(1,1).invoke()。

kotlin 中 ​

​return​

​ 語句會從最近的函數或匿名函數中傳回,但是在Lambda表達式中遇到return,則直接傳回最近的外層函數。例如下面兩個函數是不同的:

val intArray = intArrayOf(1, 2, 3, 4, 5)
    intArray.forEach {
        if (it == 3) return // 在Lambda表達式中的return 直接傳回最近的外層函數
        println(it)
    }      

輸出:

1
2      

遇到 3 時會直接傳回(有點類似循環體中的​

​break​

​行為)。

而我們給forEach傳入一個匿名函數 fun(a: Int) ,這個匿名函數裡面的return不會跳出forEach循環,有點像continue的邏輯:

val intArray = intArrayOf(1, 2, 3, 4, 5)
    intArray.forEach(fun(a: Int) { 
        if (a == 3) return // 從最近的函數中傳回
        println(a)
    })      

輸出

1
2
4
5      

為了顯式的指明 ​

​return​

​​ 傳回的位址,kotlin 還提供了 ​

​@Label​

​ (标簽) 來控制傳回語句,且看下節分解。

2.3.7 标簽(label)

在 Kotlin 中任何表達式都可以用标簽(label)來标記。 标簽的格式為辨別符後跟 ​

​@​

​​ 符号,例如:​

​abc@​

​​、​

​_isOK@​

​​ 都是有效的标簽。我們可以用Label标簽來控制 ​

​return​

​​、​

​break​

​​ 或 ​

​continue​

​的跳轉(jump)行為。

代碼示例:

val intArray = intArrayOf(1, 2, 3, 4, 5)
    intArray.forEach here@ {
        if (it == 3) return@here // 指令跳轉到 lambda 表達式标簽 here@ 處。繼續下一個it=4的周遊循環
        println(it)
    }      

輸出:

1
2
4
5      

我們在 lambda 表達式開頭處添加了标簽​

​here@​

​​ ,我們可以這麼了解:該标簽相當于是記錄了Lambda表達式的指令執行入口位址, 然後在表達式内部我們使用​

​return@here​

​ 來跳轉至Lambda表達式該位址處。這樣代碼更加易懂。

另外,我們也可以使用隐式标簽更友善。 該标簽與接收該 lambda 的函數同名。

代碼示例

val intArray = intArrayOf(1, 2, 3, 4, 5)
    intArray.forEach {
        if (it == 3) return@forEach // 傳回到 @forEach 處繼續下一個循環
        println(it)
    }      

輸出:

1
2
4
5      

接收該Lambda表達式的函數是forEach, 是以我們可以直接使用 ​

​return@forEach​

​ ,來跳轉到此處執行下一輪循環。

2.3.8 throw表達式

在 Kotlin 中 throw 是表達式,它的類型是特殊類型 Nothing。 該類型沒有值。跟C、Java中的​

​void​

​ 意思一樣。

>>> Nothing::class
class java.lang.Void      

我們在代碼中,用 Nothing 來标記無傳回的函數:

>>> fun fail(msg:String):Nothing{ throw IllegalArgumentException(msg) }
>>> fail("XXXX")
java.lang.IllegalArgumentException: XXXX
    at Line57.fail(Unknown Source)      

另外,如果把一個throw表達式的值指派給一個變量,需要顯式聲明類型為​

​Nothing​

​ , 代碼示例如下

>>> val ex = throw Exception("YYYYYYYY")
error: 'Nothing' property type needs to be specified explicitly
val ex = throw Exception("YYYYYYYY")
    ^

>>> val ex:Nothing = throw Exception("YYYYYYYY")
java.lang.Exception: YYYYYYYY      

另外,因為ex變量是Nothing類型,沒有任何值,是以無法當做參數傳給函數。

2.4 操作符與重載

Kotlin 允許我們為自己的類型提供預定義的一組操作符的實作。這些操作符具有固定的符号表示(如 ​

​+​

​​ 或 ​

​*​

​)和固定的優先級。這些操作符的符号定義如下:

KtSingleValueToken LBRACKET    = new KtSingleValueToken("LBRACKET", "[");
    KtSingleValueToken RBRACKET    = new KtSingleValueToken("RBRACKET", "]");
    KtSingleValueToken LBRACE      = new KtSingleValueToken("LBRACE", "{");
    KtSingleValueToken RBRACE      = new KtSingleValueToken("RBRACE", "}");
    KtSingleValueToken LPAR        = new KtSingleValueToken("LPAR", "(");
    KtSingleValueToken RPAR        = new KtSingleValueToken("RPAR", ")");
    KtSingleValueToken DOT         = new KtSingleValueToken("DOT", ".");
    KtSingleValueToken PLUSPLUS    = new KtSingleValueToken("PLUSPLUS", "++");
    KtSingleValueToken MINUSMINUS  = new KtSingleValueToken("MINUSMINUS", "--");
    KtSingleValueToken MUL         = new KtSingleValueToken("MUL", "*");
    KtSingleValueToken PLUS        = new KtSingleValueToken("PLUS", "+");
    KtSingleValueToken MINUS       = new KtSingleValueToken("MINUS", "-");
    KtSingleValueToken EXCL        = new KtSingleValueToken("EXCL", "!");
    KtSingleValueToken DIV         = new KtSingleValueToken("DIV", "/");
    KtSingleValueToken PERC        = new KtSingleValueToken("PERC", "%");
    KtSingleValueToken LT          = new KtSingleValueToken("LT", "<");
    KtSingleValueToken GT          = new KtSingleValueToken("GT", ">");
    KtSingleValueToken LTEQ        = new KtSingleValueToken("LTEQ", "<=");
    KtSingleValueToken GTEQ        = new KtSingleValueToken("GTEQ", ">=");
    KtSingleValueToken EQEQEQ      = new KtSingleValueToken("EQEQEQ", "===");
    KtSingleValueToken ARROW       = new KtSingleValueToken("ARROW", "->");
    KtSingleValueToken DOUBLE_ARROW       = new KtSingleValueToken("DOUBLE_ARROW", "=>");
    KtSingleValueToken EXCLEQEQEQ  = new KtSingleValueToken("EXCLEQEQEQ", "!==");
    KtSingleValueToken EQEQ        = new KtSingleValueToken("EQEQ", "==");
    KtSingleValueToken EXCLEQ      = new KtSingleValueToken("EXCLEQ", "!=");
    KtSingleValueToken EXCLEXCL    = new KtSingleValueToken("EXCLEXCL", "!!");
    KtSingleValueToken ANDAND      = new KtSingleValueToken("ANDAND", "&&");
    KtSingleValueToken OROR        = new KtSingleValueToken("OROR", "||");
    KtSingleValueToken SAFE_ACCESS = new KtSingleValueToken("SAFE_ACCESS", "?.");
    KtSingleValueToken ELVIS       = new KtSingleValueToken("ELVIS", "?:");
    KtSingleValueToken QUEST       = new KtSingleValueToken("QUEST", "?");
    KtSingleValueToken COLONCOLON  = new KtSingleValueToken("COLONCOLON", "::");
    KtSingleValueToken COLON       = new KtSingleValueToken("COLON", ":");
    KtSingleValueToken SEMICOLON   = new KtSingleValueToken("SEMICOLON", ";");
    KtSingleValueToken DOUBLE_SEMICOLON   = new KtSingleValueToken("DOUBLE_SEMICOLON", ";;");
    KtSingleValueToken RANGE       = new KtSingleValueToken("RANGE", "..");
    KtSingleValueToken EQ          = new KtSingleValueToken("EQ", "=");
    KtSingleValueToken MULTEQ      = new KtSingleValueToken("MULTEQ", "*=");
    KtSingleValueToken DIVEQ       = new KtSingleValueToken("DIVEQ", "/=");
    KtSingleValueToken PERCEQ      = new KtSingleValueToken("PERCEQ", "%=");
    KtSingleValueToken PLUSEQ      = new KtSingleValueToken("PLUSEQ", "+=");
    KtSingleValueToken MINUSEQ     = new KtSingleValueToken("MINUSEQ", "-=");
    KtKeywordToken NOT_IN      = KtKeywordToken.keyword("NOT_IN", "!in");
    KtKeywordToken NOT_IS      = KtKeywordToken.keyword("NOT_IS", "!is");
    KtSingleValueToken HASH        = new KtSingleValueToken("HASH", "#");
    KtSingleValueToken AT          = new KtSingleValueToken("AT", "@");

    KtSingleValueToken COMMA       = new KtSingleValueToken("COMMA", ",");      

2.4.1 操作符優先級

Kotlin中操作符的優先級(Precedence)如下表所示

表2-3 操作符的優先級

優先級 标題 符号
最高 字尾(Postfix )

​++​

​​, ​

​--​

​​, ​

​.​

​​, ​

​?.​

​​, ​

​?​

字首(Prefix)

​-​

​​, ​

​+​

​​, ​

​++​

​​, ​

​--​

​​, ​

​!​

​​, ​​

​labelDefinition​

​​​

​@​

右手類型運算(Type RHS,right-hand side class type (RHS) )

​:​

​​, ​

​as​

​​, ​

​as?​

乘除取餘(Multiplicative)

​*​

​​, ​

​/​

​​, ​

​%​

加減(Additive )

​+​

​​, ​

​-​

區間範圍(Range)

​..​

Infix函數 例如,給​

​Int​

​​定義擴充 ​

​infix fun Int.shl(x: Int): Int {...}​

​​,這樣調用 ​

​1 shl 2​

​​,等同于​

​1.shl(2)​

Elvis操作符

​?:​

命名檢查符(Named checks)

​in​

​​, ​

​!in​

​​, ​

​is​

​​, ​

​!is​

比較大小(Comparison)

​<​

​​, ​

​>​

​​, ​

​<=​

​​, ​

​>=​

相等性判斷(Equality)

​==​

​​, ​

​!=​

​​, ​

​===​

​​, ​

​!==​

與 (Conjunction)

​&&​

或 (Disjunction)

​||​

最低 指派(Assignment)

​=​

​​, ​

​+=​

​​, ​

​-=​

​​, ​

​*=​

​​, ​

​/=​

​​, ​

​%=​

為實作這些的操作符,Kotlin為二進制操作符左側的類型和一進制操作符的參數類型,提供了相應的函數或擴充函數。重載操作符的函數需要用 ​

​operator​

​​ 修飾符标記。中綴操作符的函數使用​

​infix​

​修飾符标記。

2.4.2 一進制操作符

一進制操作符(unary operation) 有字首操作符、遞增和遞減操作符等。

字首操作符

字首操作符放在操作數的前面。它們分别如表2-4所示

表2-4 字首操作符

表達式 翻譯為

​+a​

​a.unaryPlus()​

​-a​

​a.unaryMinus()​

​!a​

​a.not()​

以下是重載一進制減運算符的示例:

package com.easy.kotlin

data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus() = Point(-x, -y)      

測試代碼:

package com.easy.kotlin

import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class OperatorDemoTest {

    @Test
    fun testPointUnaryMinus() {
        val p = Point(1, 1)
        val np = -p
        println(np) //Point(x=-1, y=-1)
    }
}      

遞增和遞減操作符

表2-5 遞增和遞減操作符

表達式 翻譯為

​a++​

​a.inc()​

​​ 傳回值是​

​a​

​a--​

​a.dec()​

​​ 傳回值是​

​a​

​++a​

​a.inc()​

​​ 傳回值是​

​a+1​

​--a​

​a.dec()​

​​ 傳回值是​

​a-1​

​inc()​

​​ 和 ​

​dec()​

​​ 函數必須傳回一個值,它用于指派給使用

​​

​++​

​​ 或 ​

​--​

​ 操作的變量。

2.4.3 二進制操作符

Kotlin中的二進制操作符有算術運算符、索引通路操作符、調用操作符、計算并指派操作符、相等與不等操作符、Elvis 操作符、比較操作符、中綴操作符等。下面我們分别作介紹。

算術運算符

表2-6 算術運算符

表達式 翻譯為

​a + b​

​a.plus(b)​

​a - b​

​a.minus(b)​

​a * b​

​a.times(b)​

​a / b​

​a.div(b)​

​a % b​

​a.rem(b)​

​​、 ​

​a.mod(b)​

​a..b​

​a.rangeTo(b)​

代碼示例

>>> val a=10
>>> val b=3
>>> a+b
13
>>> a-b
7
>>> a/b
3
>>> a%b
1
>>> a..b
10..3
>>> b..a
3..10      

字元串的​

​+​

​運算符重載

先用代碼舉個例子:

>>> ""+1
1
>>> 1+""
error: none of the following functions can be called with the arguments supplied: 
public final operator fun plus(other: Byte): Int defined in kotlin.Int
public final operator fun plus(other: Double): Double defined in kotlin.Int
public final operator fun plus(other: Float): Float defined in kotlin.Int
public final operator fun plus(other: Int): Int defined in kotlin.Int
public final operator fun plus(other: Long): Long defined in kotlin.Int
public final operator fun plus(other: Short): Int defined in kotlin.Int
1+""
 ^      

從上面的示例,我們可以看出,在Kotlin中​

​1+""​

​​是不允許的(這地方,相比Scala,寫這樣的Kotlin代碼就顯得不大友好),隻能顯式調用​

​toString​

​來相加:

>>> 1.toString()+""
1      

自定義重載的 ​

​+​

​ 運算符

下面我們使用一個計數類 Counter 重載的 ​

​+​

​ 運算符來增加index的計數值。

代碼示例

data class Counter(var index: Int)

operator fun Counter.plus(increment: Int): Counter {
    return Counter(index + increment)
}      

測試類

package com.easy.kotlin

import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class OperatorDemoTest 
    @Test
    fun testCounterIndexPlus() {
        val c = Counter(1)
        val cplus = c + 10
        println(cplus) //Counter(index=11)
    }
}      

​in​

​操作符

表2-7 in操作符

表達式 翻譯為

​a in b​

​b.contains(a)​

​a !in b​

​!b.contains(a)​

in操作符等價于函數contains 。

索引通路操作符

表2-8 索引通路操作符操作符

表達式 翻譯為

​a[i]​

​a.get(i)​

​a[i] = b​

​a.set(i, b)​

方括号轉換為調用帶有适當數量參數的 ​

​get​

​​ 和 ​

​set​

​。

調用操作符

表2-9 調用操作符

表達式 翻譯為

​a()​

​a.invoke()​

​a(i)​

​a.invoke(i)​

圓括号轉換為調用帶有适當數量參數的 ​

​invoke​

​。

計算并指派操作符

表2-10 計算并指派操作符

表達式 翻譯為

​a += b​

​a.plusAssign(b)​

​a -= b​

​a.minusAssign(b)​

​a *= b​

​a.timesAssign(b)​

​a /= b​

​a.divAssign(b)​

​a %= b​

​a.modAssign(b)​

對于指派操作,例如 ​

​a += b​

​​,編譯器會試着生成 ​

​a = a + b​

​​ 的代碼(這裡包含類型檢查:​

​a + b​

​​ 的類型必須是 ​

​a​

​ 的子類型)。

相等與不等操作符

Kotlin 中有兩種類型的相等性:

  • 引用相等​

    ​===​

    ​​

    ​!==​

    ​(兩個引用指向同一對象)
  • 結構相等​

    ​==​

    ​​

    ​!=​

    ​​( 使用​

    ​equals()​

    ​ 判斷)

表2-11 相等與不等操作符

表達式 翻譯為

​a == b​

​a?.equals(b) ?: (b === null)​

​a != b​

​!(a?.equals(b) ?: (b === null))​

這個 ​

​==​

​​ 操作符有些特殊:它被翻譯成一個複雜的表達式,用于篩選 ​

​null​

​ 值。

意思是:如果 a 不是 null 則調用 ​

​equals(Any?)​

​​ 函數并傳回其值;否則(即 ​

​a === null​

​​)就計算 ​

​b === null​

​ 的值并傳回。

當與 null 顯式比較時,​

​a == null​

​​ 會被自動轉換為 ​

​a=== null​

注意:​

​===​

​​ 和 ​

​!==​

​不可重載。

Elvis 操作符 ​

​?:​

在Kotin中,Elvis操作符特定是跟null比較。也就是說

y = x?:0      

等價于

val y = if(x!==null) x else 0      

主要用來作​

​null​

​安全性檢查。

Elvis操作符 ​

​?:​

​ 是一個二進制運算符,如果第一個操作數為真,則傳回第一個操作數,否則将計算并傳回其第二個操作數。它是三元條件運算符的變體。命名靈感來自貓王的發型風格。

Kotlin中沒有這樣的三元運算符 ​

​true?1:0​

​​,取而代之的是​

​if(true) 1 else 0​

​。而Elvis操作符算是精簡版的三元運算符。

我們在Java中使用的三元運算符的文法,你通常要重複變量兩次, 示例:

String name = "Elvis Presley";
String displayName = (name != null) ? name : "Unknown";      

取而代之,你可以使用Elvis操作符

String name = "Elvis Presley";
String displayName = name?:"Unknown"      

我們可以看出,用Elvis操作符(?:)可以把帶有預設值的if/else結構寫的及其短小。用Elvis操作符不用檢查null(避免了​

​NullPointerException​

​),也不用重複變量。

這個Elvis操作符功能在Spring 表達式語言 (SpEL)中提供。

在Kotlin中當然就沒有理由不支援這個特性。

代碼示例:

>>> val x = null
>>> val y = x?:0
>>> y
0
>>> val x = false
>>> val y = x?:0
>>> y
false
>>> val x = ""
>>> val y = x?:0
>>> y

>>> val x = "abc"
>>> val y = x?:0
>>> y
abc      

比較操作符

表2-12 比較操作符

表達式 翻譯為

​a > b​

​a.compareTo(b) > 0​

​a < b​

​a.compareTo(b) < 0​

​a >= b​

​a.compareTo(b) >= 0​

​a <= b​

​a.compareTo(b) <= 0​

所有的比較都轉換為對 ​

​compareTo​

​​ 的調用,這個函數需要傳回 ​

​Int​

​ 值

用infix函數自定義中綴操作符

我們可以通過自定義infix函數來實作中綴操作符。

代碼示例

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

infix fun Person.grow(years: Int): Person {
    return Person(name, age + years)
}      

測試代碼

package com.easy.kotlin

import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class InfixFunctionDemoTest {

    @Test fun testInfixFuntion() {
        val person = Person("Jack", 20)
        println(person.grow(2))
        println(person grow 2)
    }
}      

輸出

Person(name=Jack, age=22)
Person(name=Jack, age=22)      

2.5 包聲明

我們在​

​*.kt​

​​源檔案開頭聲明​

​package​

​命名空間。例如在PackageDemo.kt源代碼中,我們按照如下方式聲明包

package com.easy.kotlin

fun what(){ // 包級函數
    println("This is WHAT ?")
}

fun main(args:Array<String>){ // 一個包下面隻能有一個main函數
    println("Hello,World!")
}

class Motorbike{ // 包裡面的類
    fun drive(){
        println("Drive The Motorbike ...")
    }
}      

Kotlin中的目錄與包的結構無需比對,源代碼檔案可以在檔案系統中的任意位置。

如果一個測試類PackageDemoTest跟PackageDemo在同一個包下面,我們就不需要單獨去import 類和包級函數,可以在代碼裡直接調用

package com.easy.kotlin

import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class PackageDemoTest {

    @Test
    fun testWhat() {
        what()
    }

    @Test
    fun testDriveMotorbike(){
        val motorbike = Motorbike()
        motorbike.drive()
    }
}      

其中,​

​what()​

​​ 函數跟​

​PackageDemoTest​

​​類在同一個包命名空間下,可以直接調用,不需要 ​

​import​

​​。​

​Motorbike​

​​類跟​

​PackageDemoTest​

​類同理分析。

如果不在同一個package下面,我們就需要import對應的類和函數。例如,我們在 ​

​src/test/kotlin​

​​目錄下建立一個​

​package com.easy.kotlin.test​

​​, 使用​

​package com.easy.kotlin​

​ 下面的類和函數,示例如下

package com.easy.kotlin.test

import com.easy.kotlin.Motorbike // 導入類Motorbike
import com.easy.kotlin.what // 導入包級函數what
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class PackageDemoTest {

    @Test
    fun testWhat() {
        what()
    }

    @Test
    fun testDriveMotorbike() {
        val motorbike = Motorbike()
        motorbike.drive()
    }

}      

Kotlin會會預設導入一些基礎包到每個 Kotlin 檔案中:

kotlin.*
kotlin.annotation.*
kotlin.collections.*
kotlin.comparisons.* (自 1.1 起)
kotlin.io.*
kotlin.ranges.*
kotlin.sequences.*
kotlin.text.*      

根據目标平台還會導入額外的包:

JVM:

java.lang.*
kotlin.jvm.*      
kotlin.js.*      

本章小結

繼續閱讀