"Kotlin"系列: 一、Kotlin入門

sunflowers

前言

Google 在2017年 I/O 大會上宣布,Kotlin 正式成為 Android 的一級開發(fā)語言,和 Java 平起平坐,AndroidStudio 也對 Kotlin 進(jìn)行了全面的支持,兩年后,Google 又在2019年 I/O 大會上宣布,Kotlin 已經(jīng)成為 Android 的第一開發(fā)語言,雖然說 Java 仍然可以繼續(xù)使用,但 Google 更加推薦我們使用 Kotlin 來開發(fā) Android 應(yīng)用程序,后續(xù)提供的官方 Api 也會優(yōu)先考慮 Kotlin 版本,而且現(xiàn)在的開發(fā)者無論是寫技術(shù)博客,還是第三方庫,基本上都用的Kotlin,外加公司技術(shù)演進(jìn)需要使用到 Kotlin,因此學(xué)習(xí) Kotlin 編程是非常重要和緊急的一件事情。

Kotlin 介紹

官網(wǎng)對 Kotlin 的介紹:A modern programming languagethat makes developers happier. 翻譯過來就是:Kotlin 是一門讓開發(fā)者更開心的現(xiàn)代程序設(shè)計語言 。 由 JetBrains 公司開發(fā)和設(shè)計,它也是一門基于 JVM 的靜態(tài)語言。

問題

在學(xué)習(xí) Kotlin 的時候我心里會有一些疑問??? ?

1、Android 操作系統(tǒng)是由 Google 開發(fā)出來的,為啥 JetBrains 作為一個第三方公司,卻能設(shè)計出一門語言來開發(fā) Android 應(yīng)用程序?

因?yàn)?Java 虛擬機(jī)(Android 中叫 ART,一種基于 Java 虛擬機(jī)優(yōu)化的虛擬機(jī))并不直接和你編寫的這門語言的源代碼打交道,而是和你編譯之后的 class 字節(jié)碼文件打交道?;?JVM 的語言,如 Kotlin,Groovy等,它們都會有各自的編譯器,把源文件編譯成 class 字節(jié)碼文件,Java 虛擬機(jī)不關(guān)心 class 字節(jié)碼文件是從哪里編譯而來,只要符合規(guī)格的 class 字節(jié)碼文件,它都能識別,正是因?yàn)檫@個原因,JetBrains 才能以一個第三方公司設(shè)計出一門來開發(fā) Android 應(yīng)用程序的編程語言

2、為啥有了 Java 來開發(fā) Android 應(yīng)用程序,Google 還要推薦使用 Kotlin 來開發(fā)?

原因有很多,列舉主要的幾點(diǎn):

  • 1)、Kotlin 語法更加簡潔,使用 Kotlin 開發(fā)的代碼量可能會比 Java 開發(fā)的減少 50% 甚至更多
  • 2)、Kotlin 的語法更加高級,相比于 Java 老舊的語法,Kotlin 增加了很多現(xiàn)代高級語言的語法特性,大大提升了我們的開發(fā)效率
  • 3)、Kotlin 和 Java 是 100% 兼容的,Kotlin 可以直接調(diào)用 Java 編寫的代碼,也可以無縫使用 Java 第三方開源庫,這使得 Kotlin 在加入了諸多新特性的同時,還繼承了 Java 的全部財富

3、為啥 Kotlin 中要顯示的去聲明一個非抽象類可繼承,而不像 Java 那樣定義的類默認(rèn)可繼承?

因?yàn)橐粋€類默認(rèn)可被繼承的話,它無法預(yù)知子類會如何去實(shí)現(xiàn),因此存在一些未知的風(fēng)險。類比 val 關(guān)鍵字是同樣的道理,在 Java 中,除非你主動給變量聲明 final 關(guān)鍵字,否則這個變量就是可變的,隨著項(xiàng)目復(fù)雜度增加,多人協(xié)作開發(fā),你永遠(yuǎn)不知道一個可變的變量會在什么時候被誰修改了,即使它原本不應(yīng)該修改,也很難去排查問題。因此 Kotlin 這樣的設(shè)計是為了讓程序更加的健壯,也更符合高質(zhì)量編碼的規(guī)范

下面我們就正式進(jìn)入到 Kotlin 的學(xué)習(xí)

附上一張學(xué)習(xí) Kotlin 的思維導(dǎo)圖

image-20210116114509401

注意: Kotlin 現(xiàn)作為 Android 第一開發(fā)語言,AndroidStudio 作為 Google 的親兒子,對 Kotlin 進(jìn)行了完美的支持,開發(fā)提示應(yīng)有盡有,因此下面所有的演示代碼都是跑在 AndroidStudio 上的

一、變量和函數(shù)

1、變量

1)、使用 val(value 的簡寫)關(guān)鍵字來聲明一個不可變的變量,也就是只讀變量,這種變量初始賦值后就不能重新賦值了,對應(yīng) Java 中的 final 變量

2)、使用 var (variable 的簡寫)關(guān)鍵字用來聲明一個可變的變量,也就是可讀寫變量,這種變量初始賦初值后仍然可以重新被賦值,對應(yīng) Java 中的非 final 變量

3)、Kotlin 中的每一行代碼都不用加 ;

//在 Java 中,我們會這么定義
int a = 10;
boolean b = true

//在 Kotlin 中,我們可以這么定義,當(dāng)給變量賦值后,Kotlin 編譯器會進(jìn)行類型推導(dǎo)
//定義一個不可變的變量 a 
val a = 10
//定義一個可變的變量 b
var b = true

//如果我們顯示的給變量指定類型,Kotlin 就不會進(jìn)行類型推導(dǎo)了
val a: Int = 10
var b: Boolean = "erdai"

如果你觀察的仔細(xì)會發(fā)現(xiàn),上述代碼 Kotlin 定義變量給變量顯示的指定類型時,使用的都是首字母大小的 Int,Boolean,而在 Java 中都是小寫的 int,boolean,這表明: Kotlin 完全拋棄了 Java 中的基本數(shù)據(jù)類型,全部都是對象數(shù)據(jù)類型。 下面給出一個 Java 和 Kotlin 數(shù)據(jù)類型對照表:

Java 基本數(shù)據(jù)類型 Kotlin 對象數(shù)據(jù)類型 數(shù)據(jù)類型說明
byte Byte 字節(jié)型
short Short 短整型
int Int 整型
long Long 長整型
float Float 單精度浮點(diǎn)數(shù)
double Double 雙精度浮點(diǎn)數(shù)
char Char 字符型
boolean Boolean 布爾型

2、常量

Kotlin 中定義一個常量需要滿足三個條件

1)、使用 const val 來修飾,并初始化

2)、修飾的類型只能是字符串和基礎(chǔ)對象類型

3)、只能修飾頂層的常量,object 修飾的成員,companion object 的成員,這些概念后面還會講到

//定義一個頂層的常量,這個常量不放在任何的類中
const val CONSTANT = "This is a constant"

//定義一個 object 修飾的單例類,類中定義一個常量
object SingeTon {
    const val CONSTANT = "This is a constant"
}

class KotlinPractice {
    //定義一個 companion object 修飾的伴生對象,里面定義一個常量
    companion object{
        const val CONSTANT = "This is a constant"
    }
}

3、函數(shù)

1)、函數(shù)和方法是同一個概念,在 Java 中我們習(xí)慣叫方法 (method),但是 Kotlin 中就需要叫函數(shù) (function)

2)、函數(shù)是運(yùn)行代碼的載體,像我們使用過的 main 函數(shù)就是一個函數(shù)

Kotlin 中定義語法的規(guī)則:

fun methodName(param1: Int, param2: Int): Int {
    return 0
}

//下面這兩個方法效果是一樣的
fun methodName1(params: Int,params2: Int): Unit{

}

fun methodName1(params: Int,params2: Int){

}

上述函數(shù)語法解釋:

  • fun ( function 的縮寫 ) 是定義一個函數(shù)的關(guān)鍵字,無論你定義什么函數(shù),都要用 fun 來聲明
  • 函數(shù)名稱可以隨便取,就像 Java 里面定義函數(shù)名一樣
  • 函數(shù)名里面的參數(shù)可以有任意多個,參數(shù)的聲明格式為:"參數(shù)名":"參數(shù)類型"
  • 參數(shù)名后面這部分代表返回值,我們這返回的是一個 Int 類型的值,這部分是可選的,如果不定義,默認(rèn)返回值為 Unit,且 Unit 可省略

實(shí)踐一下:

fun main() {
    val number1 = 15
    val number2 = 20
    val maxNumber = largeNumber(number1,number2)
    println(maxNumber)
}

fun largeNumber(number1: Int,number2: Int) : Int{
    //調(diào)用頂層 max 函數(shù)計算兩者中的最大值
    return max(number1,number2)
}

//打印結(jié)果
20

Kotlin 語法糖:當(dāng)一個函數(shù)體中只有一行代碼的時候,我們可以不編寫函數(shù)體,可以將唯一的一行代碼寫在函數(shù)定義的尾部,中間用 = 連接即可

那么上述 largeNumber 這個函數(shù)我們改造一下:

//根據(jù)上述語法糖,我們省略了函數(shù)體的 {} 和 return 關(guān)鍵字,增減的 = 連接
fun largeNumber(number1: Int,number2: Int) : Int = max(number1,number2)
//根據(jù) Kotlin 類型推導(dǎo)機(jī)制,我們還可以把函數(shù)的返回值給省略,最終變成了這樣
fun largeNumber(number1: Int,number2: Int) = max(number1,number2)

