1. 簡介
- Java代碼編譯后生成的并不是計算機可以識別的二進制語言胰蝠,而是特殊的class文件瘪撇,這種class文件只有java虛擬機才能識別拟淮,而這個虛擬機其實擔任的就是解釋器的角色羡滑,會在程序運行時將編譯后的class文件解釋成計算機可以識別的二進制數據聪姿。因此,java嚴格來說屬于解釋性語言棵譬。
-
Kotlin
的工作原理:如同java原理一樣显蝌,使用自己的編譯器將代碼編譯成與java虛擬機編譯出的相同規(guī)格class文件,java虛擬機依然可以識別出由kotlin
編寫出的class文件。 -
kotlin
相較于java來說曼尊,語法更簡潔酬诀,更高級,甚至幾乎杜絕了空指針異常問題的出現骆撇。
2. 編程之本:函數與變量
2.1 變量
1. Kotlin
定義一個變量瞒御,只允許在變量前聲明兩種關鍵字:var
和val
- (1)
val
:value的簡寫,用來聲明一個不可變的變量神郊,該變量在初始賦值后就不能再次賦值肴裙,對應java中的被final
修飾的變量。 - (2)
var
:variable的簡寫涌乳,用來聲明一個可變的變量蜻懦,該變量在初始賦值后還可以再次賦值,對應java中的非final
變量夕晓。 - (3)
Kotlin
具有出色的類型推導機制宛乃,如果一個變量在初始聲明的時候就被賦值,那么Kotlin
就會自動推導出該變量的類型
2. Kotlin
中的代碼每一行的結尾不加分號
fun main() {
val a = 10
println("a= "+ a )
}
3. 類型推導機制
-
Kotlin
具有出色的類型推導機制蒸辆,如果一個變量在初始聲明的時候就被賦值征炼,那么Kotlin
就會自動推導出該變量的類型。 - 但是如果對一個變量延遲賦值躬贡,則
Kotlin
就無法自動推導該變量的類型谆奥,這個時候就需要顯示的聲明該變量的類型。
var a = 10
or
var a : Int = 10
or
val a : Int
a= 10
4. 數據類型
- 如果足夠細心逗宜,你就會發(fā)現雄右,在剛剛的代碼中,數據類型
Int
的I
是大寫的纺讲,而java中的int的首字母是小寫的擂仍。這代表Kotlin
完全拋棄了java中的基本數據類型,而全部改用對象數據類型熬甚。
Java Kotlin 數據類型說明
int Int 整形
long Long 長整形
short Short 短整形
float Float 單精度浮點型
double Double 雙精度浮點型
boolean Boolean 布爾型
char Char 字符型
byte Byte 字節(jié)型
- 什么時候使用
var
?什么時候使用val
?- 建議永遠先使用
val!val!val!
聲明一個變量逢渔,當val
沒有辦法滿足要求時在使用var
。這樣設計出來的程序會更健壯乡括,更符合高質量編碼規(guī)范肃廓。
- 建議永遠先使用
2.2 函數
1. 語法規(guī)則
①fun ②methodName(③param1: Int,param2:Int):④Int{
⑤return 0
}
- ①:定義函數的關鍵字
- ②:函數名
- ③:參數列表,
param1: Int
,冒號前面為變量名诲泌,后面為變量類型 - ④:函數返回類型
- ⑤:函數體
2. 語法糖
- 當一個函數只有一行代碼的時候盲赊,可以直接將唯一的一行代碼寫在函數定義部分的尾部,中間用等號連接即可
fun largerNumber(num1:Int,num2:Int) = max(num1,num2)
- 可以發(fā)現該函數省略了大括號敷扫,return哀蘑,以及函數返回值類型,這依賴于kotlin的類型推導機制。max()方法返回的是一個Int值绘迁,而我們在largerNumber函數的尾部又銜接了max函數合溺,因此kotlin可以推導出largerNumber函數的返回值類型也必然是Int類型。
3. 邏輯控制
3.1 if條件語句
- Kotlin中的if語句與Java中的if語句幾乎是沒有區(qū)別的缀台,但是也并不是完全相同
- Kotlin中的if語句是帶有返回值的
fun largerNumber(a:Int,b:Int):Int{
val c = if(a>b){
a
}else{
b
}
}
- 而且我們可以進一步將上述代碼簡化一下
fun largeNumber(a:Int,b:Int) = if(a>b){
a
}else{
b
}
3.2 when條件語句
- 與if一樣棠赛,也是有返回值的,因此也可以使用語法糖
- 使用方式:
when(params){
匹配值 ->{執(zhí)行邏輯}
}
- 當執(zhí)行邏輯只有一句話的時候膛腐,{}可以省略
- 除了精確匹配外睛约,when還支持類型匹配。類似于java中的
instanceof
哲身。核心是is
痰腮。
fun checkNumber(num:Number) = when(num){
is Int->println("is int")
is Long->println("is Long")
else->println("no such number")
}
- when還有一種不帶參數的用法
fun getScore_2(name:String) = when{
name.startsWith("Tom") ->55
name == "Jim" ->34
name == "Lily" -> 35
else->0
}
- 可以發(fā)現Kotlin中判斷字符串和對象是否相等可以直接使用==,而不用像java一樣使用equals()
3.3 循環(huán)語句
3.3.1 while循環(huán)語句
- kotlin中的while循環(huán)語句與java中的while循環(huán)語句完全相同律罢。、
3.3.2 for循環(huán)語句
- Kotlin中的for循環(huán)進行了很大的修改棍丐。Java中常用的for-i循環(huán)在kotlin中直接被舍棄了误辑,而另一種for-each則被kotlin進行了大幅度的加強,變成了for-in循環(huán)
fun printNumber(){
for(i in 0..10){
println(i) // 0 1 2 3 4 5 6 7 8 9 10
}
for(i in 0 until 10 ){
println(i) // 0 1 2 3 4 5 6 7 8 9
}
for(i in 0 until 10 step 2){
println(i) // 0 2 4 6 8
}
for(i in 10 downTo 1){
println(i) // 10 9 8 7 6 5 4 3 2 1
}
}
- Kotlin中新增了區(qū)間的概念:
val rang = 0..10
表示創(chuàng)建了一個0到10的區(qū)間歌逢,且兩端都是閉區(qū)間巾钉。在..
的兩邊指定區(qū)間的左右端點就可以創(chuàng)建一個區(qū)間了-
for(i in 0..10)
代表的是遍歷0到10之間的數字
-
-
until
關鍵字表示:創(chuàng)建一個左閉右開的區(qū)間 -
step
的作用:for(i in 0 until 10 step 2)
的意思是每次循環(huán)都會在區(qū)間范圍內加2 -
downTo
:創(chuàng)建一個降序閉區(qū)間
4. 繼承與構造函數
4.1 繼承
4.1.1 Kotlin中的繼承與Java中的區(qū)別
- Kotlin在繼承這方面與java有些不同;Kotlin中任何一個非抽象類默認都是不可以被繼承的秘案,相當于java中的類被final修飾砰苍。
-
Effective Java
中明確提到如果一個類不是專門為了繼承而設計的,那么就應該主動將他加上final聲明阱高,禁止其可以被繼承赚导。
4.1.2 如何實現繼承
- 為了使Kotlin中的類可以被繼承,我們需要用
open
關鍵字來修飾該類
open class Person{
var name =""
var age = 0
fun eat(){
}
}
class Student : Person(){
var sno = 0
var grade = 0
}
4.2 構造函數
- 細品的話會發(fā)現赤惊,上述代碼實現繼承時吼旧,繼承Person類后面還跟著一個括號,這個跟java中也不相同未舟。這涉及到Kotlin中的主構造函數與次構造函數圈暗。
- 在Java與Kotlin中,任何一個類都會有構造函數裕膀。在Kotlin中员串,構造函數被分為兩類:主構造函數與次構造函數
(1) 主構造函數:
- 主構造函數是我們最常用的構造函數,每一個類都會有一個默認的不帶參數的主構造函數昼扛。當然寸齐,我們也可以顯示的給它指明參數。
- 特點:沒有函數體,直接定義在類名后面即可
class Student(var sno:String,var grade:Int) : Person(){
//no func body
}
- 如果我們需要創(chuàng)建一個Student對象访忿,則只需要以下步驟瞧栗,就能獲取一個student對象了
val student = Student("1",1)
- 雖然主構造函數沒有函數體,但是如果我們又想在主構造函數中加入一些邏輯海铆,這樣該怎么辦呢迹恐?Kotlin中提供了
init
結構體,所有主構造函數的邏輯可以寫在里面
class Student(var sno:String,var grade:Int) : Person(){
init{
//TODO
}
}
- 說了這么多還是沒有涉及到Person后跟著的括號卧斟,那么到底有什么關系呢殴边?這涉及到繼承中的一個性質,子類的構造函數必須要調用父類的構造函數珍语,這個規(guī)定在Kotlin中也要遵守锤岸。因此我們就會發(fā)現,既然Kotlin中主構造函數沒有函數體板乙,那我們該如何調用父類的構造函數呢是偷?第一種辦法,寫在init結構體中募逞,按理說這樣可以蛋铆,但是我們在大多數場景中是不需要寫init結構體的;第二種放接,就是我們說的括號刺啦。子類的主構造函數調用父類的哪個構造函數,在繼承的時候通過括號來進行指定纠脾。
class Student(var sno:String,var grade:Int) : Person(){}
//這個括號就代表調用的是Person中的無參構造函數
- 即使在無參數的情況下玛瘸,這對括號也不能省略。
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age){}
//這個括號就代表調用的是Person中帶有對應參數的構造函數
- 注意苟蹈,我們在向Student類的主構造函數中增加的
name
和age
這兩個字段時糊渊,不能再將它們聲明成val
或var
,因為在主構造函數中聲明成val或var的參數將自動成為該類的字段慧脱,這會導致與父類中的同名的參數發(fā)生沖突再来。因此,這里的name
和age
參數前面不用加任何關鍵字磷瘤,使其作用僅限于主構造函數中即可芒篷。
(2) 次構造函數
- 任何一個類只能有一個主構造函數,但是可以有多個次構造函數采缚。次構造函數可以用于實例化一個類针炉,不過與主構造函數不同的是,它是有函數體的扳抽。
- Kotlin中規(guī)定:次構造函數是通過
constructor
關鍵字來定義的篡帕,當一個類既有主構造函數又有次構造函數時殖侵,所有的次構造函數必須調用主題構造函數(包括間接調用)。舉一個簡單栗子:
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age){
//直接調用主構造函數
constructor(name:String,age:Int):this("",0,name,age){
//TODO
}
//間接調用镰烧,調用上一個次構造函數拢军,間接調用主構造函數
constructor():this("Tom",2){
//TODO
}
}
- 還有一種特殊的情況:類中只有次構造函數,沒有主構造函數怔鳖。Kotlin中茉唉,當一個類沒有顯示定義主構造函數,且定義了次構造函數時结执,那么這個類就是沒有主構造函數的度陆。
class Teacher : Person{
constructor(name:String,age:Int):super(name,age)
}
- 我們來分析一下:首先,Teacher類中的后面沒有顯示的定義主構造函數献幔,同時又因為定義了次構造函數懂傀,所以目前的情況下,Teacher類是沒有主構造函數的蜡感。既然沒有主構造函數了蹬蚁,繼承Person類的時候也就不需要加上括號了。然后郑兴,由于沒有主構造函數缚忧,次構造函數只能直接調用父類的構造函數了。
5. 接口與修飾符
5.1 接口
- Kotlin中接口部分與Java中幾乎是一致的杈笔。Java是單繼承的,每一個類最多只能繼承一個父類糕非,但是卻可以實現多個接口蒙具,Kotlin也是如此。
- 接口中的函數不要求有函數體朽肥。
interface Study{
fun readBook()
fun doHomework()
}
- 用之前定義的Student類進行舉例禁筏,讓其實現Study接口。
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age),Study{
override fun readBook(){
//TODO
}
override fun doHomework(){
//TODO
}
}
- 觀察上述代碼可以發(fā)現衡招,Kotlin中統(tǒng)一用冒號來表示繼承和實現篱昔,中間用逗號進行分隔。并且接口后面不需要加上括號始腾,因為它沒有構造函數可以去調用州刽。
- Kotlin中為了讓接口的功能更加靈活,增加了這樣一個功能:允許對接口中定義的函數進行默認實現浪箭。所謂的默認實現是指:接口中的一個函數具有了函數體穗椅,這個函數體中的內容就是它的默認實現。以Study接口為例奶栖,一個類實現了Study接口時匹表,只會強制要求實現readBook()函數门坷,而doHomework()可以自由的選擇是否實現,如果實現就是使用實現后的邏輯袍镀,如果不實現則使用默認的邏輯默蚌。
interface Study{
fun readBook()
fun doHomework(){
println("do homework default impl")
}
}
5.2 可見性修飾符
我們可以通過一個表格來了解Java中的修飾符與Kotlin中的修飾符的區(qū)別。
修飾符 JAVA Kotlin
public 所有類可見 所有類可見(默認)
protected 當前類苇羡,子類绸吸,同 當前類,子類可見
一包中的類可見
private 當前類可見 當前類可見
default 同一包路徑下的類 無
可見(默認)
internal 無 同一模塊中的類可見
6. 數據類與單例類
6.1 數據類
- 數據類一般都占據著很重要的角色宣虾,用于將服務器端或數據庫中的數據映射到內存中惯裕,為編程邏輯提供數據模型的支持。
- Java中的數據類绣硝,也叫JavaBean類:
public class Phone{
String brand;
double price;
public Phone(String brand,double price){
this.brand = brand;
this.price = price;
}
@Override
public boolean equals(Object obj){
//TODO
}
@Override
public int hashCode(){
//TODO
}
@Override
public String toString(){
//TODO
}
}
- Java中的數據類比較復雜蜻势,無意義代碼較多。Kotlin中數據類的實現方式則極其簡單:
data class Phone(val brand:String,val price:Double)
- 是的鹉胖,你沒有看錯握玛,僅僅需要這一行代碼,就能實現Kotlin中的數據類甫菠。神奇的地方就在于
data
這個關鍵字挠铲,在Kotlin中當一個類前面聲明了data
關鍵字時,就表明你希望這個類是一個數據類寂诱。Kotlin中會跟主構造函數中的參數幫你將equals()拂苹、hashCode()、toString()’
等固定且無意義的方法自動生成痰洒。并且瓢棒,我們可以發(fā)現這個類是沒有大括號的,在Kotlin中丘喻,當一個類中沒有任何代碼時脯宿,還可以將大括號省略掉。
6.2 單例類
- Kotlin中將固定的泉粉,重復的邏輯隱藏了起來,只暴露給我們最簡單的用法嗡靡。創(chuàng)建單例類時跺撼,僅需要將
class
更改為object
即可。
object Singleton{
fun singletonTest)(){
//TODO
}
}
- 在Kotlin中我們不需要私有化構造函數讨彼,也不需要提供
getInstance()
這種靜態(tài)方法财边,只需要把class關鍵字改為object即可。調用方法為SingleTon.singletonTest()
点骑。
7. Lambda編程
7.1 集合的創(chuàng)建與遍歷
7.1.1 Kotlin中創(chuàng)建list與set集合
- Kotlin可以與Java中創(chuàng)建ArrayList集合的方式相同
val list = ArrayList<String>()
list.add("one")
list.add("two")
list.add("three")
}
- 但對于Kotlin來說酣难,上述這種方式比較繁瑣谍夭,Kotlin中內置了
listOf()
來簡化初始化集合的方法。僅用一行代碼就可以實現集合的初始化操作憨募。不過需要注意的是紧索,listOf()
函數創(chuàng)建的是不可變的集合,意思是被創(chuàng)建的集合無法進行添加菜谣,刪除或修改操作珠漂。
val list = listOf("one","two","three")
- 如果我們需要創(chuàng)建一個可變集合的話,使用
mutableListOf()
函數即可.尾膊。
val list = mutableListOf("one","two","three")
- 創(chuàng)建Set集合的方法與List集合幾乎一模一樣媳危。只是將創(chuàng)建集合的方式更換為
setOf()
與mutableSetOf()
牡拇。需要注意的是由于set集合底層是使用hash映射機制來存儲的既荚,因此set集合中的元素是無序的。
7.1.2 Kotlin中創(chuàng)建Map集合
- Kotlin可以與Java中創(chuàng)建map集合的方式相同
fun createMap(){
val map = HashMap<String,Int>()
map.put("one",1)
map.put("two",2)
map.put("three",3)
}
- 但是Kotlin中其實不建議使用
put()
與get()
方法來對Map進行添加和讀取數據操作摔踱,而是更加推薦使用一種類似于數組下標的語法結構抓谴。
fun createMap2(){
val map = HashMap<String,Int>()
map["one"] = 1
map["two"] = 2
map["three"] = 3
}
- 但是Kotlin依然覺得這種方式太麻煩了暮蹂,因此如同list與set一樣,Kotlin依然提供了一對
mapOf()
與mutableMapOf()
方法來簡化Map癌压。我們以mapOf()
來舉例仰泻。
val map = mapOf("one" to 1,"two" to 2,"three" to 3)
- 遍歷map集合的方法依然是
forin
。只不過需要把一對(key滩届,value)放在變量的位置上集侯。
val map = mapOf("one" to 1,"two" to 2,"three" to 3)
for((numName , num) in map){
println("numName is " + numName +" ,num is" + num )
}
7.2 集合的函數式API
7.2.1 Lambda表達式
1. 首先學習函數式API的語法結構,也就是Lamdba表達式的語法結構帜消。
- 按照我們在java中的思路棠枉,如果我們想要找出單詞最長的水果名,這個算法該如何寫呢
fun maxLength(){
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit=""
for(fruit in list){
if (maxLengthFruit.length<fruit.length){
maxLengthFruit = fruit
}
}
}
- 但是在Kotlin中我們可以使用Lambda表達式來使這段代碼更簡潔
fun maxLengthLambda() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy { it.length }
}
- 只用一行代碼就能完成這個功能
2. 理解Lambda表達式的簡化步驟
(1)Lamda的定義:一小段可以作為參數傳遞的代碼
(2)語法結構:
{paramsName 1: paramsType,paramsName 2: paramsType -> funcBody}
- 首先券犁,最外層是一層大括號
- 如果有參數傳入的話,我們還需要聲明參數列表
- 參數列表的結尾用
->
表示參數列表的結束語函數體的開始 - 函數體中可以編寫任意行代碼(但不建議太長)汹碱,并且最后一行代碼會自動作為Lambda表達式的返回值
(3)正常的Lambda表達式
- 在剛剛的Lambda表達式中使用的
maxBy
函數就是一個普通的函數粘衬,只不過接收的是Lambda類型的參數,并且在遍歷集合時將每次遍歷的值作為參數傳遞給Lambda表達式咳促。這個函數的工作原理是:根據我們傳入的條件來遍歷集合稚新,從而找到該條件下的最大值。
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var lambda = {fruit:String -> fruit.length}
var maxLengthFruit = list.maxBy(lambda)
}
(4) 開始簡化
- 第一步跪腹,我們并不需要定義一個Lambda變量褂删,可以直接將這個表達式傳入函數中
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy({fruit:String -> fruit.length})
}
- 第二步,在Kotlin中規(guī)定冲茸,當Lambda表達式作為函數的最后一個參數時屯阀,可以將其移到括號外邊
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy(){fruit:String -> fruit.length}
}
- 第三步缅帘,如果lambda表達式是函數的唯一一個參數時,還可以將函數的括號省略
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy{fruit:String -> fruit.length}
}
- 第四步难衰,由于Lambda具有出色的類型推導機制钦无,因此大多數情況Lambda表達式中的參數列表其實在大多數情況下不必聲明參數類型。
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy{fruit -> fruit.length}
}
- 第五步盖袭,當Lambda表達式中只有一個參數時失暂,也不必聲明參數名,而是可以直接使用
it
來代替鳄虱,那么代碼就會變成最初的那樣
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy{ it.length}
}
7.2.2 函數式API的相互配合
1. map函數與filter函數
- 集合中的map函數是非常常用的一種數據結構弟塞,用于將集合中的每個元素都映射成另外的值,映射的規(guī)則由Lambda表達式指定拙已,最終形成一個新的集合决记。
- 舉個??;這樣會將原list中的單詞全部變?yōu)榇髮懩J接扑ǎ酥饷拐牵覀冞€可以將單詞全轉換為小寫,或只取單詞首字母惭适,結果如何是根據Lambda中的規(guī)則來的笙瑟。
val list = listOf<String>("Apple","Banana","Pear")
var newList = list.map{ it.toUpperCase()}
for(fruit in newList){
println(fruit)
}
- filter函數是用來過濾集合中的數據的,并將過濾后的數據返回為一個新的集合癞志⊥希可以單獨使用,也可以配合剛才的map一起使用凄杯。
val list = listOf<String>("Apple","Banana","Pear")
var newList = list.filter{it.length<=5}.map{it.toUpperCase()}
for(fruit in newList){
println(fruit)
}
2. any與all函數
- any函數是用來判斷集合中是否至少存在一個元素滿足指定條件错洁。
- all函數是用來判斷集合中是否所有元素都滿足指定條件。
fun anyAndall(){
val list = listOf<String>("Apple","Banana","Pear")
var anyResult = list.any{it.length<=5}
var allResult = list.all{it.length<=5}
println("anyResult is " + anyResult+",allResult is " + allResult)
}
7.2.3 Java函數式API的使用
- 如果我們在Kotlin代碼中調用了一個java方法戒突,并且讓方法接收一個Java單抽象方法接口屯碴,這種情況下可以使用函數式API。
- 單抽象方法接口:接口中只有一個抽象方法膊存。
- 以Java原生API中一個最常見的單抽象方法為例--Runnable接口导而。該接口中只有一個待實現的run()方法。在java中使用Runnable接口的方法是這樣的:
new Thread(new Runnable(){
@Override
public void run(){
//func body
}
}).start();
- 如果將Java版本的代碼改為Kotlin版本隔崎,那么結果是什么呢今艺?
Thread(object:Runnable{
override fun run(){
println("Hello ")
}
}).start()
- 由于Kotlin完全舍棄了new關鍵字,因此創(chuàng)建匿名類實例的時候不能在使用new爵卒,而是改用了object關鍵字虚缎。但是目前Thread的構造方法是符合Java函數式API的使用條件的,因此我們可以對其進行精簡钓株。一下這段代碼就是精簡的結果实牡。因為Runnable類中只有一個待實現方法陌僵,即便沒有顯示的重寫run()方法,Kotlin也能自動明白Runnable后面的Lambda表達式就是要在run()方法中實現的內容铲掐。
Thread(Runnable{
println("Hello ")
}).start()
- 另外拾弃,如果一個Java方法的參數列表不存在一個以上Java單抽象方法參數接口,我們還可以將這個接口省略摆霉。
Thread({
println("Hello ")
}).start()
- 精簡還未結束豪椿!別忘了,當Lambda表達式是方法的最后且唯一一個參數時携栋,可以將Lambda表達式移到方法括號外邊搭盾,且將方法的括號省略。那么最終結果就如下:
Thread{
println("Hello ")
}.start()
-
本小節(jié)中的Java函數式API的使用都限定于從Kotlin中調用Java方法婉支,并且單抽象方法接口也必須是用Java語言定義的鸯隅。在Kotlin中會經常調用android的SDK,而因為android中的SDK都是用Java來進行編寫的向挖,因此在調用這些SDK接口會經常用到Java函數式API蝌以。舉個栗子,Android中有一個極為常用的點擊事件接口
OnClickListner
何之。如果我們用Java代碼來注冊這個點擊事件跟畅,需要這么寫:
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
//funcbody
}
})
- 而用Kotlin代碼實現同樣的功能,就可以使用函數式API的思想來對上述代碼進行簡化溶推,結果如下徊件。是不是很簡單
button.setOnClickListener{
//funcbody
}
8 空指針檢查
在剛開始的簡介就說過,在Kotlin中幾乎杜絕了空指針問題的出現蒜危。Kotlin利用編譯時判空檢查的機制幾乎杜絕了空指針異常虱痕,所謂的編譯時判空檢查機制就是將空指針異常的檢查從運行時提前到了編譯期,如果程序存在空指針異常的風險辐赞,那么在編譯時會自動報錯部翘。
- 以之前的一個函數為例
fun doStudy(study:Study){
study.readBooks()
study.doHomework()
}
- 這段代碼看上去和Java版本的代碼并沒有什么區(qū)別,但是它是沒有空指針異常的風險的响委。因為Kotlin默認所有的參數和變量都不可為空新思,所以這里傳入的study參數也一定不會為空。但是晃酒,這種方法雖然避免了空指針異常的出現表牢,又會導致另一個問題窄绒,如果我們需要某個傳入的參數或變量為空的話該怎么辦呢贝次?為了解決上面這個問題,Kotlin提供了另外一套可為空的類型彰导,只不過這套方法需要我們在編譯期就將所有的潛在的空指針異常都處理掉蛔翅,否則代碼將無法編譯通過敲茄。
- 可為空的類型系統(tǒng)是指:在類名后面加一個問號。例如:
Int:表示不可為空的類型
Int?:表示可為空的類型
String:表示不可為空的類型
String?:表示可為空的類型
- 繼續(xù)拿上面的函數舉例:如果我們希望傳入的參數一可以為空山析,那么就應該將參數的類型從
Study
改為Study?
堰燎。但是繼續(xù)以下的寫法的話又會報出錯誤提示。
fun doStudy(study:Study?){
study.readBooks()
study.doHomework()
} - 理由很簡單笋轨,由于我們將參數改為了可空的類型秆剪,那么調用參數的方法就會有可能造成空指針異常,因此Kotlin在這種情況下不允許編譯通過爵政。處理方法為仅讽,把空指針異常都處理掉就可以了,做個判空處理钾挟。
fun doStudy(study:Study?){
if(study!=null){
study.readBooks()
study.doHomework()
}
}
8.1判空輔助工具
8.1.1 ?.
操作符
這個操作符的作用非常好理解洁灵,就是當對象不為空的時候正常調用相應的方法,當對象為空時則什么也不做掺出。比如以下處理徽千。
if(a!=null){
a.readBooks()
}
簡化為
a?.readBooks()
8.1.2 ?:
操作符
這個操作符的左右兩邊都接收一個表達式,如果左邊表達式不為空則返回左邊表達式的結果汤锨,否則就返回右邊表達式的結果双抽。
val c = if(a!=null){
a
}else{
b
}
簡化為
val c = a?:b
- 用一個例子函數來講上述兩個輔助工具結合一下。假如我們要獲取一個字符串的長度泥畅,如果不用判空工具的話是如下這種寫法荠诬。
fun getTextLength(text:String?) : Int{
if(text!=null){
return text.length
}
return 0
}
- 如果要用操作符進行簡化的話,首先位仁,text是可能為空的柑贞,因此我們在調用其length字段時需要使用
?.
操作符,可以簡化為text?.length
;其次聂抢,text?.length
返回值為null钧嘶,那我們就可以借用?:
操作符使其返回值為0。
fun getTextLength(text:String?) = text?.length?:0
8.1.3 !!
操作符
用一段代碼來解釋這個操作符的作用琳疏。我們先定義一個可為空的全局變量content有决,然后將其變?yōu)榇髮懩J?/p>
var content:String?="Hello"
fun main() {
if(content!=null){
printUpperCase()
}
}
fun printUpperCase(){
val upperCase = content.toUpperCase()
println(upperCase)
}
- 上述代碼看起來是沒有問題的,但是遺憾的是這段代碼一定是無法正常運行的空盼。因為
printUpperCase()
函數無法知道外部已經對content進行了非空檢查书幕,所以編譯的時候會認為存在空指針風險,導致無法編譯通過揽趾。 - 這種情況下台汇,就可以用到
!!
操作符了,又名非空斷言工具,寫法是在對象的后面加上!!
。這是一種有風險的寫法苟呐,意在告訴Kotlin痒芝,我非常確信這里的對象不會為空,所以不用幫我來做空指針檢查了牵素。
fun printUpperCase(){
val upperCase = content!!.toUpperCase()
println(upperCase)
}
但是這并不是一種好的實現方法严衬,因為每一次使用非空斷言工具的時候,就有可能出現空指針異常笆呆。所以在使用非空斷言工具的時候最好提醒一下自己请琳,是不是有更好的實現方式。
8.1.4 let
輔助工具
let
既不是操作符赠幕,也不是什么關鍵字单起,而是一個函數。該函數提供了函數式API的編程接口劣坊,并將原始調用對象作為參數傳遞給Lambda表達式中嘀倒。
obj.let{
obj ->
//編寫具體業(yè)務
}
- 這里調用了
obj
對象的let函數,然后Lambda表達式中的代碼會立即執(zhí)行局冰,并且將這個obj對象本身還會作為參數傳遞到Lambda表達式中测蘑。
但是我們知道,這一小節(jié)是來介紹判空輔助工具的康二,那么
let
與空指針檢查有什么關系呢碳胳?以之前的doStudy()函數舉例
fun doStudy(study:Study?){
study?.readBooks()
study?.doHomework()
}
- 細品的話,本來我們進行一次if判斷就能隨意調用study對象的任何方法沫勿,但是受制于
挨约?.
操作符的限制,現在變成了每次調用study對象的方法都需要進行一次if判斷产雹。那么這個時候就可以結合let函數對代碼進行優(yōu)化了诫惭。
fun doStudy(study:Study?){
study?.let{ stu ->
stu.doHomework()
stu.readBook()
}
}
0
- 在
?.
操作符的作用下,對象為空時就什么都不做蔓挖,不為空時則調用let
函數將study對象本身作為參數傳遞到Lambda表達式中夕土,此時study
對象本身肯定不為空了。并且根據Lambda語法的特性瘟判,當Lambda表達式的參數列表中只有一個參數時怨绣,可以不用聲明參數名,直接使用it關鍵字來代替即可拷获,我們可以進一步簡化代碼篮撑。
fun doStudy(study:Study?){
study?.let{
it.doHomework()
it.readBook()
}
}
9.Kotlin中的小技巧
9.1 字符串內嵌表達式
Kotlin中添加了該功能,彌補上了java上相關功能的缺憾匆瓜。不需要再復雜的拼接字符串了赢笨。
"hello , ${obj.name}.Nice to meet you"
- 可以看到邪财,Kotlin中允許我們在字符串中嵌入
${}
這種語法結構的表達式,并在運行時使用表達式的結果來替代這一部分质欲。另外,當表達式中僅有這一個變量的時候糠馆,還可以將兩邊大括號給省略掉嘶伟。
"hello , $name.Nice to meet you"
- 加入我們要輸出一串帶變量的字符串,在原來的方法中又碌,輸出的寫法應該是這樣的
val name = a
val age = 10
println(“people(name = ” + name + ",age = " + age + ")")
- 使用字符串內嵌表達式后九昧,可以簡化為
val name = a
val age = 10
println(“people(name =$name , age=$age)")
9.2 函數的默認參數值
在前面講解次構造函數用法的時候就提到過,次構造函數在Kotlin中很少用毕匀,因為Kotlin中提供了給函數設定默認值的功能铸鹰,在很大程度上能夠替代次構造函數的作用。具體來講皂岔,就是在定義函數的時候給任意參數設定一個默認值蹋笼,這樣調用此函數的時候就不會強制要求調用方為此參數傳值,在沒有傳值的情況下會自動使用參數的默認值躁垛。
- 方法如下:
fun printParams(num:Int,str:String = "hello"){
println("num is $num,str is $str")
}
- 如果我們使用
printParams(10)
來調用該方法剖毯,會得到如下結果。
num is 10,str is hello
- 但是上面的例子比較理想化教馆,如果我們向要讓第一個參數設定默認值逊谋,第二個參數使用賦值呢?
fun printParams(num:Int = 10,str:String){
println("num is $num,str is $str")
}
- 模仿剛剛的寫法肯定是不可以的土铺,因為編譯器會認為我們想把字符串賦值給第一個變量胶滋,從而報類型不匹配的錯誤。而Kotlin提供的另一套比較神奇的機制悲敷,就是通過鍵值對的方式來傳遞參數究恤,不用像傳統(tǒng)方法那樣按照參數定義的順序來傳遞參數。我們可以寫成這樣:
printParams(str = "hello",num = 12)
- 此時哪個參數在前哪個參數在后都無所謂后德,Kotlin可以準確的將參數匹配上丁溅。使用這種鍵值對的傳參方式之后,我們就可以省略num參數了探遵,代碼如下:
fun printParams(num:Int = 10,str:String){
println("num is $num,str is $str")
}
fun main(){
printParams(str = "hello")
}
//輸出值為
num is 10,str is hello
那么為什么說給函數參數設置默認值可以很大程度上代替次構造函數的作用的窟赏?
- 前邊學習次構造函數的代碼
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age){
//直接調用主構造函數
constructor(name:String,age:Int):this("",0,name,age){
//TODO
}
//間接調用,調用上一個次構造函數箱季,間接調用主構造函數
constructor():this("Tom",2){
//TODO
}
}
- 次構造函數在這里的作用是提供更少的參數來對Student類進行實例化的方式涯穷。無參的次構造函數會調用兩個參數的次構造函數,并將這兩個參數賦值成初始值藏雏。兩個參數的次構造函數會調用4個參數的主構造函數拷况,將缺失的參數賦值為初始值作煌。但是學習了給參數設置默認值,就完全用不到上述方式了赚瘦。我們只需要編寫一個主構造函數粟誓,并且給參數設置默認值的方式來實現。
class Student(var sno:String="",var grade:Int =0,name:String="",age:Int=0) : Person(name,age){}
9.3 Kotlin中使用findViewById()
- 加入要使用我們定義的一個button起意,在kotlin中的調用方法是這樣的
val button:Button = findViewById(R.id.button)
button.setOnClickListener{
//funcBody
}
findViewById()方法返回的是繼承View的泛型對象鹰服,因此Korlin無法自動推斷出它是一個Button還是其他控件。因此我們需要將button變量顯示的聲明成Button類型揽咕。但是如果在布局文件中有十個控件悲酷,我們就需要重復調用十次findViewById()來獲取這些控件,這無疑非常麻煩亲善。
- 但是Kotlin中不用再代碼中重復調用 findViewById()方法來獲取控件了设易,因為使用Kotlin編寫的安卓項目會在app.gradle文件的頭部默認引入了一個插件,該插件會根據布局文件中定義的控件id自動生成一個具有相同名稱的變量蛹头,我們在Activity中可以直接使用這個變量顿肺,而不需要再調用findViewById()方法了。
9.4 使用get和set方法的語法糖
- JavaBean
public class Book{
private int pages;
public int getPages(){
return pages;
}
public void setPages(int pages){
this.pages = pages;
}
}
- 在Java中調用Book類中的pages字段時渣蜗,需要使用getPages()和setPages(int pages)方法挟冠。而在Kotlin中調用這種語法結構時,可以使用一種更簡單的寫法袍睡,比如用下面這種代碼來設置Book中的pages字段
val book = Book()
book.pages = 500
val bookPages = book.pages
- 這里看上去好像我們并沒有調用Book類中的setPages()與getPages()方法知染,而是直接對pages字段進行了賦值和讀取。其實這就是Kotlin中的語法糖斑胜,他會在背后自動將上述代碼轉換成調用setPages()方法和getPages()方法
10. 標準函數與靜態(tài)函數
10.1 標準函數
我們之前所學的let函數控淡,其實就是標準函數,其主要作用就是配合
?.
操作符進行判空輔助處理止潘。而標準函數其實是指在Standard.kt中定義的函數掺炭,任何Kotlin代碼都可以自由的調用所有的標準函數。我們先主要掌握幾個常用的標準函數with,apply,run
標準函數凭戴。
(1)with函數
- 參數:第一個參數可以為任何對象涧狮,第二個參數是一個Lambda表達式。
- 原理:with函數會在Lambda表達式中提供第一個參數對象的上下文么夫,并使用最后一行代碼作為返回值返回者冤。以下面的代碼為例
val result = with(obj){
//這里是obj的上下文
value//with函數的返回值
}
- 作用:我們可以在連續(xù)調用同一個對象的多個方法時讓代碼變得更加精簡。
val list = listOf("one","two","three")
val result = with(StringBuilder()){
append("start \n")
for(fruit in list){
append(fruit + "\n")
}
toString()
}
println(result)
(2)run函數
該函數的用法和使用場景其實和with函數時非常類似的档痪,只是在參數與調用方法上有所區(qū)別涉枫。
- 首先,run函數是不能直接調用的腐螟,而是一定要被某個函數所調用才行愿汰。
- 其次困后,run函數只接受一個Lambda參數,并且會在Lambda表達式中提供調用對象的上下文衬廷。
val list = listOf("one","two","three")
val result = StringBuilder().run(){
append("start \n")
for(fruit in list){
append(fruit + "\n")
}
toString()
}
println(result)
(2)apply函數
apply函數與run函數也及其類似摇予,需要被某個對象調用,但無法指定返回值吗跋,只能返回調用對象本身侧戴。
val list = listOf("one","two","three")
val result = StringBuilder().apply{
append("start \n")
for(fruit in list){
append(fruit + "\n")
}
}
println(result.toString()) //此時result為StringBuilder()對象
(3)repeat函數
可以直接被調用,不需要通過對象小腊,接受兩個參數,第一個參數為數值n久窟,第二個參數為Lambda表達式秩冈,然后會將Lambda表達式中的內容執(zhí)行n次
10.2 靜態(tài)函數
靜態(tài)方法又叫做類方法,指的是那種不需要創(chuàng)建實例就能調用的方法斥扛。Java中定義靜態(tài)方法只需要在函數聲明時加上static關鍵字即可入问,但是Kotlin卻極度弱化了靜態(tài)方法這個概念。之所以要這樣設計稀颁,是因為Kotlin提供了更好的語法特性芬失,那就是單例類。
10.2.1 單例類
object util{
fun doAction(){
}
}
- 注意匾灶,doAction()方法實際上并不是靜態(tài)方法棱烂,只是可以使用類似于調用靜態(tài)方法的方式來調用,Util.doAction()阶女。單例類的寫法會將整個類中的所有方法全都變成類似于靜態(tài)方法的調用方式颊糜,但是如果我們只想讓一個或幾個方法變成靜態(tài)方法的調用方式話,這樣就很不方便秃踩。這時就需要伴生關鍵字
companion object
10.2.2 伴生關鍵字companion object
class Util{
fun doAction1(){}
companion object{
fun doAction2(){}
}
}
- 以上兩個函數的調用方法分別為:
Util().doAction1()
,Util.doAction2()
- 但是doAction2()方法其實也并不是靜態(tài)方法衬鱼,
companion object
改關鍵字實際上會在Util類內部創(chuàng)建一個伴生類,doAction2()方法就是定義在這個伴生類中的實例方法憔杨。只是Kotlin會保證Util類始終只會存在一個伴生類對象 - 由此可以看出鸟赫,Kotlin中確實沒有可以直接定義靜態(tài)方法的關鍵字,但是提供了一些語法特性來支持類似于靜態(tài)方法調用的寫法消别。
10.2.3 注解@JvmStatic聲明靜態(tài)方法
之前也說過抛蚤,單例類與
companion object
都不是真正的單例類,而如果我們在這些方法上加上@JvmStatic注解
寻狂,kotlin會將這些方法編譯成真正的靜態(tài)方法
class Util{
fun doAction1(){
}
companion object{
// 經過該關鍵字修飾后霉颠,doAction2()方法為真正的靜態(tài)方法
@JvmStatic
fun doAction2(){
}
}
}
@JvmStatic
關鍵字只能加在單例類或companion object中的方法上,如果加在普通方法上會提示語法錯誤
10.2.4 頂層方法
頂層方法指的是沒有定義在任何類中的方法荆虱,Kotlin會將所有的頂層方法全部編譯成靜態(tài)方法
//新建一個Kotlin文件蒿偎,在該文件中直接定義方法
fun doAction1(){
}
- 如果是在Kotlin中調用的話朽们,所有的頂層方法都可以在任何位置上被直接調用,不用管包名诉位,實例骑脱,直接輸入doAction1()即可
- 如果是java中,是無法直接調用的苍糠,會找不到這個方法叁丧,因為Java中沒有頂層方法這個概念,kotlin會將頂層方法所在的文件創(chuàng)建一個類岳瞭,比如該文件名為Help.kt拥娄,那么就會新建一個HelpKt.class的Java類,doAction1()就是以靜態(tài)方法寫在該類中的
11. 小總結
- 類型強制轉換符 為
as
12. 延遲初始化和密封類
12.1 對變量的延遲初始化
在開發(fā)中瞳筏,如果類中存在很多全局變量實例稚瘾,為了保證他們能滿足Kotlin中的空指針檢查語法標準,我們不得不在代碼中坐很多非空判斷保護才行姚炕,比如
class TestClass {
fun doXxx() {
// TODO:
}
}
class TestActivity : AppCompatActivity(), View.OnClickListener {
private var mTestClass: TestClass? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mTestClass = TestClass()
}
override fun onClick(v: View?) {
mTestClass?.doXxx()
}
}
將 mTestClass 設置為全局變量摊欠,但是它的初始化工作是在 onCreate() 函數中進行的,因此不得不先將 mTestClass 賦值為 null柱宦,同時把它的類型聲明成 TestClass?些椒。
雖然在 onCreate() 函數中對 mTestClass 進行初始化,同時能確保 onClick() 函數必然在 onCreate() 函數之后才會調用掸刊,免糕,但是在 onClick() 函數中調用 mTestClass 的任何函數時仍然要進行判空處理才行,否則編譯肯定無法通過忧侧。
當代碼中有了越來越多的全局變量實例時说墨,這個問題就會變得越來越明顯,到時候可能必須編寫大量額外的判空處理代碼苍柏,只是為了滿足 Kotlin 編譯器的要求尼斧。解決辦法就是使用全局變量進行延遲初始化抛蚁。
- 延遲初始化的關鍵字為
lateinit
棚潦,該關鍵字可以告訴編譯器,我會在晚些時候對這個變量進行初始化食寡,那么這樣就不用一開始聲明的時候就將該變量賦值為null了熄捍。
class TestActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var mTestClass: TestClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mTestClass = TestClass()
}
override fun onClick(v: View?) {
mTestClass.doXxx()
}
}
- 但是烛恤,使用lateinit關鍵字不是沒有風險的,當對一個全局變量使用lateinit關鍵字時余耽,一定要保證在任何情況缚柏,任何位置調用的時候,該關鍵字已經被賦值了碟贾,否則程序一定會崩潰
另外币喧,我們還可以通過代碼
isInitialized
來判斷一個全局變量是否已經被初始化了轨域,這樣在某些時候能有效的避免對某一個變量重復的進行初始化工作,比如
class TestActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var mTestClass: TestClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//判斷mTestClass是否已經被初始化
//::mTestClass.isInitialized的寫法看上去有點奇怪杀餐,但這是固定的寫法
if (!::mTestClass.isInitialized) {
mTestClass = TestClass()
}
}
override fun onClick(v: View?) {
mTestClass.doXxx()
}
}
12.2 密封類
網上一篇對密封類介紹的比較詳細的文章
13. 擴展函數與運算符重載
13.1 擴展函數
定義:表示即使在不修改某個類的源碼的情況下干发,仍然可以打開這個類,向該類添加新的函數
- 以一個功能為例史翘,如果我們想統(tǒng)計字符串中字母的數量枉长,那么我們該如何實現這個功能呢?
object StringUtil{
fun count(str:String?) : Int{
var count = 0
str?.let{
for(char in it){
if(char.isLetter()){
count++
}
}
return count
}
return 0
}
}
- 上面這種寫法沒有問題琼讽,可以正常使用必峰。但是有了擴展函數后,我們就可以用擴展函數將count()方法添加到String類中钻蹬。
定義擴展函數的語法結構吼蚁,相比于定義一個普通的函數,定義擴展函數只需要在函數名的前面加上
類名.
的語法結構脉让,就表示將該函數添加到指定類中了
fun className.merthodName(param1:Int,param2:Int):Int{
return 0
}
- 使用擴展函數將count()函數添加進String類中的方法:向哪個類添加擴展函數就定義一個同名的Kotlin文件桂敛,由于我們希望向String類中添加擴展函數功炮,那么我們就需要先創(chuàng)建對應的Kotlin文件溅潜。在Kotlin文件中編寫如下代碼
fun String.count():Int{
var count = 0
this?.let{
for(char in it){
if(char.isLetter()){
count++
}
}
return count
}
return 0
}
- 我們將count()方法定義成String類的擴展函數,那么該函數就自動擁有了String實例的上下文薪伏,因此該函數就不用接收一個字符串參數了滚澜,而是直接遍歷this即可,因為this就代表著字符串本身嫁怀。而我們之后就可以使用該擴展函數了
val count = "12s2w3e".count()
13.2 運算符重載
- PS:本人覺得沒太大用设捐,雖然簡化了代碼量,但是增加了代碼閱讀的困難度塘淑,不適用于公共開發(fā)萝招。
14. 高階函數
14.1 定義高階函數
高階函數與Lambda的關系是密不可分的。像map,rum,apply這種接收Lambda參數的函數可以被稱為具有函數式編程風格的api存捺,如果我們想自己定義函數式api槐沼,就得借助高階函數來實現。
什么是高階函數捌治?
所謂高階函數岗钩,就是一個函數接收另一個函數作為參數,或者返回值的類型是另一個函數肖油,那么該函數就可以被稱為高階函數
- 高階函數需要以另一個參數作為參數或者是返回值兼吓,那么怎么以一個函數作為參數呢?這就涉及到Kotlin新增的另一個概念:函數類型森枪。這個函數類型類似于整形视搏,浮點型审孽,布爾型,是Kotlin中新增的凶朗。定義一個函數類型的方式如下:
(String, Int) -> Unit
- 定義一個函數類型瓷胧,最關鍵的是要聲明該函數接收什么參數,與它的返回值類型是什么棚愤。因此搓萧,
->
左邊表示聲明該函數需要傳入的參數,多個參數之間使用逗號隔開宛畦,如果沒有參數瘸洛,使用一對空括號就可以了,->
右邊表示聲明該函數的返回值是什么類型次和,如果沒有返回值就使用Unit反肋,它大致相當于Java中的void,以下邊函數為例
fun example(func:(String,Int)->Unit){
// 函數名 參 數 返回值
func("hello",123)
}
- 可以看到example()函數接收了一個函數類型的參數踏施,因此example()函數就是一個高階函數石蔗。調用一個函數類型的參數,它的語法類似于調用一個普通的函數畅形。但是上面這個例子沒有辦法直觀的體現出高階函數的作用养距,那么這種函數具體有什么用途呢?
- 簡單概括一下高階函數的用途:高階函數允許讓函數類型的參數來決定函數的執(zhí)行邏輯日熬,即使是在同一個高階函數棍厌,只要傳入不同的函數類型參數,那么它的執(zhí)行邏輯與最終的返回結果就可能是完全不同的竖席。舉個下面這個栗子說明耘纱。
定義一個num1Andnum2()的函數,參數類型為兩個整形與一個函數類型毕荐。再定義兩個與其函數類型匹配的函數束析。
fun num1Andnum2(num1:Int,num2:Int,operation(Int,Int)->Int){
val result = operation(num1,num2)
return result
}
fun plus(num1:Int,num2:Int) : Int {
return num1 + num2
}
fun minus(num1:Int,num2:Int) : Int {
return num1 - num2
}
在main函數中編寫如下代碼
fun main() {
val num1 = 5
val num2 = 10
val result1 = num1Andnum2(5,10,::plus)
val result2 = num1Andnum2(5,10,::minus)
println("result1= "+ $result1)
println("result2=" + $result2)
}
- 注意這里調用num1Andnum2()函數的方式,第三個參數使用了
::plus
與::minus
這種寫法憎亚,表示將這兩個函數作為參數傳遞給num1Andnum2()函數员寇,然后num1Andnum2()函數根據傳入的函數類型參數決定具體的計算邏輯。
這種寫法雖然可以正常工作虽填,但是寫法是不是太復雜了丁恭,每次調用任何高階函數的時候都還需要定義一個與其函數類型相匹配到的函數,沒有起到簡化代碼的作用斋日。因此牲览,kotlin支持更多的方式來調用高階函數,比如Lambda表達式,匿名函數第献,成員引用等贡必。其中Lambda表達式是最常見最普遍的高階函數調用方式
- 將上述方法用Lambda表達式來實現,可以寫為:
fun main() {
val a = 10
val b = 5
val result1 = num1AndNum2(a,b){a,b -> a + b}
val result2 = num1AndNum2(a,b){a,b -> a - b}
println("result1 is $result1 ")
println("result2 is $result2 ")
}
- Lambda表達式同樣可以完整地表達一個函數的參數聲明和返回值聲明庸毫,但是寫法會更加簡單
繼續(xù)探究高階函數的使用仔拟。回顧之前之前學習的apply函數飒赃,該函數可以給Lambda表達式提供指定的上下文利花,當需要連續(xù)調用同一個對象的多個方法時,這個函數可以讓代碼變得更加精簡载佳。比如StringBuilder炒事,學習了高階函數后我們就可以用高階函數來模仿一個類似的功能。
fun StringBuilder.build(block : StringBuilder.() -> Unit):StringBuilder{
block()
}
- 這里給StringBuilder定義了一個擴展函數蔫慧,該擴展函數接收一個函數類型參數挠乳,返回值為StringBuilder類型。
- 注意姑躲,聲明該高階函數與之前的例子又有些不同:他在函數類型參數前面加了個
StringBuilder.
睡扬,這是什么意思呢?在函數類型參數的前面加上ClassName黍析,就表示這個函數類型參數定義在對應的類中卖怜,這里就是講函數類型參數定義在StringBuilder中。 - 但是這樣子有什么好處呢橄仍?好處就是韧涨,當我們調用build函數時傳入的Lambda表達式會自動擁有StringBuilder的上下文牍戚,與apply函數非常相似侮繁。
14.2 內聯函數
14.2.1 高階函數的實現原理
- 仍然用剛剛的 num1Andnum2函數舉例,代碼如下
fun main() {
val a = 10
val b = 5
val result1 = num1AndNum2(a,b){a,b -> a + b}
val result2 = num1AndNum2(a,b){a,b -> a - b}
println("result1 is $result1 ")
println("result2 is $result2 ")
}
fun num1Andnum2(num1:Int,num2:Int,operation(Int,Int)->Int){
val result = operation(num1,num2)
return result
}
- 上述代碼調用了 num1Andnum2函數如孝,并通過Lambda表達式指定對傳入的兩個整形參數進行求和宪哩。但是上述調用方法再Kotlin中比較好理解,比較基礎的高階函數用法第晰∷希可是Kotlin最終還是要編譯成Java字節(jié)碼的,但是Java中并沒有高階函數的概念茁瘦,那么Kotlin是如何讓Java來支持這種高階函數的用法的品抽?Kotlin強大的編譯器會將這些高階函數的語法轉換成Java支持的語法結構,上述的Kotlin代碼大致會被轉換成如下Java代碼
public static int num1AndNum2(int num1,int num2,Function operation){
int result = (int)operation.invoke(num1,num2)
return result;
}
public static void main(){
int num1 =100;
int num2 =80;
int result = num1AndNum2(num1,num2,new Function(){
@Override
public Integer invoke(Integer n1,Integer n2){
return n1 + n2;
}
});
}
- 這就表明甜熔,我們一直使用的Lambda表達式在底層被轉換為了匿名類的實現方式圆恤,每當調用一次Lambda表達式,都會創(chuàng)建一個新的匿名對象腔稀,造成額外的性能開銷盆昙。為了解決這個問題羽历,Kotlin提供了內聯函數的功能,可以將Lambda表達式帶來的運行時開銷完全消除淡喜。
定義內聯函數的方式很簡單秕磷,定義高階函數時加上inline關鍵字的聲明即可。內聯函數的工作原理就是炼团,Kotlin編譯器會將內聯函數中的代碼在編譯時自動替換到調用它的地方
inline fun num1AndNum2(num1:Int , num2 : Int , operation : (Int , Int) -> Int) : Int{
val result = operation(num1,num2)
return result
}
14.2.2 noinline和crossinLine
noinline
之前討論的情況是澎嚣,一個高階函數只接受了一個函數類型參數,如果一個高階函數中接受了兩個或更多的函數類型參數瘟芝,這是我們加上inline關鍵字币叹,Kotlin編譯器會將所有引用的Lambda表達式全部替換,但是我們只想內聯其中一個的話該怎么辦呢?這時模狭,我們可以使用
noinline
關鍵字
inline fun inlineTest(block1 : () -> Unit , noline block2 : () -> Unit){}
- 原本block1與block2所引用函數類型都會被內聯颈抚,但我們在block2參數的前面加上noinline關鍵字,那么就只會對block1參數所引用的Lambda表達式進行內聯了
- 但是我們在上一小節(jié)已經說完內聯的好處了嚼鹉,它可以減少系統(tǒng)的開銷贩汉,那么為什么我還用
noinline
關鍵字取消內聯呢?因為內聯的函數類型參數在編譯的時候會進行代碼替換锚赤,因此它沒有真正的參數屬性匹舞,而非內聯的函數類型參數可以自由地傳遞給其他函數,因為它就是一個真是的參數线脚,而內聯的函數類型參數只允許傳遞給另外一個內聯函數赐稽,這也是它最大的局限性,因此可以這么理解內聯函數=一段代碼浑侥,非內聯函數=一個真正的函數參數姊舵。 - 另外,內聯函數與非內聯函數還有一個重要的區(qū)別寓落,那就是內聯函數所引用的Lambda表達式中可以使用
return
關鍵字返回括丁,非內聯函數只能使用局部返回
fun printString(string: String, block1: (String) -> Unit) {
printlin("printString start")
block1(string)
printlin("printString end")
}
fun main() {
println("main start")
val str = ""
printString(str){s ->
println("lambda start")
if(s.isEmpty())return@printString
println(s)
println("lambda end")
}
println("main end")
}
- 以上函數的結果為除了“l(fā)ambda end”這句沒有輸出,其余皆輸出伶选。注意的是史飞,Lambda表達式中是不允許直接使用return關鍵字的,代碼中的
return@printString
代表局部返回 - 但是如果我們將printString()函數聲明成一個內聯函數仰税,那么情況就不一樣了
inline fun printString(string: String, block1: (String) -> Unit) {
printlin("printString start")
block1(string)
printlin("printString end")
}
fun main() {
println("main start")
val str = ""
printString(str){s ->
println("lambda start")
if(s.isEmpty())return
println(s)
println("lambda end")
}
println("main end")
}
- 輸出的結果為除"lambda end"與"main end"外全打印构资。因為內聯函數本質上是代碼替換,因此可以將上述函數寫為
fun main() {
println("main start")
val str = ""
printlin("printString start")
println("lambda start")
if(s.isEmpty())return
println(s)
println("lambda end")
printlin("printString end")
println("main end")
}
crossinline
- 將高階函數聲明為內聯函數是一種良好的編程習慣陨簇,大多數高階函數都可以直接聲明為內聯函數的吐绵,但是也有少數情況,如下。
inline fun runRunnable(block()->Unit){
val runnable = Runnable{//本質上為匿名對象拦赠,只能使用局部返回
block()//內聯函數巍沙,可以使用return
}
runnable.run()
}
- 這個出現錯誤的原因比較復雜。首先荷鼠,在runRunnable()函數中句携,我們創(chuàng)建了一個Runnable對象,并在Runna ble的Lambda表達式中調用了傳入的函數類型參數允乐,之前也講過矮嫉,編譯器實際上會將Lambda表達式編譯為匿名內部類的方式,也就是說上述方法是在匿名對象中調用了傳入的函數類型參數牍疏。
- 內聯函數所引用的Lambda表達式允許使用
return
關鍵字進行函數返回蠢笋,但是由于我們是在匿名內部類中調用的函數類型參數,最多只能對匿名類中的函數調用進行返回鳞陨。因此如果我們在高階函數中創(chuàng)建了另外的Lambda表達式或者匿名類的實現昨寞,并且在這些實現中調用函數類型參數,此時再將高階函數聲明為內聯函數就一定會出錯厦滤。這時就需要使用crossinLine
關鍵字援岩。
inline fun runRunnable(crossinLine block()->Unit){
val runnable = Runnable{//本質上為匿名對象,只能使用局部返回
block()//內聯函數掏导,可以使用return
}
runnable.run()
}
- crossinLine關鍵字就像一個契約享怀,保證在內聯函數的Lambda表達式中一定不會使用return關鍵字,這樣就不會有矛盾了趟咆。