Kotlin入門

0. 前言

前面的 Kotlin 的文章《從Java角度深刻理解Kotlin》雖然已經對 Kotlin 相關的核心概念有了全面的介紹,可是不少基礎入門的知識沒有說起。html

這對 Kotlin 新手來講仍是不是很好理解,因此特意寫了這篇《Kotlin從入門到進階》java

寫這篇文章主要有一下幾個目的:git

  1. 對於 Kotlin 新手來講理解這篇《從Java角度深刻理解Kotlin》仍是有些吃力,畢竟還有許多基礎知識點沒有介紹
  2. 對本身來講也有一個查缺補漏的做用

可是出於篇幅的緣由,仍是有不少知識沒有介紹到,對其餘知識點有興趣的能夠自行查閱github

1. Kotlin 基礎知識

1.1 Kotlin 函數和變量的定義

函數和變量這兩個概念是 Kotlin 中最基本的兩個元素,在介紹其餘概念以前,先介紹下這兩個基本概念正則表達式

下面咱們來定義一個函數:express

fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}
複製代碼

對上面的函數作個解釋:編程

  • fun 關鍵字用來定義一個函數
  • fun 關鍵字後面是函數名(max)
  • 括號中間是函數參數
  • 冒號後面是返回值類型
  • 語句能夠不用分號結尾

以下圖所示:數組

Kotlin 函數定義

須要注意的是 Kotlin 中沒有像 Java 中的 三元運算符安全

在 Java 中上面的 函數體 能夠改爲這樣:bash

return (a > b) ? a : b
複製代碼

Kotlin 使用 if 語句來代替 三目運算符

1.2 表達式和語句

咱們在學習任何編程語言的時候,都會遇到兩個概念:

  • 表達式(expressions)
  • 語句(statements)

可能有些開發者還搞不清什麼是 表達式 ,什麼是 語句

在不一樣的編程語言中對 表達式和語句的定義 可能會有一些細微的差異

1.2.1 Java 中表達式和語句

在 Java 中 一個 表達式 是由 變量操做符方法 調用組成, 用來獲得某種類型的返回值

好比下面的 Java 官方文檔的代碼示例:

// cadence = 0 是表達式
int cadence = 0;

// anArray[0] = 100 是表達式
anArray[0] = 100;

// "Element 1 at index 0: " + anArray[0] 是表達式
System.out.println("Element 1 at index 0: " + anArray[0]);

// result = 1 + 2 是表達式
int result = 1 + 2; // result is now 3

// value1 == value2 是表達式
if (value1 == value2) 
    //"value1 == value2" 是表達式
    System.out.println("value1 == value2");

複製代碼

咱們從中能夠看出 表達式 會返回某種類型的值

Java 中的 語句 和人類天然語言的句子差很少,一個 Java 語句 造成一個完整的執行單元,語句以分號(;)結尾

有的表達式在末尾加上分號就變成語句了,以下面幾種類型的表達式:

  • 賦值表達式
  • 任何使用了 ++ 或 -- 的表達式
  • 方法調用
  • 建立對象表達式

如:

// 賦值語句
aValue = 8933.234

// 自增語句
aValue++;

// 方法調用語句
System.out.println("Hello World!");

// 建立對象語句
Bicycle myBike = new Bicycle();
複製代碼

除此以外,還有 聲明語句(declaration statements),如:

// declaration statement
double aValue = 8933.234;
複製代碼

還有 控制流語句(control flow statements),它包括:

  • 選擇語句 decision-making statements (if-then, if-then-else, switch)
  • 循環語句 looping statements (for, while, do-while)
  • 分支語句 branching statements (break, continue, return)

1.2.2 Kotlin 中表達式和語句

Kotlin 和 Java 中對錶達式和語句的定義都是相似的

可是對於有些關鍵字是語句仍是表達式和 Java 仍是有些區別的

  1. if/when

如上所述,在 Java 中全部的 控制流 都是語句

在 Kotlin 的控制流中除了 循環(for/while/do..while) ,其餘的都是表達式

既然是表達式,那麼它就是表示某種類型的數據,能夠把它賦值給變量

val max = if (a > b) a else b
複製代碼
  1. try

在 Java 中 try 異常處理是語句

在 Kotlin 中它是表達式:

fun readNumber(reader: BufferedReader) { 
    //將 try 賦值給 number 變量
    val number = try {
        Integer.parseInt(reader.readLine()) 
    } catch (e: NumberFormatException){ 
        return 
    }
    println(number) 
}
複製代碼

3 表達式體

上面的 max 函數,由於函數體只有一個表達式,咱們能夠改寫成以下形式:

fun max (a:Int, b:Int) = if (a > b) a else b
複製代碼

能夠看出咱們把一個表達式賦值給一個函數,表達式的返回值就是函數的返回值

若是一個函數的函數體放在花括號({})中,咱們說該函數有一個 區塊體(block body)

若是一個函數直接返回一個表達式,咱們說該函數有一個 表達式體(expression body)

爲何上面的 max 函數能夠省略 return 關鍵字呢?

實際上任何一個變量和表達式都有一個類型;

Kotlin 每一個函數都會有返回類型,這個後面介紹的函數的時候回繼續講解

表達式的類型,Kotlin 會經過 類型推導(type inference) 來得知該表達式的類型

而後把獲得的類型當作函數的返回值類型

1.2.3 變量的定義

Kotlin 中對變量的定義和 Java 不同

在 Java 中一般以變量的類型開頭,後面跟着變量名稱

Kotlin 定義變量的語法爲: var/val name:Type

  • var 關鍵字是 variable 的簡稱,表示該變量能夠被修改
  • val 關鍵字是 value 的簡稱,表示該變量一旦賦值後不能被修改
// 定義一個能夠被修改的變量
var age : Int = 17
// 定義一個不可修改的變量
val id : Int= "1000"

// 還能夠省略變量類型
// Kotlin會類型推導出變量的類型
var age = 17
val id = "1000"
複製代碼

須要注意的是,val 表示該變量 引用不可變,可是對象裏的內容能夠變

1.2 Kotlin 類、枚舉和屬性

Kotlin 類的定義能夠參考以前的文章:《從Java角度深刻理解Kotlin》

在 Java 中使用 enum 關鍵定義枚舉類

Kotlin 使用 enume class 來定義枚舉類,如:

enum class Color(val r: Int, val g: Int, val b: Int ){ //枚舉常量屬性

    // 定義枚舉常量對象
    RED(255, 0, 0), ORANGE(255, 165, 0),
    YELLOW(255, 255, 0), GREEN(0, 255, 0), 
    BLUE(0, 0, 255), INDIGO(75, 0, 130), 
    VIOLET(238, 130, 238); //最後一個枚舉對象須要分號結尾
    
    // 在枚舉類中定義函數
    fun rgb() = (r * 256 + g) * 256 + b
}
複製代碼

關於類的屬性,在介紹如何建立類的時候已經有過詳細的講解,這裏再作一些補充

如何自定義類屬性的訪問?

咱們知道經過 val 關鍵聲明的公有屬性,只會生成它對應的 getter 函數

若是咱們須要在這個 getter 函數裏添加邏輯怎麼作呢?以下所示:

class Rectangle(val height: Int, val width: Int) { 

    val isSquare: Boolean
        get() {// 自定義 getter 方法
            return height == width 
        }
}
複製代碼

1.3 when、if 和循環語句

13.1. when

在 Java 中有 switch 語句,在 Kotlin 中使用 when 來代替 switch

  1. when 的基本語法
when(parameter){
    branch1 -> logic
    branch2 -> logic
}
複製代碼

when 括號裏是參數,參數是可選的。箭頭(->) 左邊是條件分支,右邊是對應的邏輯體

when 不須要向 switch 那樣須要加上 break 語句,符合條件自動具備 break 功能

若是邏輯體代碼比較多,能夠放到花括號({})裏:

when(parameter){
    branch1 -> {
        //...
    }
    branch1 -> {
        //...
    }
}
複製代碼

若是要組合多個分支,可使用逗號(,)分隔分支:

when(parameter){
    branch1,branch1 -> {
        //...
    }
}
複製代碼
  1. 枚舉類對象做爲 when 參數
fun getMnemonic(color: Color) = when (color) {
     Color.RED -> "Richard" 
     Color.ORANGE -> "Of" 
     Color.YELLOW -> "York" 
     Color.GREEN -> "Gave" 
     Color.BLUE -> "Battle" 
     Color.INDIGO -> "In" 
     Color.VIOLET -> "Vain"
}

複製代碼

須要注意的是,when 使用枚舉對象做爲參數,須要把該枚舉類的全部對象列舉完

因此 枚舉對象做爲 when 參數不須要 else 分支

  1. 任意對象做爲 when 參數

Kotlin 中的 when 比 Java 中的 switch 功能更強大

Java 的 switch 參數只能是 枚舉常量、字符串、整型或整型的包裝類型(浮點型不能夠)

Kotlin 的 when 能夠是任意對象:

fun mix(c1: Color, c2: Color) = when (setOf(c1, c2)) {
    
    setOf(RED, YELLOW) -> ORANGE 
    
    setOf(YELLOW, BLUE) -> GREEN 
    
    setOf(BLUE, VIOLET) -> INDIGO
    
    //須要處理 其餘 狀況
    else -> throw Exception("Dirty color") 
}
複製代碼
  1. 無參數的 when 表達式

上面的 mix 函數比較低效,由於每次比較的時候都會建立一個或多個 set 集合

若是該函數調用頻繁,會建立不少臨時對象

可使用無參的 when 表達式來改造下:

fun mixOptimized(c1: Color, c2: Color) = when {
    (c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) ->
        ORANGE
    (c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) ->
        GREEN
    (c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) ->
        INDIGO
    else -> throw Exception("Dirty color")
}
複製代碼

無參數的 when 表達式的條件分支必須是 boolean 類型

  1. 智能類型轉換(smart casts)

在 Java 中對某個對象進行類型轉換的時候時候,須要經過 instanceof 來判斷是否能夠被強轉

void test(Object obj) {
    if (obj instanceof String) {
        String str = (String) obj;
        str.substring(0, str.length() / 2);
    }
    //...
}

複製代碼

Kotlin 經過 is 關鍵字來判斷類型,而且編譯器會自動幫你作類型轉換

fun test(obj: Any) {
    if (obj is String) {
        // 不須要手動作類型轉換操做
        obj.substring(0, obj.length / 2)
    }
    //...
}
複製代碼

1.3.2. if

if 表達式 用於條件判斷,在 Kotlin 中 若是判斷分支比較多,一般使用 when 來替代 if

fun test(obj: Any) {
    when (obj) {
        is String -> obj.substring(0, obj.length / 2)
        is Type2 -> ignore
        is Type3 -> ignore
    }
}
複製代碼

1.3.3. 循環

Kotlin 中的 whiledo...while 循環和 Java 沒有什麼區別

while (condition) {
    /*...*/
}

do {
    /*...*/
} while (condition)
複製代碼

for 循環的語法和 Java 中的循環仍是有些區別

// Java for 循環
for (int i = 0; i <= 100; i++) {
    System.out.println(i);
}

// 對應 Kotlin 版本
for(i in 0..100){
    println(i)
}
複製代碼

使用 .. 操做符 表示一個區間,該區間是閉區間,包含開始和結束的元素

而後使用 in 操做符來遍歷這個區間

這個區間是從小到大的,若是開始的數字比結尾的還要大,則沒有意義

若是想要表示 半閉區間 ,即只包含頭部元素,不包含尾部

可使用 until 操做符:

for(i in 0 until 100){
    println(i)
}
複製代碼

若是想要倒序遍歷,可使用 downStep 關鍵字:

for(i in 100 downTo 0){
    println(i)
}
複製代碼

遍歷的時候 步長(step) 默認是 1,能夠經過 step 關鍵字來指定步長

for( i in 100 downTo 0 step 2){
    println(i)
}
複製代碼

操做符 ..downTo 表示區間都是閉區間,包含首尾元素的

1.4 Kotlin 異常處理

Kotlin 中的異常處理和 Java 的很是相似,可是也有一些用法上的區別

throw 關鍵字在 Kotlin 中是 表達式

val percentage = if (number in 0..100)
        number
    else
        throw IllegalArgumentException(
                "A percentage value must be between 0 and 100: $number")
複製代碼

另外一個不一樣點是在 Kotlin 中能夠選擇性地處理 checked exception

fun readNumber(reader: BufferedReader): Int? {
    try {
        // throws IOException
        val line = reader.readLine()
        
        // throws NumberFormatException
        return Integer.parseInt(line)
        
    } catch (e: NumberFormatException) {
        return null
    } finally {
    
        // throws IOException
        reader.close()
    }
}
複製代碼
  • reader.readLine() 會拋出 IOException 異常
  • Integer.parseInt(line) 會拋出 NumberFormatException 異常
  • reader.close() 會拋出 IOException 異常

可是咱們只處理了 NumberFormatException 並無對 IOException 進行處理

若是是在 Java 中則須要在聲明函數的時候 throws IOException 如:

int readNumber( BufferedReader reader) throws IOException {
    try {
        String line = reader.readLine(); // throws IOException
        return Integer.parseInt(line);
    } catch (NumberFormatException e) {
        return -1;
    } finally {
        reader.close(); // throws IOException
    }

}
複製代碼

固然咱們也能夠對 Integer.parseInt(line) 拋出的異常不作處理

由於 NumberFormatException 並非 checked exception 而是 runtime exception

在 Java 中,對於 checked exception 是必定要顯示的處理的,不然會編譯報錯;而對於runtime exception 則不會

對於上面的 Java 代碼,還能夠經過 Java7 的 try-with-resources 改造下:

int readNumber( BufferedReader reader) throws IOException {
    try (reader) { //把須要管理的資源做爲try的參數
        String line = reader.readLine(); 
        return Integer.parseInt(line);
    } catch (NumberFormatException e) {
        return -1;
    }
    // 省略 reader.close();
}
複製代碼

在 Kotlin 中可使用 use 函數來實現該功能:

fun readNumber(reader: BufferedReader): Int? {
    reader.use { 
        val line = reader.readLine()
        try {
            return Integer.parseInt(line)
        } catch (e: NumberFormatException) {
            return null
        }
        // 省略 reader.close();
    }
}
複製代碼

2. 再談 Kotlin 函數

上面咱們已經介紹了函數的定義和組成,下面在繼續分析函數的其餘方面

2.1 更方便的函數調用

2.1.1 調用函數時指定參數的名字

假設咱們有以下的函數:

fun <T> joinToString(collection: Collection<T>,
    separator: String, 
    prefix: String, 
    postfix: String): String
複製代碼

而後調用該函數(爲參數值指定參數名稱):

joinToString(collection, separator = " ", prefix = " ", postfix = ".")
複製代碼

2.1.2 爲函數參數指定默認值

咱們能夠把 joinToString 定義改爲以下形式:

fun <T> joinToString(collection: Collection<T>, 
    separator: String = ", ", 
    prefix: String = "", 
    postfix: String = ""
複製代碼

咱們分別爲函數的最後三個參數都設置了默認值,咱們能夠這樣調用該函數:

joinToString(list)
joinToString(list, prefix = "# ")
複製代碼

這樣也就間接的實現了Java中所謂的重載(overload),代碼也更簡潔,不用定義多個方法了

2.1.3 Parameter和Argument的區別

看過 《Kotlin In Action》 的英文原版細心的同窗可能會發現:書中的 3.2.1 章節是 Named Arguments

直譯過來是:爲參數命名。做者爲何沒有寫成 Named Parameters 呢?

下面咱們就來看下 Parameter 和 Argument 的區別

簡而言之,就是在定義函數時候的參數稱之爲 Parameter;調用函數傳入的參數稱之爲 Argument

以下圖所示:

parameters VS arguments

由於 《Kotlin In Action》3.2.1 章節是講調用函數的時候爲參數命名,因此使用了 Arguments

此外,除了 Parameter 和 Argument ,還有 Type Parameter 和 Type Argument

由於下面還要用到這兩個的概念,因此這裏咱們介紹下 Type Parameter 和 Type Argument

Type Parameter 和 Type Argument 的概念是在泛型類或者泛型函數的時候出現:

type-parameters VS type arguments

2.2 頂級函數和屬性

在 Java 中咱們須要把函數和屬性放在一個類中

在 Kotlin 中咱們能夠把某個函數或屬性直接放到某個 Kotlin 文件中

把這樣的函數或屬性稱之爲 頂級(top level)函數或屬性

例如在 join.kt 文件中:

package strings

fun joinToString(...): String { 
    ... 
}
複製代碼

在 Java 代碼中如何調用該方法呢?由於 JVM 虛擬機只能執行類中的代碼

因此 Kotlin 會生成一個名叫 JoinKt 的類,而且頂級函數是靜態的

因此能夠在 Java 中這樣調用頂級函數:

JoinKt.joinToString(...)
複製代碼

在Kotlin中如何調用,若是在不一樣的包,須要把這個頂級函數導入才能調用

//至關於 import strings.JoinKt.joinToString
import strings.joinToString 

//至關於 import strings.JoinKt.*
import strings.* 
複製代碼

全部的工具類均可以使用這樣的方式來定義

頂級屬性 一樣也是 static 靜態的

若是使用 var 來定義會生成對應的靜態setter、getter函數

若是使用 val 來定義只會生成對應的靜態getter函數

咱們知道頂級函數和屬性,最終仍是會編譯放在一個類裏面,這個類名就是頂級函數或屬性的 Kotlin文件名稱+Kt

若是所在的Kotlin文件名被修改,編譯生成的類名也會被修改,能夠經過註解的方式來固定編譯生成的類名:

@file:JvmName("StringFunctions")

package strings
fun joinToString(...): String { 
    ... 
}
複製代碼

調用的時候就能夠這樣來調用:

import strings.StringFunctions; 

StringFunctions.joinToString(list, ", ", "", "");
複製代碼

2.3 擴展函數

何謂 擴展函數 ? 擴展函數是在類的外部定義,可是能夠像類成員同樣調用該函數

擴展函數的定義格式以下圖所示:

擴展函數

其中 receiver type 就是咱們擴展的目標類,receiver object 就是目標類的對象(哪一個對象調用該擴展函數,這個this就是哪一個對象)

lastChar 就是咱們爲 String 類擴展的函數

package strings

fun String.lastChar(): Char = this.get(this.length - 1)
複製代碼

而後咱們這樣來調用該擴展函數:

println("Kotlin".lastChar())
複製代碼

若是擴展函數所在的包名和使用地方的包名不同的話,須要導入擴展函數

import strings.*
//或者
import strings.lastChar

val c = "Kotlin".lastChar()
複製代碼

2.4 擴展函數原理分析

擴展函數本質上是靜態函數,如上面的擴展函數 lastChar 反編譯後對應的 Java 代碼:

public static final char lastChar(@NotNull String $receiver) {
  Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
  return $receiver.charAt($receiver.length() - 1);
}
複製代碼

編譯的時候,會在調用的該擴展函數的地方使用 StringUtilsKt.lastChar("") 代替

因此,若是要在 Java 中使用 Kotlin 定義的擴展函數,也是直接調用該靜態方法便可

而且擴展函數是不能被覆寫(override) 的,由於它本質上是一個靜態函數

2.5 擴展屬性

擴展屬性和擴展函數的定義很是類似:

val String.lastChar: Char 
    get() = this.get(length - 1)
複製代碼

咱們必須爲這個擴展屬性定義 getter 函數,由於擴展屬性沒有 backing field

擴展屬性在定義的時候,也會生成靜態方法:

public static final char getLastChar(@NotNull String $receiver) {
  Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
  return $receiver.charAt($receiver.length() - 1);
}
複製代碼

若是擴展屬性的 receiver object 能夠被修改,能夠把擴展屬性定義成 var

var StringBuilder.lastChar: Char
    get() = get(length - 1) 
    set(value: Char) {
        this.setCharAt(length - 1, value) 
    }
複製代碼

2.6 函數的可變參數和展開操做符

2.6.1 可變參數

在 Java 中經過三個點(...)來聲明可變參數,如:

public static <T> List<T> listOf(T... items) {
	System.out.println(items.getClass()); //數組類型
	return Arrays.asList(items);
}
複製代碼

Kotlin 和 Java 不同,Kotlin 使用 vararg 關鍵來定義可變參數:

fun <T> listOf(vararg items: T): List<T> {
    println(items.javaClass)     //數組類型
    return Arrays.asList(*items) // * spread operator
}
複製代碼

對於可變參數的函數,調用它的時候能夠傳遞任意個參數

2.6.2 展開操做符

經過上面的兩段代碼比較咱們發現:Kotlin 須要顯示的將可變參數經過 * 展開,而後傳遞給 asList 函數

這裏的 * 就是 展開操做符(spread operator),在 Java 中是沒有 展開操做符

下面咱們再來看下,展開操做符的方便之處:

val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, intArr).run {
	println("size = $size")
}

//輸出結果:
size = 2
複製代碼

能夠發現,不用展現操做符的話,集合裏面只有兩個元素

那咱們把它改爲使用 展開操做符 的狀況:

val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run {
	println("size = $size")
}

//輸出結果:
size = 5
複製代碼

2.6.3 Java中的Arrays.asList()的坑和原理分析

既然上面用到了 Java 中的 Arrays.asList() 函數,下面來說下該函數的容易遇到的坑及原理分析:

public static void testArrays() {
	int[] intArr = {1, 2, 3};
	List list = Arrays.asList(intArr);
	println(list.size());   //size = 1
}

public static void testArrays2() {
	Integer[] intArr ={1, 2, 3};
	List list = Arrays.asList(intArr);
	println(list.size());  //size = 3
}
複製代碼

上面的 testArraystestArrays2 函數很是類似,只不過是數組的類型不一樣,致使 Arrays.asList(arr) 返回的集合大小不同

只要是 原始類型數組 Arrays.asList 返回的集合大小爲 1,若是是 複雜類型的數組,Arrays.asList 返回的集合大小爲數組的大小

爲何會產生這種狀況呢?下面來分析下:

首先看下 Arrays.asList 是怎麼定義的:

public static <T> List<T> asList(T... a)
複製代碼

Java 中的可變參數至關於數組:

public static <T> List<T> asList(T[] a)
複製代碼

咱們知道 Java 中的泛型必須是複雜類型,因此這裏的泛型 T 也必須是 複雜類型

當咱們傳遞 int[] 數組的時候,就會出現問題,由於 int 是原始類型,T 是複雜類型

因此 int[] 賦值給 T[] 是非法的,當 一維原始類型的數組 當作給可變參數的時候,編譯器會把這個可變參數編譯成一個 二維數組

這就是爲何會出現上面狀況的緣由

咱們再來看下 Arrays.asList 完整源碼:

public static <T> List<T> asList(T... a) {
	return new ArrayList<>(a);
}

private static class ArrayList<E> extends AbstractList<E>
	implements RandomAccess, java.io.Serializable
{
	private static final long serialVersionUID = -2764017481108945198L;
	private final E[] a;

	ArrayList(E[] array) {
		a = Objects.requireNonNull(array);
	}
	//省略其餘...
}
複製代碼