二、程序的邏輯控制

1、if 條件語句

1)、Kotlin 中的 if 條件語句除了繼承了 Java 中 if 條件語句的所有特性,且可以把每一個條件中的最后一行代碼作為返回值

我們改造一下上述 largeNumber 函數(shù)的內(nèi)部實(shí)現(xiàn):

//Kotlin 中把每一個條件中的最后一行代碼作為返回值
fun largeNumber(number1: Int,number2: Int) : Int{
    return if(number1 > number2){
        number1
    }else {
        number2
    }
}

//根據(jù)上面學(xué)習(xí)的語法糖和 Kotlin 類型推導(dǎo)機(jī)制,我們還可以簡寫 largeNumber 函數(shù),最終變成了這樣
fun largeNumber(number1: Int,number2: Int) = if(number1 > number2) number1 else number 2

2、when 條件語句

類比 Java 中的 Switch 語句學(xué)習(xí),Java 中的 Switch 并不怎么好用:

1)、Switch 語句只能支持一些特定的類型,如整型,短于整型,字符串,枚舉類型。如果我們使用的并非這幾種類型,Switch 并不可用

2)、Switch 語句的 case 條件都要在最后加上一個 break

這些問題在 Kotlin 中都得到了解決,而且 Kotlin 還加入了許多強(qiáng)大的新特性:

1)、when 條件語句也是有返回值的,和 if 條件語句類似,條件中的最后一行代碼作為返回值

2)、when 條件語句允許傳入任意類型的參數(shù)

3)、when 條件體中條件格式:匹配值 -> { 執(zhí)行邏輯 }

4)、when 條件語句和 if 條件語句一樣,當(dāng)條件體里面只有一行代碼的時候,條件體的 {} 可省略

//when 中有參數(shù)的情況
fun getScore(name: String) = when (name) {
    "tom" -> 99
    "jim" -> 80
    "lucy" -> 70
    else -> 0
}

//when 中無參數(shù)的情況,Kotin 中判斷字符串或者對象是否相等,直接使用 == 操作符即可
fun getScore(name: String) = when {
    name == "tom" -> 99
    name == "jim" -> 80
    name =="lucy" -> 70
    else -> 0
}

3、循環(huán)語句

主要有以下兩種循環(huán):

1)、while 循環(huán),這種循環(huán)和 Java 沒有任何區(qū)別

2)、for 循環(huán),Java 中常用的循環(huán)有:for-i,for-each,Kotlin 中主要是:for-in

區(qū)間

1)、使用 .. 表示創(chuàng)建兩端都是閉區(qū)間的升序區(qū)間

2)、使用 until 表示創(chuàng)建左端是閉區(qū)間右端是開區(qū)間的升序區(qū)間

3)、使用 downTo 表示創(chuàng)建兩端都是閉區(qū)間的降序區(qū)間

4)、在區(qū)間的后面加上 step ,表示跳過幾個元素

//注意: Kotlin 中可以使用字符串內(nèi)嵌表達(dá)式,也就是在字符串中可以引用變量,后續(xù)還會講到
//情況1
fun main() {
    //使用 .. 表示創(chuàng)建兩端都是閉區(qū)間的升序區(qū)間
    for (i in 0..10){
        print("$i ")
    }
}
//打印結(jié)果
0 1 2 3 4 5 6 7 8 9 10

//情況2
fun main() {
    //使用 until 表示創(chuàng)建左端是閉區(qū)間右端是開區(qū)間的升序區(qū)間
    for (i in 0 until 10){
        print("$i ")
    }
}
//打印結(jié)果
0 1 2 3 4 5 6 7 8 9

//情況3
fun main() {
    //使用 downTo 表示創(chuàng)建兩端都是閉區(qū)間的降序區(qū)間
    for (i in 10 downTo 0){
        print("$i ")
    }
}
//打印結(jié)果
10 9 8 7 6 5 4 3 2 1 0

//情況4
fun main() {
    //使用 downTo 表示創(chuàng)建兩端都是閉區(qū)間的降序區(qū)間,每次在跳過3個元素
    for (i in 10 downTo 0 step 3){
        print("$i ")
    }
}
//打印結(jié)果
10 7 4 1 

三、面向?qū)ο缶幊?/h2>

對于面向?qū)ο缶幊痰睦斫猓好嫦驅(qū)ο蟮恼Z言是可以創(chuàng)建類的,類是對事物一種的封裝,例如人,汽車我們都可以把他們封裝成類,類名通常是名詞,類中有自己的字段和函數(shù),字段表示該類擁有的屬性,通常也是名詞,就像人可以擁有姓名和年齡,汽車可以擁有品牌和價格,函數(shù)表示該類擁有那些行為,一般為動詞,就像人需要吃飯睡覺,汽車可以駕駛和保養(yǎng),通過這種類的封裝,我們就可以在適當(dāng)?shù)牡胤絼?chuàng)建這些類,然后調(diào)用他們的字段和函數(shù)來滿足實(shí)際的編程需求,這就是面向?qū)ο缶幊套罨镜乃枷?/p>

1、類與對象

我們使用 AndroidStudio 創(chuàng)建一個 Person 類,在彈出的對話框中輸入 Person ,選擇Class,對話框默認(rèn)情況下自動選中的是創(chuàng)建一個File,F(xiàn)ile 通常是用于編寫 Kotlin 頂層函數(shù)和擴(kuò)展函數(shù)等,如下圖:

image-20210316190225365

1)、當(dāng)我們在類中創(chuàng)建屬性的時候,Kotlin 會自動幫我們創(chuàng)建 get 和 set 方法

2)、Kotlin 中實(shí)例化對象和 Java 類似,但是把 new 關(guān)鍵字給去掉了

3)、一般在類中,我們會用 var 關(guān)鍵字去定義一個屬性,因?yàn)閷傩砸话闶强勺兊?,如果你確定某個屬性不需要改變,則用 val

class Person {
    var name = ""
    var age = 0

    fun sleep(){
        println("$name is sleep, He is $age years old.")
    }
}

fun main() {
    val person = Person()
    person.name = "erdai"
    person.age = 20
    person.sleep()
}
//打印結(jié)果
erdai is sleep, He is 20 years old.

2、繼承與構(gòu)造函數(shù)

繼承

1)、Kotlin 中規(guī)定,如果要聲明一個非抽象類可繼承,必須加上 open 關(guān)鍵字,否則不可繼承,這點(diǎn)和 Java 中不同,Java 中的類默認(rèn)是可被繼承的,Effective Java 這本書中提到:如果一個類不是專門為繼承而設(shè)計的,那么就應(yīng)該主動將它加上 final 聲明,禁止他可以被繼承

2)、Kotlin中的繼承和實(shí)現(xiàn)都是用 : 表示

//聲明 Person 類可以被繼承
open class Person {
    var name = ""
    var age = 0
  
    fun sleep() {
        println("$name is sleep, He is $age years old.")
    }
}

//定義 Student 繼承 Person 類
//為啥 Person 后面會有一個括號呢?因?yàn)樽宇惖臉?gòu)造函數(shù)必須調(diào)用父類中的構(gòu)造函數(shù),在 Java 中,子類的構(gòu)造函數(shù)會隱式的去調(diào)用
class Student : Person(){
    
}

構(gòu)造函數(shù)

1)、主構(gòu)造函數(shù)的特點(diǎn)是沒有函數(shù)體,直接跟在類名的后面即可,如果需要在主構(gòu)造函數(shù)里面做邏輯,復(fù)寫 init 函數(shù)即可

2)、主構(gòu)造函數(shù)中聲明成 val 或者 var 的參數(shù)將自動成為該類的字段,如果不加,那么該字段的作用域僅限定在主構(gòu)造函數(shù)中

3)、次構(gòu)造函數(shù)是通過 constructor 關(guān)鍵字來定義的

4)、當(dāng)一個類沒有顯示的定義主構(gòu)造函數(shù),但是定義了次構(gòu)造函數(shù)時,那么被繼承的類后面不需要加 ()

//定義 Student 類,定義主構(gòu)造函數(shù),定義屬性 sno 和 grade, 繼承 Person 類
class Student(var sno: String, var grade: Int) : Person() {
    //做一些初始化的邏輯
    init {
        name = "erdai"
        age = 20
    }
    
    //聲明帶一個參數(shù)的次構(gòu)造函數(shù)
    constructor(sno: String): this(sno,8){

    }

    //聲明一個無參的次構(gòu)造函數(shù)
    constructor(): this("123",7){

    }

    fun printInfo(){
        println("I am $name, $age yeas old, sno: $sno, grade: $grade")
    }
}

fun main() {
    val student1 = Student()
    val student2 = Student("456")
    val student3 = Student("789",9)
    student1.printInfo()
    student2.printInfo()
    student3.printInfo()
}
//打印結(jié)果
I am erdai, 20 yeas old, sno: 123, grade: 7
I am erdai, 20 yeas old, sno: 456, grade: 8
I am erdai, 20 yeas old, sno: 789, grade: 9

//一種特殊情況:當(dāng)一個類沒有顯示的定義主構(gòu)造函數(shù),但是定義了次構(gòu)造函數(shù)時,那么被繼承的類后面不需要加 ()
class Student : Person{
    constructor() : super(){
        
    }
}

