
前言
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)圖

注意: 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ù)等,如下圖:

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 ,編譯器會報錯:

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

你會發(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ù)有下面這些,如圖:

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)用的地方 ,如下圖:

替換后代碼變成了這樣:
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)用的地方 ,如下圖:

替換后代碼變成了這樣:
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 場景的?? :

上面圖片中的代碼報錯了,編譯器提示我們的大致原因是:這個地方不能使用 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 ,會出來如下界面:

點(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)作的動力