通過上面的分析咱們知道,若是是一維原始類型的數組傳遞給可變參數,這個可變參數就是 二維數組

而後把二維數組傳遞給內部ArrayList的構造方法,經過 E[] 保存下來。這裏的泛型 E 就至關於 int[]E[] 至關於 int[][]

須要注意是 Java 不容許 將個二維數組 直接賦值 給一維的泛型數組:

int[][] intArray = {{1},{2}};
T[] t = intArray;  //非法
複製代碼

可是 Java 容許 把二維數組傳遞給參數是一維的泛型數組的函數,如:

public static <T> void testGeneric(T[] data){
}
int[][] intArray = {{1},{2}};
testGeneric(intArray);
複製代碼

2.6.4 Kotlin 展開操做符的原理分析

講到這裏你可能火燒眉毛的想知道,爲何咱們上面的代碼使用了展開操做符 Arrays.asList(*intArr) 返回的集合大小就是 5 呢?

val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run {
	println("size = $size")
}

//輸出結果:
size = 5

複製代碼

反編譯後對應的 Java 代碼以下:

Integer[] intArr2 = new Integer[]{1, 2, 3, 4};
SpreadBuilder var10000 = new SpreadBuilder(2);
var10000.add(0);             //第1個元素
var10000.addSpread(intArr2); //數組裏的4個元素
List var2 = Arrays.asList((Integer[])var10000.toArray(new Integer[var10000.size()]));
int var7 = false;
String var5 = "size = " + var2.size();
System.out.println(var5);
複製代碼

原來會經過 SpreadBuilder 來處理展開操做符,SpreadBuilder 裏面維護了一個ArrayList

全部的元素都會保存到這個 ArrayList 中,而後把這個集合轉成 元素爲複雜類型數組,再傳給 Arrays.asList(arr) 函數

根據上面咱們對 Arrays.asList(arr) 的分析,咱們就知道返回的集合大小是 5

2.7 中綴調用

咱們都知道什麼是前綴(prefix),後綴(suffix)。那什麼是函數的中綴(infix)調用呢?

使用關鍵字 infix 修飾的函數都可以 中綴調用

被關鍵字 infix 修飾的函數只能有一個參數

Kotlin 中的 to 就是一箇中綴函數:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
複製代碼

下面咱們來對比下 to 函數的常規調用和中綴調用:

1.to("one")  //普通的函數調用
1 to "one"   //函數的中綴調用
複製代碼

除了 to 函數,還有咱們介紹 循環 的時候講到的 until、downTo、step 也是中綴函數:

public infix fun Int.until(to: Int): IntRange {
    if (to <= Int.MIN_VALUE) return IntRange.EMPTY
    return this .. (to - 1).toInt()
}

public infix fun Int.downTo(to: Int): IntProgression {
    return IntProgression.fromClosedRange(this, to, -1)
}

public infix fun IntProgression.step(step: Int): IntProgression {
    checkStepIsPositive(step > 0, step)
    return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}


//使用示例:
for(i in 0 until 100){
}

for (i in 100 downTo 0 step 2) {
}
複製代碼

2.8 局部函數

局部函數(local function) 是在函數裏面定義函數,局部函數只能在函數內部使用

局部函數說白了就是函數嵌套。何時使用局部函數?當一個函數裏的邏輯不少重複的邏輯,能夠把這些邏輯抽取到一個局部函數。以《Kotlin In Action》的代碼爲例:

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("Cannot save user ${user.id}: Name is empty")
    }
    if (user.address.isEmpty()) { 
        throw IllegalArgumentException("Cannot save user ${user.id}: Address is empty")
    }
    // Save user to the database 
}
複製代碼

這個 saveUser 函數裏面有些重複邏輯,若是 name 或 address 爲空都會拋出異常

可使用局部函數優化下:

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) { 
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user ${user.id}: " + "$fieldName is empty")
        }
    }
    validate(user.name, "Name") 
    validate(user.address, "Address")
    // Save user to the database   
}
複製代碼

局部函數避免了模板代碼的出現。若是不使用局部函數,咱們須要把 validate函數 定義到外面去,可是這個函數只會被 saveUser函數 使用到,從而污染了外面的全局做用域。經過局部函數使得代碼更加清晰,可讀性更高。

須要注意的是,雖然 Kotlin 容許在函數內部定義函數,可是不要嵌套太深,不然會致使可讀性太差

2.9 匿名函數

匿名函數顧名思義就是沒有名字的函數:如:

fun(x: Int, y: Int): Int {
    return x + y
}
複製代碼

匿名函數的返回類型的推導機制和普通函數同樣:

fun(x: Int, y: Int) = x + y
複製代碼

若是聲明瞭一個匿名函數 ,如何調用呢?

(fun(x: Int, y: Int): Int {
    val result = x + y
    println("sum:$result")
    return result
})(1, 9)

輸出結果:
sum:10
複製代碼

3. 字符串

Kotlin 的 String 字符串和 Java 中的幾乎是同樣的,Kotlin 在此基礎上添加了一系列的擴展函數,方便開發者更好的使用字符串

同時也屏蔽了 Java String 中容易引發開發者困惑的函數,下面咱們從 String 的 split 函數開始提及

3.1. String.split()

在 Java 中的 split 函數接收一個字符串參數:

public String[] split(String regex)
複製代碼

開發者可能會這樣來使用它:

public static void main(String[] args) {
    String[] arr = "www.chiclaim.com".split(".");
    System.out.println(arr.length); // length = 0
}
複製代碼

咱們想經過字符 . 來分割字符串 www.chiclaim.com 可是返回的是數組大小是 0

由於 split 函數接收的是一個正則字符串,而字符 . 在正則中表示全部字符串

爲了不開發開發者的困惑,Kotlin 對 CharSequence 擴展了 split 函數

若是你想經過字符串來分割,你能夠調用:

public fun CharSequence.split(
        vararg delimiters: String, 
        ignoreCase: Boolean = false, 
        limit: Int = 0): List<String>
複製代碼

若是你想經過正則表達式來分割,你能夠調用:

fun CharSequence.split(regex: Regex, limit: Int = 0): List<String>
複製代碼

經過不一樣的參數類型來減小開發者在使用過程當中出錯的概率

3.2. 三引號字符串

假如咱們須要對以下字符串,分割成 路徑、文件名和後綴「/Users/chiclaim/kotlin-book/kotlin-in-action.doc」

fun parsePathRegexp(path: String) {
    val regex = "(.+)/(.+)\\.(.+)".toRegex()
    val matchResult = regex.matchEntire(path)
    if (matchResult != null) {
        val (directory, filename, extension) = matchResult.destructured
        println("Dir: $directory, name: $filename, ext: $extension")
    }
}
複製代碼

咱們從中能夠看出 (.+)/(.+)\.(.+) 咱們使用了兩個反斜槓

不用反斜槓的話字符 . 表示任意字符,因此須要用反斜槓轉義(escape)

可是若是使用一個反斜槓,編譯器會包錯:非法轉義符

在 Java 中兩個反斜槓表示一個反斜槓

這個時候可使用三引號字符串,這樣就不要只須要一個反斜槓

val regex = """(.+)/(.+)\.(.+)""".toRegex()
複製代碼

三引號字符串中,不須要對任何字符串轉義,包括反斜槓

上面的例子,除了可使用正則來實現,還能夠經過 Kotlin 中內置的一些函數來實現:

fun parsePath(path: String) {
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")
    val fileName = fullName.substringBeforeLast(".")
    val extension = fullName.substringAfterLast(".")
    println("Dir: $directory, name: $fileName, ext: $extension")
}
複製代碼

三引號字符串除了能夠避免字符轉義,三引號字符串還能夠包含任意字符串,包括換行

而後輸出時候能夠原樣輸出 多行三引號字符串 格式:

val kotlinLogo = """ | // .|// .|/ \""" println(kotlinLogo) 輸出結果: | // .|// .|/ \ 複製代碼

因此能夠將 JSON 字符串很方便的放在 三引號字符串 中,不用管 JSON 內的特殊字符

4. 類、對象和接口

關於類的定義、對象的建立、接口的原理分析,能夠參考以前的文章:《從Java角度深刻理解Kotlin》

5. Lambda表達式

關於Lambda表達式及原理分析,能夠參考以前的文章:《從Java角度深刻理解Kotlin》

6. Kotlin 類型體系

6.1 原始數據類型

關於 Kotlin 原始數據類型,能夠查看以前的文章《從Java角度深刻理解Kotlin》

裏面有詳細的介紹,下面咱們就來介紹下 Kotlin 中關於 可空類型

6.2 可空類型

可空類型 是 Kotlin 用來避免 NullPointException 異常的

例以下面的 Java 代碼就可能會出現 空指針異常:

/*Java*/
int strLen(String s){ 
    return s.length();
}

strLen(null); // throw NullPointException
複製代碼

若是上面的代碼想要在 Kotlin 中避免空指針,可改爲以下:

fun strLen(s: String) = s.length

strLen(null); // 編譯報錯
複製代碼

上面的函數參數聲明表示參數不可爲null,調用的時候杜絕了參數爲空的狀況

若是容許 strLen 函數能夠傳 null 怎麼辦呢?能夠這樣定義該函數:

fun strLenSafe(s: String?) = if (s != null) s.length else 0
複製代碼

在參數類型後面加上 ? ,表示該參數能夠爲 null

須要注意的是,可爲空的變量不能賦值給不可爲空的變量,如:

val x: String? = null
var y: String = x //編譯報錯
//ERROR: Type mismatch: inferred type is String? but String was expected
複製代碼

在爲空性上,Kotlin 中有兩種狀況:可爲空和不可爲空;而 Java 都是能夠爲空的

6.3 安全調用操做符:?.

安全調用操做符(safe call operator): ?.

安全調用操做符 結合了 null 判斷和函數調用,如:

fun test(s:String?){
    s?.toUpperCase()
}
複製代碼

若是 s == null 那麼 s?.toUpperCase() 返回 null,若是 s!=null 那就正常調用便可

以下圖所示:

kotlin-safe-call

因此上面的代碼不會出現空指針異常

安全調用操做符 ?.,不只能夠調用函數,還能夠調用屬性。

須要注意的是,使用了 ?. 須要注意其返回值類型:

val length = str?.length

if(length == 0){
    //do something
}

複製代碼

這個時候若是 str == null 的話,那麼 length 就是 null,它永遠不等於0了

6.4 Elvis操做符: ?:

Elvis操做符 用來爲null提供默認值的,例如:

fun foo(s: String?) {
    val t: String = s ?: ""
}
複製代碼

若是 s == null 則返回 "",不然返回 s 自己,以下圖所示:

elvis操做符

上面介紹 可空性 時候的例子能夠經過 Elvis操做符改形成更簡潔:

fun strLenSafe(s: String?) = if (s != null) s.length else 0

//改爲以下形式:
fun strLenSafe(s: String?) = s.length ?: 0
複製代碼

6.5 安全強轉操做符:as?

前面咱們講到了 Kotlin 的智能強轉(smart casts),即經過 is 關鍵字來判斷是否屬於某個類型,而後編譯器自動幫咱們作強轉操做

若是咱們不想判斷類型,直接強轉呢?在 Java 中可能會出現 ClassCastException 異常

在 Kotlin 中咱們能夠經過 as? 操做符來避免相似這樣的異常

as? 若是不能強轉返回 null,反之返回強轉以後的類型,以下圖所示:

safe-cast 操做符

6.6 非空斷言:!!

咱們知道 Kotlin 中類型有可爲空和不可爲空兩種

好比有一個函數的參數是不可空類型的,而後咱們把一個可空的變量當作參數傳遞給該函數

此時Kotlin編譯器確定會報錯的,這個時候可使用非空斷言。非空斷言意思就是向編譯器保證我這個變量確定不會爲空的

以下面僞代碼:

var str:String?

// 參數不可爲空
fun test(s: String) {
    //...
}

// 非空斷言
test(str!!)

複製代碼

注意:對於非空斷言要謹慎使用,除非這個變量在實際狀況真的不會爲null,不然不要使用非空斷言。雖然使用了非空斷言編譯器不報錯了,可是若是使用非空斷言的變量是空依然會出現空指針異常

非空斷言的原理以下圖所示:

NotNullAssert.png

6.7 延遲初始化屬性

延遲初始化屬性(Late-initialized properties),主要爲了解決不必的 非空斷言 的出現

例以下面的代碼:

class MyService {
    fun performAction(): String = "foo"
}
class MyTest {
    private var myService: MyService? = null
    
    @Before fun setUp(){ 
        myService = MyService()
    }
    
    @Test fun testAction(){ 
        Assert.assertEquals("foo",myService!!.performAction())
    }
}
複製代碼

咱們知道屬性 myService 確定不會爲空的,可是咱們不得不爲它加上 非空斷言

這個時候可使用 lateinit 關鍵字來對 myService 進行延遲初始化了

class MyTest {
    private lateinit var myService: MyService
    
    @Before fun setUp(){ 
        myService = MyService()
    }
    
    @Test fun testAction(){ 
        Assert.assertEquals("foo", myService.performAction())
    }
}

複製代碼

這樣就無需爲 myService 加上非空斷言了

6.8 可空類型的擴展函數

在前面的章節咱們已經介紹了擴展函數,那什麼是 可空類型的擴展函數

可空類型的擴展函數 就是在 Receive Type 後面加上問號(?)

如 Kotlin 內置的函數 isNullOrBlank

public inline fun CharSequence?.isNullOrBlank(): Boolean
複製代碼

Kotlin 爲咱們提供了一些經常使用的 可空類型的擴展函數

如:isNullOrBlank、isNullOrEmpty

fun verifyUserInput(input: String?){ 
    if (input.isNullOrBlank()) {
        println("Please fill in the required fields")
    }
}

verifyUserInput(null)
複製代碼

有些人可能會問 input==null,input.isNullOrBlank() 不會空指針嗎?

根據上面對擴展函數的講解,擴展函數編譯後會變成靜態調用

6.9 數字類型轉換

Kotlin 和 Java 另外一個重要的不一樣點就是數字類型的轉換上。

Kotlin 不會自動將數字從一個類型轉換到另外一個類型,例如:

val i = 1
val l: Long = i // 編譯報錯 Type mismatch
複製代碼

須要顯示的將 Int 轉成 Long:

val i = 1
val l: Long = i.toLong()
複製代碼

這些顯式類型轉換函數定義在每一個原始類型上,除了 Boolean 類型

Kotlin 之因此在數字類型的轉換上使用顯示轉換,是爲了不一些奇怪的問題。

例如,下面的 Java 例子 返回 false:

new Integer(42).equals(new Long(42)) //false
複製代碼

Integer 和 Long 使用 equals 函數比較,底層是先判斷參數的類型:

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}
複製代碼

若是 Kotlin 也支持隱式類型轉換的話,下面的代碼也會返回 false ,由於底層也是經過 equals 函數來判斷的:

val x = 1    // Int
val list = listOf(1L, 2L, 3L)
x in list
複製代碼

可是在Kotlin中上面的代碼會編譯報錯,由於類型不匹配

上面的 val x = 1,沒有寫變量類型,Kotlin編譯器會推導出它是個 Int

  • 若是字面量是整數,那麼類型就是 Int
  • 若是字面量是小數,那麼類型就是 Double
  • 若是字面量是以 f 或 F 結尾,那麼類型就是 Float
  • 若是字面量是 L 結尾,那麼類型就是 Long
  • 若是字面量是十六進制(前綴是0x或0X),那麼類型是 Long
  • 若是字面量是二進制(前綴是0b或0B),那麼類型是 Int
  • 若是字面量是單引號中,那麼類型就是 Char

須要注意的是,數字字面量當作函數參數或進行算術操做時,Kotlin會自動進行相應類型的轉換

fun foo(l: Long) = println(l)
val y = 0
foo(0)  // 數字字面量做爲參數
foo(y)  // 編譯報錯


val b: Byte = 1
val l = b + 1L // b 自動轉成 long 類型
複製代碼

6.10 Any類型

Any 類型 和 Java 中的 Object 相似,是Kotlin中全部類的父類

包括原始類型的包裝類:Int、Float 等

Any 在編譯後就是 Java 的 Object

Any 類也有 toString() , equals() , and hashCode() 函數

若是想要調用 wait 或 notify,須要把 Any 強轉成 Object

6.11 Unit 類型

Unit 類型和 Java 中的 void 是一個意思

下面介紹它們在使用過程的幾個不一樣點:

  1. 函數沒有返回值,Unit能夠省略

例以下面的函數能夠省略 Unit:

fun f(): Unit { ... }
fun f() { ... } //省略 Unit
複製代碼

可是在 Java 中則不能省略 void 關鍵字

  1. Unit 做爲 Type Arguments

例以下面的例子:

interface Processor<T> {
    fun process(): T
}

// Unit 做爲 Type Arguments
class NoResultProcessor : Processor<Unit> {
    override fun process() { // 省略 Unit
        // do stuff
    }
}
複製代碼

若是在 Java 中,則須要使用 Void 類:

class NoResultProcessor implements Processor<Void> {

    @Override
    public Void process() {
        return null; //須要顯式的 return null
    }
}
複製代碼

6.12 Nothing 類型

Nothing 類是一個 標記類

Nothing 不包含任何值,它是一個空類

public class Nothing private constructor()
複製代碼

Nothing 主要用於 函數的返回類型 或者 Type Argument

關於 Type Argument 的概念已經在前面的 Parameter和Argument的區別 章節介紹過了

下面介紹下 Nothing 用於函數的返回類型

對於有些 Kotlin 函數的返回值沒有什麼實際意義,特別是在程序異常中斷的時候,例如:

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}
複製代碼

你可能會問,既然返回值沒有意義,使用Unit不就能夠了嗎?

可是若是使用Unit,當與 Elvis 操做符 結合使用的時候就不太方便:

fun fail(message: String) { // return Unit
    throw IllegalStateException(message)
}

fun main() {
    var address: String? = null
    val result = address ?: fail("No address")
    //編譯器報錯,由於result是Unit類型,因此result沒有length屬性
    println(result.length) 
}
複製代碼

這個時候使用 Nothing 類型做爲 fail 函數的返回類型 就能夠解決這個問題:

fun fail(message: String) : Nothing {
    throw IllegalStateException(message)
}

fun main() {
    var address: String? = null
    val result = address ?: fail("No address")
    println(result.length) // 編譯經過
}

複製代碼

7. 集合與數組

能夠參考:《從Java角度深刻理解Kotlin》 裏關於集合和數組的部分

8. Kotlin 操做符重載

操做符重載內容有點多,單獨寫了一篇文章:《Kotlin操做符重載詳解》 ,學完這篇文章相信你對 Kotlin 操做符 有全新的理解和掌握

9. 高階函數

高階函數的定義及原理分析,能夠參考以前的文章:《從Java角度深刻理解Kotlin》

10. 內聯函數

《從Java角度深刻理解Kotlin》中關於高階函數的部分,咱們說到爲了優化高階函數須要使用 inline 關鍵字來修飾

可是不是全部的 高階函數 都能聲明爲內聯(inline)函數

若是高階函數的 lambda 參數被局部變量保存起來,那麼這個高階函數就不能被 inline 修飾

由於若是Kotlin支持這麼作,那麼這個局部變量就必須包含lambda代碼,可是Java/Kotlin底層並無能保存一段代碼的類型

例如高階函數 Sequence<T>.map 就不能使用 inline 修飾,下面分析其緣由:

public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
    return TransformingSequence(this, transform)
}


internal class TransformingSequence<T, R>
constructor(
    private val sequence: Sequence<T>, 
    private val transformer: (T) -> R) : Sequence<R> {
    // 省略實現代碼
}
複製代碼

從中能夠發現,Sequence<T>.map 的參數(lambda),被傳遞給了 TransformingSequence 的構造函數

而後被 TransformingSequence 經過屬性的形式將該lambda 保存起來了

因此一個高階函數可以被內聯,要麼參數lambda被直接使用,要麼直接傳遞給另外一個內聯函數,沒有將lambda保存到局部變量或者屬性中

另外一方面,若是一個高階函數接收多個lambda,並且有的lambda參數包含的代碼量比較大,能夠指定該lambda不內聯:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
    // ...
}
複製代碼

11. 泛型

11.1 泛型的類型參數

泛型的類型參數:Type Parameter 和 Type Argument 已經在前面介紹過了,這裏就不贅述了

就像通常類型同樣,Type Argument 一般可以被 Kotlin 編譯推導出具體的類型

例如,經過 listOf 函數建立集合:

val list = listOf("Chiclaim", "Kotlin")
複製代碼

此時Kotlin編譯推導出 Type Argument 的類型是 String,因此建立的集合是 String 類型的

若是你建立的是一個空集合,那麼你就必須指定 Type Argument

由於空集合 Kotlin 編譯器沒法推導出 Type Argument 的類型:

val list: MutableList<String> = mutableListOf()
//或者
val list = mutableListOf<String>()
複製代碼

因此,在Kotlin中要麼顯式地指定 Type Argument;要麼編譯器可以推導Type Argument 的類型

可是,Java是容許不指定 Type Argument 的,這主要是由於 泛型是 JDK1.5 纔出來的新特性

爲了兼容之前的代碼,因此 Java 容許在使用泛型的時候不指定 Type Argument,這就是所謂的 raw type

11.2 泛型函數和屬性

例如上面用到的 listOf 就是一個泛型函數

除此以外,泛型還能夠用在 擴展函數 上,如:

public fun <T> List<T>.slice(indices: IntRange): List<T>
複製代碼

該函數的 type parameter 用在 receiver type 和 return type 中,以下圖所示:

一樣地,調用泛型擴展函數要麼顯式指定 type argument,要麼編譯可以推導出類型:

val letters = ('a'..'z').toList()
letters.slice<Char>(0..2)) // 顯式指定 type argument
letters.slice(10..13)      // 編譯器類型推導出 Char
複製代碼

泛型除了能夠用在擴展函數上,還能夠用到擴展屬性上:

val <T> List<T>.penultimate: T 
    get() = this[size - 2]

println(listOf(1, 2, 3, 4).penultimate)
複製代碼

11.3 泛型類

Kotlin 泛型類和 Java 的泛型類差很少,都是將泛型放到 <>

// 聲明類的時候聲明 type parameter
interface List<T> {
    //將 type parameter 用於返回類型
    operator fun get(index: Int): T { 
        // ... 
    }
}
複製代碼

繼承或者實現這個接口的時候須要提供 type argument

這個 type argument 能夠是具體的類,也能夠是另外一個泛型:

// type argument 就是 String 類
class StringList: List<String> {
    override fun get(index: Int): String = ... 
}

//使用另外一個泛型T當作 type argument
class ArrayList<T> : List<T> {
    override fun get(index: Int): T = ... 
}
複製代碼

須要注意的是 ArrayList<T> 這裏的 TList<T>T 是兩個東西,只是名字同樣而已

ArrayList 定義了一個全新的 Type parameter 名爲 T,而後將其當作父類List的 Type argument

11.4 Type parameter 約束

Type parameter約束 可以限制 Type argument 的類型

例以下面的一段 Java 代碼:

<T extends Number> T sum(List<T> list)
複製代碼

調用 sum 函數的時候,List 集合裏的元素只能是 Number 或 Number 的子類型

也就是說 Type argument 只能是 Number 或 Number的子類型

把這裏的 Number 稱之爲 上界(upper bound)

上面的代碼用 Kotlin 來表示就是這樣的:

fun <T : Number> List<T>.sum(): T
複製代碼

上面只是限制 T 是 Number 或者 Number 的子類,若是還想加上其餘限制呢?

例以下面的例子,爲 Type parameter 加上了兩個類型約束

T 必須同時知足兩個條件:

  • T 是 CharSequence 或者 CharSequence 子類
  • T 是 Appendable 或者 Appendable 子類
fun <T> ensureTrailingPeriod(seq: T)
        where T : CharSequence, T : Appendable {
    if (!seq.endsWith('.')) {
        seq.append('.')
    }
}

val helloWorld = StringBuilder("Hello World")
ensureTrailingPeriod(helloWorld)
//下面代碼 編譯報錯,由於String是CharSequence的子類,但不是Appendable子類
ensureTrailingPeriod("Hello Word") 
複製代碼

11.5 泛型的不變性、協變性、逆變性及泛型具體化

泛型的不變性、協變性、逆變性及泛型具體化及原理分析,能夠參考以前的文章:《從Java角度深刻理解Kotlin》

12. Kotlin 中的 break、return 和 label

12.1 返回和跳轉

Kotlin 有三種結構化跳轉表達式:

  • break。默認終止最直接包圍它的循環。
  • continue。默認繼續下一次最直接包圍它的循環。
  • return。默認從最直接包圍它的函數或者匿名函數返回。

12.1.1 break 和 break label

break默認是終止離它最近的循環,這個和 Java 是同樣的:

fun breakLabel() {
    for (i in 5..10) {
        for (j in 1..10) {
            //當 i==j 中斷最裏面的循環
            if (i == j) break 
            println("$i-$j")
        }
    }
}
複製代碼

若是要跳出最外層循環,則須要和 label,結合使用

label 的語法爲:labelName@

fun breakLabel() {
    //最外層循環處定義了一個名爲loop的label
    loop@ for (i in 5..10) {
        for (j in 1..10) {
            if (i == j) break@loop //跳出最外層循環
            println("$i-$j")
        }
    }
}

//輸出結果:
5-1
5-2
5-3
5-4
複製代碼

12.1.2 continue 和 continue label

continue 、continue label 用法和 break、break label 是相似的,就不作贅述了

12.1.3 return 和 return label

return 默認從最直接包圍它的函數或者匿名函數返回。如:

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return // 跳出整個函數
        print(it)
    }
    println("this point is unreachable")
}
複製代碼

有可能有人會疑問:離 return 表達式最近的不是 forEach 函數嗎?不該該是 return forEach 函數嗎?

其實 forEach 是一個高階函數,該函數接收一個 lambda表達式,更準確的說 return 表達式是 在lambda 表達式內

而lambda表達式不是函數,因此 最直接包圍 return 表達式的foo 函數

若是想 return forEach 處怎麼辦呢?有三種方式

第一種方式,配合 label 來實現:

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach lit@{ // 聲明 名爲 lit 的 label
        if (it == 3) return@lit 
        print(it)
    }
    print(" done with explicit label")
}

//輸出結果
1245 done with explicit label
複製代碼

第二種是方式:使用 隱式label

每一個高階函數都有和它名字同樣的隱式label:

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return@forEach
        print(it)
    }
    print(" done with implicit label")
}
//輸出結果
1245 done with explicit label
複製代碼

第三種方式:使用 匿名函數

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {
        if (value == 3) return
        print(value)
    })
    print(" done with anonymous function")
}
//輸出結果
1245 done with explicit label
複製代碼

能夠看出當 value==3 的時候被過濾掉了,可是還會繼續下一次循環

若是想要 value==3 中斷循環,怎麼辦?

fun foo() {
    run loop@{
        listOf(1, 2, 3, 4, 5).forEach {
            if (it == 3) return@loop
            print(it)
        }
    }
    print(" done with nested loop")
}
//輸出結果
12 done with nested loop
複製代碼

12.2 lambda表達式中return一個值

例如 Kotlin 中的 filter 函數,接收一個lambda 表達式,該lambda返回一個 boolean 值

listOf<Int>().filter { it > 0 }
複製代碼

在該例子中,lambda體內只有一行表達式,且該表達式就是 boolean 類型,因此不須要顯式的 return

可是若是須要在 lambda body 裏進行復雜的判斷,而後根據條件來 return 怎麼辦?

lambda-return

固然上面的代碼能夠經過 when 來簡化,這個例子代碼只是來演示 lambda return 值的狀況

13. Kotlin反射和註解

13.1 定義註解

定義註解的方式很是簡單:

annotation class AnnotationName
複製代碼

定義註解的語法和定義類的語法很是類似,只須要在後者的基礎上加上 annotation 關鍵字

爲註解加上參數:

annotation class JsonName(val name: String)
複製代碼

相比之下,Java定義的方式是這樣的:

public @interface JsonName {
    String name();
}
複製代碼

在使用 JsonName 註解的時候,Kotlin 註解是容許省略 name 參數的,而 Java 不容許:

@JsonName("Chiclaim") //Kotlin容許省略參數名name
@JJsonName(name = "Chiclaim") // Java不容許省略參數名name
var username: String? = null
複製代碼

13.2 元註解(Meta-Annotation)

元註解就是描述註解的註解。 元註解主要用於告訴編譯器如何處理該註解

13.2.1 @Target元註解

@Target(AnnotationTarget.PROPERTY)
annotation class JsonName
複製代碼

Target 註解就是元註解,告訴編譯器 JsonName 註解只用到類的屬性上

AnnotationTarget 是一個枚舉類,經常使用的枚舉對象有:

  • CLASS:用於類、接口、註解類
  • ANNOTATION_CLASS:只用於註解類
  • TYPE_PARAMETER:泛型參數
  • PROPERTY:屬性
  • FIELD:字段,包括back-field
  • LOCAL_VARIABLE:局部變量
  • VALUE_PARAMETER:用於函數或構造函數的參數
  • CONSTRUCTOR:僅用於構造函數
  • FUNCTION:用於函數,不包括構造函數
  • PROPERTY_GETTER:僅用於屬性的getter函數
  • PROPERTY_SETTER:僅用於屬性的setter函數
  • EXPRESSION:用於表達式

13.2.2 @Retention元註解

@Retention 元註解用於告訴編譯器,目標註解保留到哪一個階段,例如是編譯仍是運行時階段

Java 默認的保留到 class 字節碼中,運行時不可見

Kotlin 默認是保留到 runtime,運行時可見

@Retention 經過 AnnotationRetention 來制定註解保留在哪一個階段:

  • SOURCE:註解只保留在源文件中,編譯後的二進制文件不存在該註解
  • BINARY:註解保留在編譯後的二進制文件中
  • RUNTIME:註解保留到運行時,反射可見

13.2.3 @Repeatable元註解

容許在一個元素上使用屢次該註解,例如:

@JsonName
@JsonName
var firstName: String? = null
複製代碼

13.2.4 @MustBeDocumented元註解

代表該註解是生成的API文檔的一部分,這樣在API文檔中既能夠看到該註解了

13.3 Kotlin 中經常使用的註解

13.3.1 @Deprecated

Kotlin中的 @Deprecated 比 Java 中的更強大,Kotlin 還能夠指定用哪一個 API 來替換廢棄的API

@Deprecated("use greeting", ReplaceWith("greeting()",
        "annotation.Person.greeting()"))
fun sayHello(){

}
複製代碼

還可使用 修復 (Show Intention Actions) 快捷鍵將廢棄的 API 用替換成建議使用的 API:

deprecated.png

13.3.2 @Suppress

@Suppress 用來去除代碼中的一些 warning

對於編輯器提示的 warning,咱們要儘量的去修復,由於有多是代碼的不規範致使的

可是對於一些可有可無的或者暫時沒法處理的 warning,能夠經過 @Suppress 來去掉 warning

@Suppress("UNCHECKED_CAST")
fun test(source: Any) {
    if (source is List<*>) {
        val list = source as List<String>
        val str: String = list[0]
    }
}
複製代碼

有些同窗可能會問,@Suppress 註解的參數,具體有哪些呢?

這些參數統必定義在 Errors.java

13.3.3 @JvmField

編譯器不會爲被 @JvmField 修飾的屬性生成getter、setter方法:

class Person(
        @JvmField
        var name: String,
        var age: Int
)

//生成代碼:

public final class Person {
   @JvmField
   @NotNull
   public String name;
   private int age;

   public final int getAge() {
      return this.age;
   }

   public final void setAge(int var1) {
      this.age = var1;
   }

   public Person(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.age = age;
   }
}
複製代碼

name 字段沒有生成 getter、setter 方法 而且該屬性是 public 的

也就是說 Kotlin 編譯器把該屬性當作 字段 暴露給外面,能夠經過 private 關鍵顯式指定爲 private

13.3.4 @JvmName

@JvmName 指定編譯器生成的 class 字節碼中該元素對應的名稱

用於解決 Kotlin 和 Java 交互 Kotlin 命名發生變化的兼容問題

//指定編譯生成的class字節碼文件的名稱
@file:JvmName("JavaClassName")

package annotation.kotlin_annoation

//指定 javaMethodName 編譯成 class 字節碼後,對應的名稱
@JvmName("java_method_name")
fun javaMethodName(){
    println("java method name")
}

//在Kotlin中調用
javaMethodName()

//在Java中調用(Kotlin函數名發生變化也不影響Java調用)
JavaClassName.java_method_name();
複製代碼

13.3.5 @JvmStatic

@JvmStatic 用於告訴編譯器在 class字節碼 中生成一個靜態方法

這樣 Java 在調用這個 Kotlin 的 companion 函數是就不須要 Companion 內部類了

class JvmStaticTest {
    companion object {
    
        fun greeting() {
            println("hello...")
        }

        @JvmStatic
        fun sayHello() {
            println("hello...")
        }
    }
}

//在 Java 中調用
JvmStaticTest.Companion.greeting();
JvmStaticTest.sayHello();
複製代碼

13.4. Kotlin 反射

13.4.1 Kotlin反射的基本概念

Java 中的反射 API 主要集中在 com.java.reflect 包中

Kotlin 中的反射 API 主要集中在 kotlin.reflect 包中

爲何 Kotlin 還要搞一套反射的 API 呢?

由於 Kotlin 有不少 Java 沒有的語言特性,

例如 Java 是經過 Class 來描述一個類,Kotlin 經過 KClass 來表述一個類

對於 Kotlin 一個類,它多是 sealed class,而 Java 中的類沒有這個概念

因此 Kotlin 的反射 API 是針對語言自己特性設計的

同時,Kotlin 在編譯後也是 class字節碼,因此 Java 的反射API是兼容全部 Kotlin 代碼的

下面咱們來看下經常使用經常使用的 Kotlin 和 Java API的對應關係:

Java Kotlin
java.lang.Class kotlin.reflect.KClass
java.lang.reflect.Field kotlin.reflect.KProperty
java.lang.reflect.Method kotlin.reflect.KFunction
java.lang.reflect.Parameter kotlin.reflect.KParameter

咱們知道 Kotlin 中的 KClass 對應 Java 中的 Class,那麼在 Kotlin 代碼中如何獲取 Class 和 KClass:

// Kotlin Class
val kclazz: KClass<Book> = Book::class
val kclazz2: KClass<Book> = book.javaClass.kotlin

// Java Class
val jclazz: Class<Book> = Book::class.java
val jclazz2: Class<Book> = book.javaClass
複製代碼

13.4.2 成員引用和反射

上面的獲取 Class 和 KClass 代碼中使用到了成員引用操做符(::),因此下面介紹成員引用和反射的關係

成員引用(Member references)包括:屬性引用(Property references)和函數引用(Function references)

  1. 屬性引用

若是成員引用操做符後面是屬性,那麼這就是屬性引用

class Book(val name: String, var author: String) {
    fun present() = "book's name = $name, author = $author "
}

// 屬性引用
val pro = Book::name
複製代碼

咱們上面定義的屬性引用究竟是什麼呢?看下反編譯後的Java代碼:

KProperty1 prop = MemberReferenceTestKt$main$prop$1.INSTANCE;

final class MemberReferenceTestKt$main$prop$1 extends PropertyReference1 {
   public static final KProperty1 INSTANCE = new MemberReferenceTestKt$main$prop$1();

   public String getName() {
      return "name";
   }

   public String getSignature() {
      return "getName()Ljava/lang/String;";
   }

   public KDeclarationContainer getOwner() {
      return Reflection.getOrCreateKotlinClass(Book.class);
   }

   @Nullable
   public Object get(@Nullable Object receiver) {
      return ((Book)receiver).getName();
   }
}
複製代碼

發現咱們上面的定義的屬性引用,內部生成了一個繼承自 PropertyReference1 的內部類

而這個 PropertyReference1 實際上最終實現了 KProperty 接口

因此 Book::name 返回的值其實是 KProperty 類型

因爲屬性 name 使用 val 關鍵字修飾,若是是 var 關鍵字修飾,

Book::name 返回的值其實是 KMutableProperty 類型

KProperty<T,V> 是一個泛型類,該泛型接收兩個參數:

  • 第一個泛型參數是 receiver 的類型,receiver 就是屬性的全部者
  • 第二個泛型參數是該屬性的類型

介紹完 KProperty 咱們能夠寫一個函數用來輸出任意類的 屬性名稱屬性值

fun <T> printProperty(instance: T, prop: KProperty1<T, *>) {
    println("${prop.name} = ${prop.get(instance)}")
}
複製代碼

還能夠利用 KProperty 來爲 var 修飾的屬性設置值:

fun <T> changeProperty(instance: T, prop: KMutableProperty1<T, String>) {
    val value = prop.get(instance)
    prop.set(instance, "$value, Johnny")
}
複製代碼

下面演示下這兩個函數的使用:

val book = Book("Kotlin從入門到放棄", "Chiclaim")

// 打印 name 屬性的值
printProperty(book, Book::name)
// 打印 author 屬性的值
printProperty(book, Book::author)
// 修改 author 屬性的值
changeProperty(book, Book::author)
println("book's author is [${book.author}]")
 
輸出結果:
name = Kotlin從入門到放棄
author = Chiclaim
book's author is [Chiclaim, Johnny] 複製代碼
  1. 函數引用

成員引用操做符 後面是函數名的就是函數引用,例如:

// 函數引用
val fk = Book::present
複製代碼

屬性引用 會生成一個內部類實現了 KProperty 接口,同理,函數引用 內部也會生成一個內部類,只不過這個類實現了 KFunction 接口

因此 Book::present 返回值是 KFunction 類型

能夠用過 KFunction 來打印函數名稱和函數調用:

val fk = Book::present
// 函數名稱
println("function name is ${fk.name}")
// 函數調用
println(fk.call(book))

輸出結果: 
function name is present
book's name = Kotlin從入門到放棄, author = Chiclaim, Johnny 複製代碼

對於頂級函數能夠這樣來定義函數引用:::foo


聯繫我

下面是個人公衆號,乾貨文章不錯過,有須要的能夠關注下,有任何問題能夠聯繫我:

公衆號:  chiclaim

Reference

《Kotlin in action》

Java expressions

相關文章
相關標籤/搜索