3、接口

1)、Kotlin 和 Java 中定義接口沒有任何區(qū)別

//定義接口中的一系列的抽象行為 Kotlin 中增加了接口中定義的函數(shù)可以有默認(rèn)實(shí)現(xiàn),其實(shí) Java 在 JDK1.8 之后也開始支持這個功能
interface Study{
     fun readBooks()
     //如果子類沒有重寫這個方法,那么就會調(diào)用這個方法的默認(rèn)實(shí)現(xiàn)
     fun doHomework(){
         println("do homework default implementation")
     }
}

//定義一個可被繼承的 People 類,有 name 和 age 兩個屬性
open class People(val name: String,val age: Int){

}

//定義一個 Student 類,繼承 People 類,實(shí)現(xiàn) Study 接口
class Student(name: String, age: Int) : People(name, age),Study{
    override fun readBooks() {
        println("$name is read book")
    }
}

//定義的一個方法 然后在main函數(shù)調(diào)用
fun doStudy(study: Study){
    study.readBooks()
    study.doHomework()
}

//main函數(shù)調(diào)用
fun main(){
    val student = Student("erdai",20)
    //這里student實(shí)現(xiàn)了Study接口,這種叫做面向接口編程,也可以稱為多態(tài)
    doStydy(student)
}

//打印結(jié)果
erdai is read book
do homework default implementation

4、函數(shù)的可見性修飾符

修飾符 Java Kotlin
public 所有類可見 所有類可見(默認(rèn))
private 當(dāng)前類可見 當(dāng)前類可見
protected 當(dāng)前類,子類,同一個包下的可見 當(dāng)前類和子類可見
default 同一個包下的可見(默認(rèn))
internal 同一個模塊中的類可見

5、數(shù)據(jù)類與單例類

數(shù)據(jù)類

1)、在 Java 中,數(shù)據(jù)類通常需要重寫 equals( ),hashCode( ),toString( ) 這幾個方法,其中 equals( ) 方法用于判斷兩個數(shù)據(jù)類是否相等。hashCode( ) 方法作為 equals( ) 的配套方法,也需要一起重寫,否則會導(dǎo)致 hash 相關(guān)的系統(tǒng)類無法正常工作,toString( ) 方法則用于提供更清晰的輸入日志,否則一個數(shù)據(jù)類默認(rèn)打印出來的是一行內(nèi)存地址

2)、在 Kotlin 中,我們只需要使用 data 關(guān)鍵字去修飾一個類,Kotlin 就會自動幫我們生成 Java 需要重寫的那些方法

//在 Java 中,我們會這么寫
public class Cellphone {
    
    String brand;
    double price;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Cellphone cellphone = (Cellphone) o;
        return Double.compare(cellphone.price, price) == 0 &&
                Objects.equals(brand, cellphone.brand);
    }

    @Override
    public int hashCode() {
        return Objects.hash(brand, price);
    }

    @Override
    public String toString() {
        return "Cellphone{" +
                "brand='" + brand + '\'' +
                ", price=" + price +
                '}';
    }
}

//在 Kotlin 中,你會發(fā)現(xiàn)是如此的簡潔
data class Cellphone(val brand: String, val price: Double)

單例類

1)、Kotlin 中,我們只需要使用 object 關(guān)鍵字去替換 class 關(guān)鍵字就可以去定義一個單例類了

2)、調(diào)用單例類中的方法也比較簡單,直接使用類名 . 上方法就可以了,類似于 Java 中的靜態(tài)方法調(diào)用方式

//java中單例 懶漢式
public class Singleton{
    private static Singleton instance;
  
    public synchronized static Singleton getInstace() {
            if(instance == null){
                instance = new Singleton();
          }
            return instance;
    }
  
    public void singleonTest(){
        System.out.println("singletonTest in Java is called.");
    }
}

//Kotlin中的單例
object Singleton{
    fun singletonTest(){
        println("singletonTest in Kotlin is called.")
    }
}

fun main() {
    Singleton.singletonTest()
}
//打印結(jié)果
singletonTest in Kotlin is called.

Lambda 編程

Kotlin 從第一個版本就開始支持了 Lambda 編程,并且 Kotlin 中的 Lambda 表達(dá)式極為強(qiáng)大,本章我們學(xué)習(xí) Lambda 編程的一些基礎(chǔ)知識:

1)、簡單來說,Lambda 就是一段可以作為參數(shù)傳遞的代碼,它可以作為函數(shù)的參數(shù),返回值,同時也可以賦值給一個變量

2)、Lambda 完整的表達(dá)式的語法結(jié)構(gòu):{ 參數(shù)名1:參數(shù)類型,參數(shù)名2:參數(shù)類型 -> 函數(shù)體 }

3)、很多時候,我們會使用簡化形式的語法結(jié)構(gòu),直接就是一個函數(shù)體:{函數(shù)體},這種情況是當(dāng) Lambda 表達(dá)式的參數(shù)列表中只有一個參數(shù)的時候,我們可以把參數(shù)給省略,默認(rèn)會有個 it 參數(shù)

4)、Kotlin 中規(guī)定,當(dāng) Lambda 表達(dá)式作為函數(shù)的最后一個參數(shù)的時候,我們可以把 Lambda 表達(dá)式移到函數(shù)括號的外面

5)、Kotlin 中規(guī)定,當(dāng) Lambda 表達(dá)式是函數(shù)的唯一參數(shù)的時候,函數(shù)的括號可以省略

1、集合的創(chuàng)建和遍歷

1)、不可變集合:在集合初始化之后,我們不能對其進(jìn)行增刪改操作

2)、可變集合:在集合初始化之后,我們還能對其進(jìn)行增刪改操作

不可變集合 可變集合
listOf mutableListOf
setOf mutableSetOf
mapOf mutableMapOf
//List 集合 
//定義一個不可變 List 集合
val list1 = listOf("Apple","Banana","Orange","Pear","Grape")
//定義一個可變 List 集合
val list2 = mutableListOf("Apple","Banana","Orange","Pear","Grape")
//添加元素
list2.add("Watermelon")
for (i in list2) {
    print("$i ")
}
//打印結(jié)果
Apple Banana Orange Pear Grape Watermelon

//Set 集合和 List 集合用法完全一樣
//定義一個不可變 Set 集合
val set1 = setOf("Apple","Banana","Orange","Pear","Grape")
//定義一個可變 Set 集合
val set2 = mutableSetOf("Apple","Banana","Orange","Pear","Grape")
//添加元素
set2.add("Watermelon")
for (i in set2) {
    print("$i ")
}
//打印結(jié)果
Apple Banana Orange Pear Grape Watermelon

//Map 集合
//定義一個不可變 Map 集合
val map1 = mapOf("Apple" to 1,"Banana" to 2,"Orange" to 3, "Pear" to 4,"Grape" to 5)
//定義一個可變 Map 集合
val map2 = mutableMapOf("Apple" to 1,"Banana" to 2,"Orange" to 3, "Pear" to 4,"Grape" to 5)
//當(dāng)前 key 存在則修改元素,不存在則添加元素
map2["Watermelon"] = 6
for ((key,value) in map2) {
    print("$key: $value ")
}
//打印結(jié)果
Apple: 1 Banana: 2 Orange: 3 Pear: 4 Grape: 5 Watermelon: 6 

2、集合的函數(shù)式 API

//定義一個不可變 List 集合
val list1 = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
//現(xiàn)在我想打印集合中英文名字最長的字符串,我們可以這么做
//方式1
var maxLengthFruit = ""
for (fruit in list1) {
    if(fruit.length > maxLengthFruit.length){
        maxLengthFruit = fruit
    }
}
print(maxLengthFruit)
//打印結(jié)果
Watermelon

//但是如果使用函數(shù)式 Api 將會變得更加簡單, maxBy 函數(shù)會根據(jù)你的條件遍歷得到符合條件的最大值
//方式2
val maxLengthFruit = list1.maxBy {
    it.length
}
print(maxLengthFruit)
//打印結(jié)果
Watermelon

//通過 maxBy 函數(shù)結(jié)合 Lambda 表達(dá)式語法結(jié)構(gòu),我們來剖析方式2這種寫法的原理, 如下所示
//1
val list1 = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
val lambda = {fruit: String -> fruit.length}
//maxBy 函數(shù)實(shí)際上接收的是一個函數(shù)類型的參數(shù),后續(xù)講高階函數(shù)的時候會講到,也就是我們這里可以傳入一個 Lambda 表達(dá)式
val maxLengthFruit = list1.maxBy(lambda)

//2 替換 lambda
val maxLengthFruit = list1.maxBy({fruit: String -> fruit.length})

//3 Kotlin 中規(guī)定,當(dāng) Lambda 表達(dá)式作為函數(shù)的最后一個參數(shù)的時候,我們可以把 Lambda 表達(dá)式移到函數(shù)括號的外面
val maxLengthFruit = list1.maxBy(){fruit: String -> fruit.length}

//4 Kotlin 中規(guī)定,當(dāng) Lambda 表達(dá)式是函數(shù)的唯一參數(shù)的時候,函數(shù)的括號可以省略
val maxLengthFruit = list1.maxBy{fruit: String -> fruit.length}

//5 當(dāng) Lambda 表達(dá)式的參數(shù)列表中只有一個參數(shù)的時候,我們可以把參數(shù)給省略,默認(rèn)會有個 it 參數(shù)
val maxLengthFruit = list1.maxBy{ it.length }

//經(jīng)過上面 1->2->3->4->5 這幾個步驟,我們最終得到了 5 的這種寫法

集合中還有很多這樣的函數(shù)式 Api,下面我們通過 list 集合來實(shí)踐一下其他的一些函數(shù)式 Api:

val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
//1
//通過 map 操作,把一個元素映射成一個新的元素
val newList = list.map{
    it.toUpperCase()
}
for (s in newList) {
    print("$s ")
}
//打印結(jié)果
APPLE BANANA ORANGE PEAR GRAPE WATERMELON 

//2
//通過 filter 篩選操作,篩選長度小于等于5的字符串
val newList = list.filter {
    it.length <= 5
}
for (s in newList) {
    print("$s ")
}
//打印結(jié)果
Apple Pear Grape

3、Java 函數(shù)式 API 的使用

1)、Kotlin 中調(diào)用 Java 方法也可以使用函數(shù)式 Api ,但必須滿足兩個條件:1、得是用 Java 編寫的接口 2、接口中只有一個待實(shí)現(xiàn)的方法

2)、Kotlin 中寫匿名內(nèi)部類和 Java 有一點(diǎn)區(qū)別,Kotlin 中因?yàn)閽仐壛?new 關(guān)鍵字,改用 object 關(guān)鍵字就可以了

//java 中的匿名內(nèi)部類
new Thread(new Runnable() {
     @Override
     public void run() {

     }
}).start();

//Kotlin 中可以這么寫
Thread(object : Runnable{
    override fun run() {
            
    }
}).start()

/**
 * 我們接著來簡化 Kotlin 中的寫法
 * 因?yàn)?Runnable 類中只有一個待實(shí)現(xiàn)方法,即使這里沒有顯示的重寫 run() 方法,
 * Kotlin 也能明白后面的 Lambda 表達(dá)式就是要在 run() 方法中實(shí)現(xiàn)的內(nèi)容
 */
Thread(Runnable{
  
}).start()

//因?yàn)槭菃纬橄蠓椒ń涌?,我們可以將接口名進(jìn)行省略
Thread({
  
}).start()

//當(dāng) Lambda 表達(dá)式作為函數(shù)的最后一個參數(shù)的時候,我們可以把 Lambda 表達(dá)式移到函數(shù)括號的外面
Thread(){
  
}.start()

//當(dāng) Lambda 表達(dá)式是函數(shù)的唯一參數(shù)的時候,函數(shù)的括號可以省略
Thread{
  
}.start()

四、空指針檢查

Android 系統(tǒng)上奔潰最高的異常就是空指針異常(NullPointerException),造成這種現(xiàn)象的主要原因是因?yàn)榭罩羔樖且环N不受編程語言檢查的運(yùn)行時異常,只能由程序員主動通過邏輯判斷來避免,但即使在出色的程序員,也不可能將所有潛在的空指針異常都考慮到。但是這種情況在 Kotlin 上得到了很好的解決,Kotlin 把空指針異常提前到了編譯期去檢查,這樣的做法幾乎杜絕了空指針異常,但是這樣子會導(dǎo)致代碼變得比較難寫,不過 Kotlin 提供了一系列的輔助工具,讓我們能輕松的處理各種判空的情況,下面我們就來學(xué)習(xí)它

1、可空類型系統(tǒng)和判空輔助工具

1)、在類型后面加上 ? ,表示可空類型,Kotlin 默認(rèn)所有的參數(shù)和變量不可為空

2)、在對象調(diào)用的時候,使用 ?. 操作符,它表示如果當(dāng)前對象不為空則調(diào)用,為空則什么都不做

3)、?: 操作符表示如果左邊的結(jié)果不為空,返回左邊的結(jié)果,否則返回右邊的結(jié)果

4)、在對象后面加 !! 操作符表示告訴Kotlin我這里一定不會為空,你不用進(jìn)行檢測了,如果為空,則拋出空指針異常

5)、let 函數(shù),提供函數(shù)式 Api,并把當(dāng)前調(diào)用的對象當(dāng)作參數(shù)傳遞到 Lambda 表達(dá)式中

情況1: 在類型后面加上 ? ,表示可空類型,Kotlin 默認(rèn)所有的參數(shù)和變量不可為空

interface Study{
    fun readBooks()
    fun domeHomework(){
        println("do homework default implementation")
    }
}

fun doStudy(study: Study){
    study.readBooks()
    study.domeHomework()
}

上面這段代碼是不會出現(xiàn)空指針異常的,如果你嘗試向 doStudy 這個方法傳遞一個 null ,編譯器會報錯:

image-20210317101826003

因此這種情況我們就可以使用可空類型,把 Study 改成 Study?,如下圖:

image-20210317104058401

你會發(fā)現(xiàn)雖然調(diào)用 doStudy 方法不報錯了,但是 doStudy 內(nèi)部的調(diào)用卻報錯了,因?yàn)榇藭r doStudy 接受一個可空的類型參數(shù),可能會造成內(nèi)部的空指針, Kotlin 編譯器不允許這種情況存在,那么我們進(jìn)行如下改造就好了:

fun doStudy(study: Study?){
    if(study != null){
        study.readBooks()
        study.domeHomework()
    }
}

情況2: 在對象調(diào)用的時候,使用 ?. 操作符,它表示如果當(dāng)前對象不為空則調(diào)用,為空則什么都不做

針對上面的 doStudy 方法,我們還可以這么做:

fun doStudy(study: Study?){
    study?.readBooks()
    study?.domeHomework()
}

情況3: ?: 操作符表示如果左邊的結(jié)果不為空,返回左邊的結(jié)果,否則返回右邊的結(jié)果

//平時我們可能寫這樣的代碼
val a = if (b != null) {
    b
} else {
    c
}

//使用 ?: 操作符可以簡化成這樣
val a = b ?: c

情況4: 在對象后面加 !! 操作符表示告訴Kotlin我這里一定不會為空,你不用進(jìn)行檢測了,如果為空,則拋出空指針異常

//下面這段代碼編譯通不過,因?yàn)?printName 方法里的 name 并不知道你在外面做了非空判斷
val name: String? = "erdai"

fun printName(){
    val upperCaseName = name.toUpperCase()
    print(upperCaseName)
}

fun main() {
    if(name != null){
       printName()
    }
}

//因此在上面這種明確不會為空的情況下,我們可以使用 !! 操作符,改造一下 printName 方法
//同時要提醒一下自己,是否存在更好的實(shí)現(xiàn)方式,因?yàn)槭褂眠@種操作符,還是會存在潛在的空指針異常
fun printName(){
    val upperCaseName = name!!.toUpperCase()
    print(upperCaseName)
}
//打印結(jié)果
ERDAI

情況5: let 函數(shù),提供函數(shù)式 Api,并把當(dāng)前調(diào)用的對象當(dāng)作參數(shù)傳遞到 Lambda 表達(dá)式中

//這是我們情況2 實(shí)現(xiàn)的方式,但是如果這種調(diào)用方式一多,會顯得特別啰嗦,例如:
fun doStudy(study: Study?){
    study?.readBooks()
    study?.domeHomework()
    study?.a()
    study?.b()
}
//上面這種情況等同于如下代碼:
fun doStudy(study: Study?){
    if(study != null){
        study?.readBooks()
    }
  
    if(study != null){
        study?.domeHomework()
    }
  
    if(study != null){
        study?.a()
    }
  
    if(study != null){
        study?.b()
    }
}

//這個時候我們就可以使用 let 函數(shù)來操作了
fun doStudy(study: Study?){
    study?.let{
      it.readBooks()
      it.domeHomework()
      it.a()
      it.b()
    }
}

五、Kotlin中的小魔術(shù)

1、字符串的內(nèi)嵌表達(dá)式

1)、Kotlin 中,字符串里面可以使用 ${} 引用變量值和表達(dá)式,當(dāng) {} 里面只有一個變量,非表達(dá)式時,{}也可以去掉

fun main() {
    val a = "erdai"
    val b = "666"
    print("$a ${a + b}")
}
//打印結(jié)果
erdai erdai666

2、函數(shù)的參數(shù)默認(rèn)值

1)、定義一個函數(shù)時,我們可以給函數(shù)的參數(shù)添加一個默認(rèn)值,這樣子我們就不需要去傳那個參數(shù)

2)、在我們調(diào)用一個函數(shù)時,我們可以使用 key value 的形式來傳參

//情況1:定義一個函數(shù)時,我們可以給函數(shù)的參數(shù)添加一個默認(rèn)值,這樣子我們就不需要去傳那個參數(shù)
fun printParams(name: String,age: Int = 20){
    print("I am $name, $age years old.")
}

fun main() {
    printParams("erdai")
}
//打印結(jié)果
I am erdai, 20 years old.

//當(dāng)然我們也可以選擇覆蓋默認(rèn)參數(shù)
fun main() {
    printParams("erdai",25)
}
//打印結(jié)果
I am erdai, 25 years old.

//情況2:在我們調(diào)用一個函數(shù)時,我們可以使用 key value 的形式來傳參
fun main() {
    //注意 printParams 方法的一個參數(shù)是 name ,第二個才是 age, 但是通過 key value 的形式來傳參就不會出現(xiàn)參數(shù)順序問題
    printParams(age = 19,name = "erdai666")
}
//打印結(jié)果
I am erdai666, 19 years old.

小技巧:我們可以通過函數(shù)的參數(shù)默認(rèn)值來代替次構(gòu)造函數(shù),使用主構(gòu)造函數(shù)就好了

六、標(biāo)準(zhǔn)函數(shù)和靜態(tài)方法

1、標(biāo)準(zhǔn)函數(shù)let,also,with,run 和 apply

1)、let 函數(shù),必須讓某個對象調(diào)用,接收一個 Lambda 表達(dá)式參數(shù),Lambda 表達(dá)式中的參數(shù)為當(dāng)前調(diào)用者,且最后一行代碼作為返回值

2)、also 函數(shù),必須讓某個對象調(diào)用,接收一個 Lambda 表達(dá)式參數(shù),Lambda 表達(dá)式中的參數(shù)為當(dāng)前調(diào)用者,無法指定返回值,這個函數(shù)返回的是當(dāng)前調(diào)用對象本身

3)、with 函數(shù),接收兩個參數(shù),第一個為任意類型參數(shù),第二個為 Lambda 表達(dá)式參數(shù),Lambda 表達(dá)式中擁有第一個參數(shù)的上下文 this ,且最后一行代碼作為返回值

4)、run 函數(shù),必須讓某個對象調(diào)用,接收一個 Lambda 表達(dá)式參數(shù),Lambda 表達(dá)式中擁有當(dāng)前調(diào)用對象的上下文 this ,且最后一行代碼作為返回值

5)、apply 函數(shù),必須讓某個對象調(diào)用,接收一個 Lambda 表達(dá)式參數(shù),Lambda 表達(dá)式中擁有當(dāng)前調(diào)用對象的上下文 this ,無法指定返回值,這個函數(shù)返回的是當(dāng)前調(diào)用對象本身

注意:在Lambda 表達(dá)式中,擁有對象的上下文 this,和擁有該對象是一樣的,只不過 this 可省略,而擁有該對象我們可以自定義參數(shù)名,如果不寫該參數(shù),默認(rèn)會有個 it 參數(shù)

下面通過代碼來感受一下:

/**
 * 情況1:let 函數(shù)
 * 1、創(chuàng)建一個 StringBuilder 對象調(diào)用 let 函數(shù),Lambda 表達(dá)式中的參數(shù)為 StringBuilder 對象
 * 2、當(dāng) Lambda 表達(dá)式中只有一個參數(shù)的時候可省略,默認(rèn)會有個 it 的參數(shù),返回值即為 Lambda 表達(dá)式中最后一行代碼
 */
fun main() {
    val name = "erdai"
    val age = 20
    val returnValue = StringBuilder().let {
        it.append(name).append(" ").append(age)
    }
    println(returnValue)
}
//打印結(jié)果
erdai 20

/**
 * 情況2:also 函數(shù)
 * 1、創(chuàng)建一個 StringBuilder 對象調(diào)用 also 函數(shù),Lambda 表達(dá)式中的參數(shù)為 StringBuilder 對象
 * 2、當(dāng) Lambda 表達(dá)式中只有一個參數(shù)的時候可省略,默認(rèn)會有個 it 的參數(shù),無法指定返回值,返回調(diào)用對象本身
 */
fun main() {
    val name = "erdai"
    val age = 20
    val stringBuilder = StringBuilder().also {
        it.append(name).append(" ").append(age)
    }
    println(stringBuilder.toString())
}
//打印結(jié)果
erdai 20

/**
 * 情況3:with 函數(shù)
 * 1、接收兩個參數(shù),第一個參數(shù)為 StringBuilder 對象,第二個參數(shù)為 Lambda 表達(dá)式,
 * 2、Lambda 表達(dá)式中擁有 StringBuilder 對象的上下文 this, 返回值即為 Lambda 表達(dá)式中的最后一行代碼
 */
fun main() {
    val name = "erdai"
    val age = 20
    val returnValue = with(StringBuilder()) {
        append(name).append(" ").append(age)
    }
    println(returnValue)
}
//打印結(jié)果
erdai 20

/**
 * 情況4:run 函數(shù)
 * 1、創(chuàng)建一個 StringBuilder 對象調(diào)用 also 函數(shù),Lambda 表達(dá)式中擁有 StringBuilder 對象的上下文 this
 * 2、返回值即為 Lambda 表達(dá)式中的最后一行代碼
 */
fun main() {
    val name = "erdai"
    val age = 20

    val returnValue = StringBuilder().run {
        append(name).append(" ").append(age)
    }
    println(returnValue)
}
//打印結(jié)果
erdai 20

/**
 * 情況5:apply 函數(shù)
 * 1、創(chuàng)建一個 StringBuilder 對象調(diào)用 apply 函數(shù),Lambda 表達(dá)式中擁有 StringBuilder 對象的上下文 this
 * 2、無法指定返回值,返回調(diào)用對象本身
 */
fun main() {
    val name = "erdai"
    val age = 20

    val stringBuilder = StringBuilder().apply {
        append(name).append(" ").append(age)
    }
    println(stringBuilder.toString())
}
//打印結(jié)果
erdai 20

其實(shí)上面 5 個標(biāo)準(zhǔn)函數(shù)有很多相似的地方,我們需搞清楚它們差異之處,下面我們用一個圖表來總結(jié)一下:

標(biāo)準(zhǔn)函數(shù) 函數(shù)參數(shù) 是否是擴(kuò)展函數(shù) 返回值
T.let it 最后一行代碼
T.also it 對象本身
with this 最后一行代碼
T.run this 最后一行代碼
T.apply this 對象本身

2、定義靜態(tài)方法

Kotlin 中沒有直接提供定義為靜態(tài)方法的關(guān)鍵字,但是提供了一些類似的語法特性來支持靜態(tài)方法調(diào)用的寫法

1)、使用 companion object 為一個類創(chuàng)建一個伴生類,然后調(diào)用這個伴生類的方法,這個方法不叫靜態(tài)方法,但是可以當(dāng)作靜態(tài)方法調(diào)用

2)、使用 object 關(guān)鍵字定義一個單例類,通過單例類,去調(diào)用方法,這種方法也不叫靜態(tài)方法,但是可以當(dāng)作靜態(tài)方法調(diào)用

3)、如果想定義真正的靜態(tài)方法,Kotlin 中也提供了兩種方式:1、使用 @JvmStatic 注解,且注解只能加在伴生類和單例類上的方法上面 2、定義頂層方法

4)、頂層方法就是不定義在任何類中的方法,頂層方法在任何位置都能被調(diào)用到,Kotlin 編譯器會把所有的頂層方法編譯成靜態(tài)方法

5)、如果在 Java 中調(diào)用頂層方法,Java 默認(rèn)是沒有頂層方法的概念的,Kotlin 編譯器會生成一個我們定義這個文件的 Java 類,例如我在 Kotlin 中的 Util.kt 文件中定義了一個頂層方法,那么就會生成一個 UtilKt 的 Java 類供在 Java 中調(diào)用

6)、在 Kotlin 中比較常用的是 單例,伴生類和頂層方法,@JvmStatic 注解用的比較少

//在 Java 中我們可以這樣定義一個靜態(tài)方法
public class Util {
  
    public static void doAction(){
        System.out.println("do something");
    }
}

//Kotlin 中類似這樣靜態(tài)調(diào)用多種多樣
//情況1:使用 companion object 為一個類創(chuàng)建一個伴生類
fun main() {
   Util.doAction()
}

class Util{
    companion object{
        fun doAction(){
            println("do something")
        }
    }
}
//打印結(jié)果
do something

//情況2:使用 object 關(guān)鍵字定義一個單例類
fun main() {
   Util.doAction()
}

object Util {

    fun doAction() {
        println("do something")
    }
}
//打印結(jié)果
do something

//情況3:1、使用 @JvmStatic 注解 2、定義頂層方法
//1
//單例類
object Util {
  
    @JvmStatic
    fun doAction() {
        println("do something")
    }
}

//伴生類
class Util {
  
    companion object{
        fun doAction() {
            println("do something")
        }
    }
}

//2 使用 AndroidStudio 新建一個文件,在彈框中選擇 File 即可,我們在這個 File 中編寫一個頂層方法
//頂層方法在任何位置都能調(diào)用到
fun doAction(){
    println("do something")
}

上述代碼大家可以將 Kotlin 文件轉(zhuǎn)換成 Java 文件看一下,你就會發(fā)現(xiàn)定義真正的靜態(tài)方法和非靜態(tài)方法的區(qū)別

七、延遲初始化和密封類

1、對變量延遲初始化

1)、使用 lateinit 關(guān)鍵字對一個變量延遲初始化

使用 lateinit 關(guān)鍵字注意事項(xiàng):

1、只能作用于 var 屬性,且該屬性沒有自定義 get 和 set 方法

2、該屬性必須是非空類型,且不能是原生類型

2)、當(dāng)你對一個變量使用了 lateinit 關(guān)鍵字,Kotlin 編譯器就不會在去檢查這個變量是否會為空了,此時你要確保它在被調(diào)用之前已經(jīng)初始化了,否則程序運(yùn)行的時候會報錯,可以使用 ::object.isInitialized 這種固定的語法結(jié)構(gòu)判斷變量是否已經(jīng)初始化

3)、使用 by lazy 對一個變量延遲初始化

使用 by lazy 注意事項(xiàng):

1、只能作用于 val 屬性

//情況1:使用 lateinit 關(guān)鍵字對一個變量延遲初始化
lateinit var name: String

fun main() {
   name = "erdai"
   println(name)
}
//打印結(jié)果
erdai

//情況2: 使用 ::object.isInitialized 這種固定的語法結(jié)構(gòu)判斷變量是否已經(jīng)初始化
lateinit var name: String

fun main() {
    if(::name.isInitialized){
        println(name)
    }else{
        println("name not been initialized")
    }
}
//打印結(jié)果
name not been initialized

//情況3: 使用 by lazy 對一個變量延遲初始化
//特點(diǎn):該屬性調(diào)用的時候才會初始化,且 lazy 后面的 Lambda 表達(dá)式只會執(zhí)行一次
val name: String by lazy {
    "erdai"
}

fun main() {
    println(name)
}
//打印結(jié)果
erdai

2、使用密封類優(yōu)化代碼

密封類能使我們寫出更加規(guī)范和安全的代碼

1)、使用 sealed class 定義一個密封類

2)、密封類及其子類,只能定義在同一個文件的頂層位置

3)、密封類可被繼承

4)、當(dāng)我們使用條件語句的時候,需要實(shí)現(xiàn)密封類所有子類的情況,避免寫出永遠(yuǎn)不會執(zhí)行的代碼

//在使用密封類之前我們可能會寫出這種代碼
interface Result
class Success : Result
class Failure : Result
/**
 * 那么此時如果我新增一個類實(shí)現(xiàn) Result 接口,編譯器并不會提示我們?nèi)バ略鲂碌臈l件分支
 * 如果我們沒有新增相應(yīng)的條件分支,那么就會出現(xiàn)執(zhí)行 else 的情況
 * 其實(shí)這個 else 就是一個無用分支,這僅僅是為了滿足編譯器的要求
 */
fun getResultMsg(result: Result) = when (result){
    is Success -> "Success"
    is Failure -> "Failure"
    else -> throw RuntimeException()
}

//在使用密封類之后
sealed class Result
class Success : Result()
class Failure : Result()
/**
 * 此時我們就避免了寫 else 分支,這個時候如果我新增一個類實(shí)現(xiàn) Result 密封類
 * 編譯器就會提示異常,需要 when 去新增相應(yīng)的條件分支
 */
fun getResultMsg(result: Result) = when (result){
    is Success -> "Success"
    is Failure -> "Failure"
}

八、擴(kuò)展函數(shù)和運(yùn)算符

1、大有用途的擴(kuò)展函數(shù)

擴(kuò)展函數(shù)允許我們?nèi)U(kuò)展一個類的函數(shù),這種特性是 Java 中所沒有的

1)、擴(kuò)展函數(shù)的語法結(jié)構(gòu)如下:

fun ClassName.methodName(params1: Int, params2: Int) : Int{
  
}

相比于普通的函數(shù),擴(kuò)展函數(shù)只需要在函數(shù)前面加上一個 ClassName. 的語法結(jié)構(gòu),就表示把該函數(shù)添加到指定的類中

2)、一般我們要定義哪個類的擴(kuò)展函數(shù),我們就定義一個同名的 Kotlin 文件,便于后續(xù)查找,雖然說也可以定義在任何一個類中,但是更推薦將它定義成頂層方法,這樣可以讓擴(kuò)展方法擁有全局的訪問域

3)、擴(kuò)展函數(shù)默認(rèn)擁有這個類的上下文環(huán)境

例如我們現(xiàn)在要給 String 這個類擴(kuò)展一個 printString 方法,我們就可以新建一個 String.kt 的文件,然后在這個文件下面編寫擴(kuò)展函數(shù):

fun String.printString(){
    println(this)
}

fun main() {
    val name = "erdai"
    name.printString()
}
//打印結(jié)果
erdai

2、有趣的運(yùn)算符重載

Kotlin 的運(yùn)算符重載允許我們讓任意兩個對象進(jìn)行相加,或者是進(jìn)行其他更多的運(yùn)算操作

1)運(yùn)算符重載使用的是 operator 關(guān)鍵字,我們只需要在指定函數(shù)前面加上 operator 關(guān)鍵字,就可以實(shí)現(xiàn)運(yùn)算符重載的功能了。

上面所說的指定函數(shù)有下面這些,如圖:

image-20210319102601471

2)例如我現(xiàn)在要實(shí)現(xiàn)兩個對象相加的功能,它的語法結(jié)構(gòu)如下:

class Obj {
   operator fun plus(obj: Obj): Obj{
        //do something
   }
}

下面我們來實(shí)現(xiàn)一個金錢相加的例子:

class Money(val value: Int) {

    //實(shí)現(xiàn)運(yùn)算符重載 Money + Money
    operator fun plus(money: Money): Money {
        val sum = value + money.value
        return Money(sum)
    }

    //實(shí)現(xiàn)運(yùn)算符重載 Money + Int
    operator fun plus(money: Int): Money{
        val sum = value + money
        return Money(sum)
    }
}

fun main() {
    val money1 = Money(15)
    val money2 = Money(20)
    val money3 = money1 + money2
    val money4 = money3 + 15
    println(money3.value)
    print(money4.value)
}

//打印結(jié)果
35
50

九、高階函數(shù)詳解

高階函數(shù)和 Lambda 表達(dá)式是密不可分的,在之前的章節(jié),我們學(xué)習(xí)了一些 函數(shù)式 Api 的用法,你會發(fā)現(xiàn),它們都會有一個共同的特點(diǎn):需要傳入一個 Lambda 表達(dá)式作為參數(shù)。像這種接收 Lambda 表達(dá)式的函數(shù)我們就可以稱之為具有函數(shù)式編程風(fēng)格的 Api,而如果你要定義自己的函數(shù)式 Api,那么就需要使用高階函數(shù)來實(shí)現(xiàn)了

1、定義高階函數(shù)

1)高階函數(shù)的定義:一個函數(shù)接收另外一個函數(shù)作為參數(shù),或者返回值,那么就可以稱之為高階函數(shù)

Kotlin 中新增了函數(shù)類型,如果我們將這種函數(shù)類型添加到一個函數(shù)的參數(shù)聲明或者返回值,那么這就是一個高階函數(shù)

2)函數(shù)類型的語法規(guī)則如下

(String,Int) -> Unit
//或者如下
() -> Unit

-> 的左邊聲明函數(shù)接收什么類型的參數(shù),-> 的右邊聲明的是函數(shù)的返回值,現(xiàn)在我們來聲明一個高階函數(shù):

fun example(func: (String,Int) -> Unit) {
    //do something
}

3)高階函數(shù)的調(diào)用,我們只需要在參數(shù)名后面加上一對括號,傳入對應(yīng)類型的參數(shù)即可,例如以上面定義的這個高階函數(shù)為例子:

fun example(func: (String,Int) -> Unit) {
    //函數(shù)類型調(diào)用
    func("erdai",666)
}

下面我們就來實(shí)踐一下:

//我們使用高階函數(shù)來獲取兩個數(shù)相加的和
fun numberPlus(num1: Int,num2: Int,func: (Int,Int) -> Int): Int{
    val sum = func(num1,num2)
    return sum
}

fun plus(num1: Int,num2: Int): Int{
    return num1 + num2
}

fun minus(num1: Int,num2: Int): Int{
    return num1 - num2
}

//調(diào)用高階函數(shù)的兩種方式
//方式1:成員引用,使用 ::plus,::minus這種寫法引用一個函數(shù)
fun main() {
    val numberPlus = numberPlus(10, 20, ::plus)
    val numberMinus = numberPlus(10, 20, ::minus)
    println(numberPlus)
    println(numberMinus)
}
//打印結(jié)果
30
-10

//方式2:使用 Lambda 表達(dá)式的寫法
fun main() {
    val numberPlus = numberPlus(10, 20){ num1,num2 ->
        num1 + num2
    }
    val numberMinus = numberPlus(10, 20){ num1,num2 ->
        num1 - num2
    }
    println(numberPlus)
    println(numberMinus)
}
//打印結(jié)果
30
-10

其中使用 Lambda 表達(dá)式的寫法是高階函數(shù)中最普遍的調(diào)用方式

2、內(nèi)聯(lián)函數(shù)的作用

1)內(nèi)聯(lián)函數(shù)可以消除 Lambda 表達(dá)式運(yùn)行時帶來的開銷

Kotlin 代碼最終還是會轉(zhuǎn)換成 Java 字節(jié)碼文件,舉個??:

fun numberPlus(num1: Int,num2: Int,func: (Int,Int) -> Int): Int{
    val sum = func(num1,num2)
    return sum
}

fun main() {
    val num1 = 10
    val num2 = 20
    val numberPlus = numberPlus(num1, num2){ num1,num2 ->
        num1 + num2
    }
}

//上面這些代碼最終轉(zhuǎn)換成 Java 代碼大概會變成這樣:
public static int numberPlus(int num1, int num2, Function operation){
    int sum = (int) operation.invoke(num1,num2);
    return sum;
}

public static void main(){
    int num1 = 10;
    int num2 = 20;
    int sum = numberPlus(num1,num2,new Function(){
        @Override
        public Integer invoke(Integer num1,Integer num2){
            return num1 + num2;
        }
    });
}

可以看到,轉(zhuǎn)換之后,numberPlus 函數(shù)的第三個參數(shù)變成了一個 Function 接口,這是一種 Kotlin 的內(nèi)置接口,里面有一個待實(shí)現(xiàn)的 invoke 函數(shù),而 numberPlus 函數(shù)其實(shí)就是調(diào)用了 Function 接口的 invoke 函數(shù),并把 num1 和 num2 傳了進(jìn)去。之前的 Lambda 表達(dá)式在這里變成了 Function 接口的匿名類實(shí)現(xiàn),這就是 Lambda 表達(dá)式的底層轉(zhuǎn)換邏輯,因此我們每調(diào)用一次 Lambda 表達(dá)式,都會創(chuàng)建一個新的匿名類實(shí)例,這樣就會造成額外的內(nèi)存和性能開銷。但是我們使用內(nèi)聯(lián)函數(shù),就可以很好的去解決這個問題

2)定義高階函數(shù)時加上 inline 關(guān)鍵字修飾,我們就可以把這個函數(shù)稱之為內(nèi)聯(lián)函數(shù)

//定義一個內(nèi)聯(lián)函數(shù)
inline fun numberPlus(num1: Int,num2: Int,func: (Int,Int) -> Int): Int{
    val sum = func(num1,num2)
    return sum
}

那這里我就會有個疑問,為啥內(nèi)聯(lián)函數(shù)能消除 Lambda 表達(dá)式運(yùn)行時帶來的開銷呢?

這個時候我們就需要去剖析一下內(nèi)聯(lián)函數(shù)的工作原理了,如下:

inline fun numberPlus(num1: Int,num2: Int,func: (Int,Int) -> Int): Int{
    val sum = func(num1,num2)
    return sum
}

fun main() {
    val num1 = 10
    val num2 = 20
    val numberPlus = numberPlus(num1, num2){ num1,num2 ->
        num1 + num2
    }
}

第一步替換過程:Kotlin 編譯器會把 Lambda 表達(dá)式中的代碼替換到函數(shù)類型參數(shù)調(diào)用的地方 ,如下圖:

image-20210318103738396

替換后代碼變成了這樣:

inline fun numberPlus(num1: Int,num2: Int,func: (Int,Int) -> Int): Int{
    val sum = num1 + num2
    return sum
}

fun main() {
    val num1 = 10
    val num2 = 20
    val numberPlus = numberPlus(num1, num2);
}

第二步替換過程:Kotlin 編譯器會把內(nèi)聯(lián)函數(shù)中的全部代碼替換到函數(shù)調(diào)用的地方 ,如下圖:

image-20210318104214303

替換后代碼變成了這樣:

fun main() {
    val num1 = 10
    val num2 = 20
    val numberPlus = num1 + num2
}

上述步驟就是內(nèi)聯(lián)函數(shù)的一個工作流程:Kotlin 編譯器會把內(nèi)聯(lián)函數(shù)中的代碼在編譯的時候自動替換到調(diào)用它的地方 ,這樣也就不存在運(yùn)行時的開銷了

3)使用 noinline 關(guān)鍵字修飾的函數(shù)類型參數(shù),表示該函數(shù)類型參數(shù)不需要進(jìn)行內(nèi)聯(lián)

一般使用 noinline 關(guān)鍵字,是在一個內(nèi)聯(lián)函數(shù)中存在多個函數(shù)類型的參數(shù)

//使用內(nèi)聯(lián)函數(shù)定義的高階函數(shù),其里面的函數(shù)類型參數(shù)都會進(jìn)行內(nèi)聯(lián),因此這里使用 noinline 表示我這個函數(shù)類型參數(shù)不需要內(nèi)聯(lián)
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit){
  
}

前面我們講到,使用內(nèi)聯(lián)函數(shù)能減少運(yùn)行時開銷,為啥現(xiàn)在又要出來個 noinline 關(guān)鍵字定義不需要內(nèi)聯(lián)呢?原因如下:

1、內(nèi)聯(lián)函數(shù)在編譯的時候會進(jìn)行代碼替換,因此它沒有真正的參數(shù)屬性,它的函數(shù)類型參數(shù)只能傳遞給另外一個內(nèi)聯(lián)函數(shù),而非內(nèi)聯(lián)函數(shù)的函數(shù)類型參數(shù)可以自由的傳遞給其他任何函數(shù)

2、內(nèi)聯(lián)函數(shù)所引用的 Lambda 表達(dá)式可以使用 return 關(guān)鍵字來進(jìn)行函數(shù)返回,非內(nèi)聯(lián)函數(shù)所引用的 Lambda 表達(dá)式可以使用 return@Method 語法結(jié)構(gòu)來進(jìn)行局部返回

//情況1:非內(nèi)聯(lián)函數(shù)所引用的 Lambda 表達(dá)式可以使用 return 關(guān)鍵字來進(jìn)行局部返回
//定義一個非內(nèi)聯(lián)的高階函數(shù)
fun printString(str: String, block: (String) -> Unit){
    println("printString start...")
    block(str)
    println("printString end...")
}

fun main() {
    println("main start...")
    val str = ""
    printString(str){
        println("lambda start...")
        /**
         * 1,非內(nèi)聯(lián)函數(shù)不能直接使用 return 關(guān)鍵字進(jìn)行局部返回
         * 2,需要使用 return@printString 進(jìn)行局部返回
         */
        if (str.isEmpty())return@printString
        println(it)
        println("lambda end...")
    }
    println("main end...")
}
//打印結(jié)果
main start...
printString start...
lambda start...
printString end...
main end...

//情況2:內(nèi)聯(lián)函數(shù)所引用的 Lambda 表達(dá)式可以使用 return 關(guān)鍵字來進(jìn)行函數(shù)返回
//定義一個非內(nèi)聯(lián)的高階函數(shù)
inline fun printString(str: String, block: (String) -> Unit){
    println("printString start...")
    block(str)
    println("printString end...")
}

fun main() {
    println("main start...")
    val str = ""
    printString(str){
        println("lambda start...")
        if (str.isEmpty())return
        println(it)
        println("lambda end...")
    }
    println("main end...")
}
//因?yàn)閮?nèi)聯(lián)函數(shù)會進(jìn)行代碼替換,因此這個 return 就相當(dāng)于外層函數(shù)調(diào)用的一個返回,如下代碼:
fun main() {
    println("main start...")
    val str = ""
    println("printString start...")
    println("lambda start...")
    if (str.isEmpty())return
    println(str)
    println("lambda end...")
    println("printString end...")
    println("main end...")
}

//打印結(jié)果
main start...
printString start...
lambda start...

4)、使用 crossinline 關(guān)鍵字保證內(nèi)聯(lián)函數(shù)的 Lambda 表達(dá)式中一定不會使用 return 關(guān)鍵字,但是還是可以使用 return@Method 語法結(jié)構(gòu)進(jìn)行局部返回,其他方面和內(nèi)聯(lián)函數(shù)特性一致

舉個使用 crossinline 場景的?? :

image-20210318115342264

上面圖片中的代碼報錯了,編譯器提示我們的大致原因是:這個地方不能使用 inline ,因?yàn)樗赡馨蔷植康?return 返回,添加 crossinline 修飾符去修飾這個函數(shù)類型的參數(shù)。

為啥呢?我們來分析一下:

我們創(chuàng)建了一個 Runnable 對象,在 Runnable 中的 Lambda 表達(dá)式中調(diào)用了函數(shù)類型參數(shù),Lambda 表達(dá)式在編譯的時候會被轉(zhuǎn)換成匿名內(nèi)部類的方式,內(nèi)聯(lián)函數(shù)允許我們在 Lambda 表達(dá)式中使用 return 關(guān)鍵字進(jìn)行函數(shù)返回,但是由于我們是在匿名類中調(diào)用的函數(shù)類型參數(shù),此時是不可能進(jìn)行外層調(diào)用函數(shù)返回的,最多是在匿名函數(shù)中進(jìn)行返回,因此這里就提示了錯誤,知道了原因那我們使用 crossinline 關(guān)鍵字來修改一下

inline fun runRunnable(crossinline block: () -> Unit) {
    println("runRunnable start...")
    val runnable = Runnable {
        block()
    }
    runnable.run()
    println("runRunnable end...")
}


fun main() {
    println("main start...")
    runRunnable {
        println("lambda start...")
        return@runRunnable
        println("lambda end...")
    }
    println("main end...")
}
//打印結(jié)果
main start...
runRunnable start...
lambda start...
runRunnable end...
main end...

十、泛型和委托

1、泛型的基本用法

1)、首先我們解釋下什么是泛型,泛型就是參數(shù)化類型,它允許我們在不指定具體類型的情況下進(jìn)行編程。我們在定義一個類,方法,或者接口的時候,給他們加上一個類型參數(shù),就是為這個類,方法,或者接口添加了一個泛型

//1、定義一個泛型類,在類名后面使用 <T> 這種語法結(jié)構(gòu)就是為這個類定義一個泛型
class MyClass<T>{
    fun method(params: T) {
      
    }
}
//泛型調(diào)用
val myClass = MyClass<Int>()
myClass.method(12)

//2、定義一個泛型方法,在方法名的前面加上 <T> 這種語法結(jié)構(gòu)就是為這個方法定義一個泛型
class MyClass{
    fun <T> method(params: T){

    }
}
//泛型調(diào)用
val myClass = MyClass()
myClass.method<Int>(12)
//根據(jù) Kotlin 類型推導(dǎo)機(jī)制,我們可以把泛型給省略
myClass.method(12)

//3、定義一個泛型接口,在接口名后面加上 <T> 這種語法結(jié)構(gòu)就是為這個接口定義一個泛型
interface MyInterface<T>{
    fun interfaceMethod(params: T)
}

上面的 T 不是固定的,可以是任意單詞和字母,但是定義的泛型盡量做到見名知義

2)、為泛型指定上界,我們可以使用 <T : Class> 這種語法結(jié)構(gòu),如果不指定泛型的上界,默認(rèn)為 Any? 類型

class MyClass{
    //我們指定了泛型的上界為 Number, 那么我們就只能傳入數(shù)字類型的參數(shù)了
    fun <T : Number> method(params: T) {
      
    }
}

2、類委托和委托屬性

委托模式的意義:在于我們大部分方法實(shí)現(xiàn)可以調(diào)用輔助對象去實(shí)現(xiàn),少部分方法的實(shí)現(xiàn)由自己來重寫,甚至加入一些自己獨(dú)有的方法,使我們這個類變成一個全新數(shù)據(jù)結(jié)構(gòu)的類

1)、類委托核心思想就是把一個類的具體實(shí)現(xiàn)委托給另外一個類,使用 by 關(guān)鍵字進(jìn)行委托

//定義一個 MySet 類,它里面的具體實(shí)現(xiàn)都委托給了 HashSet 這個類,這是是類委托
class MySet<T>(val helperSet: HashSet<T>) : Set<T>{

    override val size: Int get() = helperSet.size

    override fun contains(element: T) = helperSet.contains(element)

    override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)

    override fun isEmpty() = helperSet.isEmpty()

    override fun iterator() = helperSet.iterator()
}

/**
 * 如果我們使用 by 關(guān)鍵字,上面的代碼將會變得非常整潔,同時我們可以對某個方法進(jìn)行重寫或者新增方法
 * 那么 MySet 就變成了一個全新的數(shù)據(jù)結(jié)構(gòu)類
 */
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet{
     fun helloWord(){
        println("Hello World")
    }
    
    override fun isEmpty() = false
}

2)、屬性委托的核心思想是將一個屬性的具體實(shí)現(xiàn)委托給另一個類去完成

屬性委托的語法結(jié)構(gòu)如下:

/**
* 使用 by 關(guān)鍵字連接了左邊的 p 屬性和右邊的 Delegate 實(shí)例
* 這種寫法就代表著將 p 屬性的具體實(shí)現(xiàn)委托給了 Delegate 去完成
*/
class MyClass{

   var p by Delegate()
}

/**
* 下面是一個被委托類的代碼實(shí)現(xiàn)模版
* 一、getValue 方法和setValue 方法必須使用 operator 關(guān)鍵字修飾
* 
* 二、getValue 方法主要接收兩個參數(shù):
* 1、第一個參數(shù)表明 Delegate 類的委托功能可以在什么類中使用
* 2、第二個參數(shù) KProperty<*> 是 Kotlin 中的一個屬性操作類,
*    可用于獲取各種屬性的相關(guān)值,<*>這種泛型的寫法類似 Java 的
*    <?>,表示我不關(guān)心泛型的具體類型
*
* 三、setValue 方法也是相似的,接收三個參數(shù):
* 1、前面兩個參數(shù)和 getValue 是一樣的
* 2、第三個參數(shù)表示具體要賦值給委托屬性的值,這個參數(shù)的類型必須和
*    getValue 方法返回值的類型保持一致
*
*
* 一種特殊情況:用 val 定義的變量不需要實(shí)現(xiàn) setValue 方法,因?yàn)?val
*             關(guān)鍵字聲明的屬性只可讀,賦值之后就不能更改了
*/
class Delegate{
   
   var propValue: Any? = null

   operator fun getValue(any: Any?,prop: KProperty<*>): Any?{
       return propValue
   }

   operator fun setValue(any: Any?,prop: KProperty<*>,value: Any?){
       propValue = value
   }
}

十一、使用 infix 函數(shù)構(gòu)建更可讀的語法

infix 函數(shù)語法結(jié)構(gòu)可讀性高,相比于調(diào)用一個函數(shù),它更接近于使用英語 A to B 這樣的語法結(jié)構(gòu)

例如我們調(diào)用一個函數(shù)會使用: A.to(B) 這種結(jié)構(gòu),但是使用 infix 函數(shù)我們可以這么寫:A to B,這種語法我們在講 Map 的時候用過

//定義一個不可變 Map 集合
val map1 = mapOf("Apple" to 1,"Banana" to 2,"Orange" to 3, "Pear" to 4,"Grape" to 5)

1)、在函數(shù)前面加上 infix 關(guān)鍵字,就可以聲明這是一個 infix 函數(shù)

//對 String 增加一個擴(kuò)展的 infix 函數(shù),最終調(diào)用的還是 String 的 startsWith 函數(shù)
infix fun String.beginWith(string: String) = startsWith(string)

fun main() {
    val name = "erdai"
    println(name beginWith "er")
}
//打印結(jié)果
true

我們再來實(shí)現(xiàn)一個初始化 Map 時里面?zhèn)魅?A to B 這種 infix 函數(shù)

//這是 A to B 的源碼實(shí)現(xiàn)
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

//我們仿照它寫一個
public infix fun <A,B> A.with(that: B): Pair<A,B> = Pair(this,that)

fun main() {
    val map = mapOf("Apple" with 1,"Banana" with 2,"Orange" with 3,"Pear" with 4,"Grape" with 5)
}

十二、使用 DSL 構(gòu)建專有的語法結(jié)構(gòu)

1)、DSL 介紹

DSL英文全稱:domain specific language,中文翻譯即領(lǐng)域特定語言,例如:HTML,XML等 DSL 語言

特點(diǎn)

  • 解決特定領(lǐng)域的專有問題
  • 它與系統(tǒng)編程語言走的是兩個極端,系統(tǒng)編程語言是希望解決所有的問題,比如 Java 語言希望能做 Android 開發(fā),又希望能做后臺開發(fā),它具有橫向擴(kuò)展的特性。而 DSL 具有縱向深入解決特定領(lǐng)域?qū)S袉栴}的特性。

總的來說,DSL 的核心思想就是:“求專不求全,解決特定領(lǐng)域的問題”。

2)Kotin DSL

首先介紹一下Gradle:Gradle 是一個開源的自動化構(gòu)建工具,是一種基于 Groovy 或 Kotin 的 DSL。我們的 Android 應(yīng)用就是使用 Gradle 構(gòu)建的,因此后續(xù)寫腳本,寫插件,我們可以使用 Kotlin 去編寫,而且 AndroidStudio 對 Kotlin 的支持很友好,各種提示,寫起來很爽。

對于我們 Android 開發(fā),在 build.gradle 文件里面添加依賴的方式很常見:

dependencies {
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
}

上面這種寫法是一種基于 Groovy 的 DSL,下面我們就使用 Kotlin 來實(shí)現(xiàn)一個類似的 DSL:

class Dependency {

    fun implementation(lib: String){

    }
}

fun dependencies(block: Dependency.() -> Unit){
    val dependency = Dependency()
    dependency.block()
}

fun main() {
    //因?yàn)?Groovy 和 Kotlin 語法不同,因此寫法會有一點(diǎn)區(qū)別
    dependencies {
        implementation ("androidx.core:core-ktx:1.3.2")
        implementation ("androidx.appcompat:appcompat:1.2.0")
    }
}

十三、Java 與 Kotlin 代碼之間的轉(zhuǎn)換

Java 代碼轉(zhuǎn) Kotlin 代碼

方式有2:

1)、直接將 Java 代碼復(fù)制到 Kotlin 文件中,AndroidStudio 會出來提示框詢問你是否轉(zhuǎn)換

2)、打開要轉(zhuǎn)換的 Java 文件,在導(dǎo)航欄點(diǎn)擊 Code -> Convert Java File to Kotlin File

Kotlin 代碼轉(zhuǎn) Java 代碼

打開當(dāng)前需要轉(zhuǎn)換的 Kotlin 文件,在導(dǎo)航欄點(diǎn)擊 Tools -> Kotlin ->Show Kotlin Bytecode ,會出來如下界面:

image-20210318152946177

點(diǎn)擊 Decompile 就可以把 Kotlin 字節(jié)碼文件反編譯成 Java 代碼了

十四、總結(jié)

本篇文章很長,我們介紹了 Kotlin 大部分知識點(diǎn),按照文章開頭的思維導(dǎo)圖,我們就只剩下 Kotlin 泛型高級特性和 Kotlin 攜程沒有講了,這兩部分相對來說比較難,咋們后續(xù)在來仔細(xì)分析。相信你如果從頭看到這里,收獲一定很多,如果覺得我寫得還不錯,請給我點(diǎn)個贊吧??

參考和推薦

第一行代碼 Android 第3版 :郭神出品,必屬精品,對 Kotlin 的講解寫得通俗易懂

全文到此,原創(chuàng)不易,歡迎點(diǎn)贊,收藏,評論和轉(zhuǎn)發(fā),你的認(rèn)可是我創(chuàng)作的動力

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容