1. 簡(jiǎn)介
- Java代碼編譯后生成的并不是計(jì)算機(jī)可以識(shí)別的二進(jìn)制語(yǔ)言,而是特殊的class文件,這種class文件只有java虛擬機(jī)才能識(shí)別,而這個(gè)虛擬機(jī)其實(shí)擔(dān)任的就是解釋器的角色,會(huì)在程序運(yùn)行時(shí)將編譯后的class文件解釋成計(jì)算機(jī)可以識(shí)別的二進(jìn)制數(shù)據(jù)。因此,java嚴(yán)格來(lái)說(shuō)屬于解釋性語(yǔ)言。
-
Kotlin的工作原理:如同java原理一樣,使用自己的編譯器將代碼編譯成與java虛擬機(jī)編譯出的相同規(guī)格class文件,java虛擬機(jī)依然可以識(shí)別出由kotlin編寫出的class文件。 -
kotlin相較于java來(lái)說(shuō),語(yǔ)法更簡(jiǎn)潔,更高級(jí),甚至幾乎杜絕了空指針異常問(wèn)題的出現(xiàn)。
2. 編程之本:函數(shù)與變量
2.1 變量
1. Kotlin定義一個(gè)變量,只允許在變量前聲明兩種關(guān)鍵字:var和val
- (1)
val:value的簡(jiǎn)寫,用來(lái)聲明一個(gè)不可變的變量,該變量在初始賦值后就不能再次賦值,對(duì)應(yīng)java中的被final修飾的變量。 - (2)
var:variable的簡(jiǎn)寫,用來(lái)聲明一個(gè)可變的變量,該變量在初始賦值后還可以再次賦值,對(duì)應(yīng)java中的非final變量。 - (3)
Kotlin具有出色的類型推導(dǎo)機(jī)制,如果一個(gè)變量在初始聲明的時(shí)候就被賦值,那么Kotlin就會(huì)自動(dòng)推導(dǎo)出該變量的類型
2. Kotlin中的代碼每一行的結(jié)尾不加分號(hào)
fun main() {
val a = 10
println("a= "+ a )
}
3. 類型推導(dǎo)機(jī)制
-
Kotlin具有出色的類型推導(dǎo)機(jī)制,如果一個(gè)變量在初始聲明的時(shí)候就被賦值,那么Kotlin就會(huì)自動(dòng)推導(dǎo)出該變量的類型。 - 但是如果對(duì)一個(gè)變量延遲賦值,則
Kotlin就無(wú)法自動(dòng)推導(dǎo)該變量的類型,這個(gè)時(shí)候就需要顯示的聲明該變量的類型。
var a = 10
or
var a : Int = 10
or
val a : Int
a= 10
4. 數(shù)據(jù)類型
- 如果足夠細(xì)心,你就會(huì)發(fā)現(xiàn),在剛剛的代碼中,數(shù)據(jù)類型
Int的I是大寫的,而java中的int的首字母是小寫的。這代表Kotlin完全拋棄了java中的基本數(shù)據(jù)類型,而全部改用對(duì)象數(shù)據(jù)類型。
Java Kotlin 數(shù)據(jù)類型說(shuō)明
int Int 整形
long Long 長(zhǎng)整形
short Short 短整形
float Float 單精度浮點(diǎn)型
double Double 雙精度浮點(diǎn)型
boolean Boolean 布爾型
char Char 字符型
byte Byte 字節(jié)型
- 什么時(shí)候使用
var?什么時(shí)候使用val?- 建議永遠(yuǎn)先使用
val!val!val!聲明一個(gè)變量,當(dāng)val沒(méi)有辦法滿足要求時(shí)在使用var。這樣設(shè)計(jì)出來(lái)的程序會(huì)更健壯,更符合高質(zhì)量編碼規(guī)范。
- 建議永遠(yuǎn)先使用
2.2 函數(shù)
1. 語(yǔ)法規(guī)則
①fun ②methodName(③param1: Int,param2:Int):④Int{
⑤return 0
}
- ①:定義函數(shù)的關(guān)鍵字
- ②:函數(shù)名
- ③:參數(shù)列表,
param1: Int,冒號(hào)前面為變量名,后面為變量類型 - ④:函數(shù)返回類型
- ⑤:函數(shù)體
2. 語(yǔ)法糖
- 當(dāng)一個(gè)函數(shù)只有一行代碼的時(shí)候,可以直接將唯一的一行代碼寫在函數(shù)定義部分的尾部,中間用等號(hào)連接即可
fun largerNumber(num1:Int,num2:Int) = max(num1,num2)
- 可以發(fā)現(xiàn)該函數(shù)省略了大括號(hào),return,以及函數(shù)返回值類型,這依賴于kotlin的類型推導(dǎo)機(jī)制。max()方法返回的是一個(gè)Int值,而我們?cè)趌argerNumber函數(shù)的尾部又銜接了max函數(shù),因此kotlin可以推導(dǎo)出largerNumber函數(shù)的返回值類型也必然是Int類型。
3. 邏輯控制
3.1 if條件語(yǔ)句
- Kotlin中的if語(yǔ)句與Java中的if語(yǔ)句幾乎是沒(méi)有區(qū)別的,但是也并不是完全相同
- Kotlin中的if語(yǔ)句是帶有返回值的
fun largerNumber(a:Int,b:Int):Int{
val c = if(a>b){
a
}else{
b
}
}
- 而且我們可以進(jìn)一步將上述代碼簡(jiǎn)化一下
fun largeNumber(a:Int,b:Int) = if(a>b){
a
}else{
b
}
3.2 when條件語(yǔ)句
- 與if一樣,也是有返回值的,因此也可以使用語(yǔ)法糖
- 使用方式:
when(params){
匹配值 ->{執(zhí)行邏輯}
}
- 當(dāng)執(zhí)行邏輯只有一句話的時(shí)候,{}可以省略
- 除了精確匹配外,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還有一種不帶參數(shù)的用法
fun getScore_2(name:String) = when{
name.startsWith("Tom") ->55
name == "Jim" ->34
name == "Lily" -> 35
else->0
}
- 可以發(fā)現(xiàn)Kotlin中判斷字符串和對(duì)象是否相等可以直接使用==,而不用像java一樣使用equals()
3.3 循環(huán)語(yǔ)句
3.3.1 while循環(huán)語(yǔ)句
- kotlin中的while循環(huán)語(yǔ)句與java中的while循環(huán)語(yǔ)句完全相同。、
3.3.2 for循環(huán)語(yǔ)句
- Kotlin中的for循環(huán)進(jìn)行了很大的修改。Java中常用的for-i循環(huán)在kotlin中直接被舍棄了,而另一種for-each則被kotlin進(jìn)行了大幅度的加強(qiáng),變成了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)建了一個(gè)0到10的區(qū)間,且兩端都是閉區(qū)間。在..的兩邊指定區(qū)間的左右端點(diǎn)就可以創(chuàng)建一個(gè)區(qū)間了-
for(i in 0..10)代表的是遍歷0到10之間的數(shù)字
-
-
until關(guān)鍵字表示:創(chuàng)建一個(gè)左閉右開的區(qū)間 -
step的作用:for(i in 0 until 10 step 2)的意思是每次循環(huán)都會(huì)在區(qū)間范圍內(nèi)加2 -
downTo:創(chuàng)建一個(gè)降序閉區(qū)間
4. 繼承與構(gòu)造函數(shù)
4.1 繼承
4.1.1 Kotlin中的繼承與Java中的區(qū)別
- Kotlin在繼承這方面與java有些不同;Kotlin中任何一個(gè)非抽象類默認(rèn)都是不可以被繼承的,相當(dāng)于java中的類被final修飾。
-
Effective Java中明確提到如果一個(gè)類不是專門為了繼承而設(shè)計(jì)的,那么就應(yīng)該主動(dòng)將他加上final聲明,禁止其可以被繼承。
4.1.2 如何實(shí)現(xiàn)繼承
- 為了使Kotlin中的類可以被繼承,我們需要用
open關(guān)鍵字來(lái)修飾該類
open class Person{
var name =""
var age = 0
fun eat(){
}
}
class Student : Person(){
var sno = 0
var grade = 0
}
4.2 構(gòu)造函數(shù)
- 細(xì)品的話會(huì)發(fā)現(xiàn),上述代碼實(shí)現(xiàn)繼承時(shí),繼承Person類后面還跟著一個(gè)括號(hào),這個(gè)跟java中也不相同。這涉及到Kotlin中的主構(gòu)造函數(shù)與次構(gòu)造函數(shù)。
- 在Java與Kotlin中,任何一個(gè)類都會(huì)有構(gòu)造函數(shù)。在Kotlin中,構(gòu)造函數(shù)被分為兩類:主構(gòu)造函數(shù)與次構(gòu)造函數(shù)
(1) 主構(gòu)造函數(shù):
- 主構(gòu)造函數(shù)是我們最常用的構(gòu)造函數(shù),每一個(gè)類都會(huì)有一個(gè)默認(rèn)的不帶參數(shù)的主構(gòu)造函數(shù)。當(dāng)然,我們也可以顯示的給它指明參數(shù)。
- 特點(diǎn):沒(méi)有函數(shù)體,直接定義在類名后面即可
class Student(var sno:String,var grade:Int) : Person(){
//no func body
}
- 如果我們需要?jiǎng)?chuàng)建一個(gè)Student對(duì)象,則只需要以下步驟,就能獲取一個(gè)student對(duì)象了
val student = Student("1",1)
- 雖然主構(gòu)造函數(shù)沒(méi)有函數(shù)體,但是如果我們又想在主構(gòu)造函數(shù)中加入一些邏輯,這樣該怎么辦呢?Kotlin中提供了
init結(jié)構(gòu)體,所有主構(gòu)造函數(shù)的邏輯可以寫在里面
class Student(var sno:String,var grade:Int) : Person(){
init{
//TODO
}
}
- 說(shuō)了這么多還是沒(méi)有涉及到Person后跟著的括號(hào),那么到底有什么關(guān)系呢?這涉及到繼承中的一個(gè)性質(zhì),子類的構(gòu)造函數(shù)必須要調(diào)用父類的構(gòu)造函數(shù),這個(gè)規(guī)定在Kotlin中也要遵守。因此我們就會(huì)發(fā)現(xiàn),既然Kotlin中主構(gòu)造函數(shù)沒(méi)有函數(shù)體,那我們?cè)撊绾握{(diào)用父類的構(gòu)造函數(shù)呢?第一種辦法,寫在init結(jié)構(gòu)體中,按理說(shuō)這樣可以,但是我們?cè)诖蠖鄶?shù)場(chǎng)景中是不需要寫init結(jié)構(gòu)體的;第二種,就是我們說(shuō)的括號(hào)。子類的主構(gòu)造函數(shù)調(diào)用父類的哪個(gè)構(gòu)造函數(shù),在繼承的時(shí)候通過(guò)括號(hào)來(lái)進(jìn)行指定。
class Student(var sno:String,var grade:Int) : Person(){}
//這個(gè)括號(hào)就代表調(diào)用的是Person中的無(wú)參構(gòu)造函數(shù)
- 即使在無(wú)參數(shù)的情況下,這對(duì)括號(hào)也不能省略。
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age){}
//這個(gè)括號(hào)就代表調(diào)用的是Person中帶有對(duì)應(yīng)參數(shù)的構(gòu)造函數(shù)
- 注意,我們?cè)谙騍tudent類的主構(gòu)造函數(shù)中增加的
name和age這兩個(gè)字段時(shí),不能再將它們聲明成val或var,因?yàn)?strong>在主構(gòu)造函數(shù)中聲明成val或var的參數(shù)將自動(dòng)成為該類的字段,這會(huì)導(dǎo)致與父類中的同名的參數(shù)發(fā)生沖突。因此,這里的name和age參數(shù)前面不用加任何關(guān)鍵字,使其作用僅限于主構(gòu)造函數(shù)中即可。
(2) 次構(gòu)造函數(shù)
- 任何一個(gè)類只能有一個(gè)主構(gòu)造函數(shù),但是可以有多個(gè)次構(gòu)造函數(shù)。次構(gòu)造函數(shù)可以用于實(shí)例化一個(gè)類,不過(guò)與主構(gòu)造函數(shù)不同的是,它是有函數(shù)體的。
- Kotlin中規(guī)定:次構(gòu)造函數(shù)是通過(guò)
constructor關(guān)鍵字來(lái)定義的,當(dāng)一個(gè)類既有主構(gòu)造函數(shù)又有次構(gòu)造函數(shù)時(shí),所有的次構(gòu)造函數(shù)必須調(diào)用主題構(gòu)造函數(shù)(包括間接調(diào)用)。舉一個(gè)簡(jiǎn)單栗子:
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age){
//直接調(diào)用主構(gòu)造函數(shù)
constructor(name:String,age:Int):this("",0,name,age){
//TODO
}
//間接調(diào)用,調(diào)用上一個(gè)次構(gòu)造函數(shù),間接調(diào)用主構(gòu)造函數(shù)
constructor():this("Tom",2){
//TODO
}
}
- 還有一種特殊的情況:類中只有次構(gòu)造函數(shù),沒(méi)有主構(gòu)造函數(shù)。Kotlin中,當(dāng)一個(gè)類沒(méi)有顯示定義主構(gòu)造函數(shù),且定義了次構(gòu)造函數(shù)時(shí),那么這個(gè)類就是沒(méi)有主構(gòu)造函數(shù)的。
class Teacher : Person{
constructor(name:String,age:Int):super(name,age)
}
- 我們來(lái)分析一下:首先,Teacher類中的后面沒(méi)有顯示的定義主構(gòu)造函數(shù),同時(shí)又因?yàn)槎x了次構(gòu)造函數(shù),所以目前的情況下,Teacher類是沒(méi)有主構(gòu)造函數(shù)的。既然沒(méi)有主構(gòu)造函數(shù)了,繼承Person類的時(shí)候也就不需要加上括號(hào)了。然后,由于沒(méi)有主構(gòu)造函數(shù),次構(gòu)造函數(shù)只能直接調(diào)用父類的構(gòu)造函數(shù)了。
5. 接口與修飾符
5.1 接口
- Kotlin中接口部分與Java中幾乎是一致的。Java是單繼承的,每一個(gè)類最多只能繼承一個(gè)父類,但是卻可以實(shí)現(xiàn)多個(gè)接口,Kotlin也是如此。
- 接口中的函數(shù)不要求有函數(shù)體。
interface Study{
fun readBook()
fun doHomework()
}
- 用之前定義的Student類進(jìn)行舉例,讓其實(shí)現(xiàn)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ā)現(xiàn),Kotlin中統(tǒng)一用冒號(hào)來(lái)表示繼承和實(shí)現(xiàn),中間用逗號(hào)進(jìn)行分隔。并且接口后面不需要加上括號(hào),因?yàn)樗鼪](méi)有構(gòu)造函數(shù)可以去調(diào)用。
- Kotlin中為了讓接口的功能更加靈活,增加了這樣一個(gè)功能:允許對(duì)接口中定義的函數(shù)進(jìn)行默認(rèn)實(shí)現(xiàn)。所謂的默認(rèn)實(shí)現(xiàn)是指:接口中的一個(gè)函數(shù)具有了函數(shù)體,這個(gè)函數(shù)體中的內(nèi)容就是它的默認(rèn)實(shí)現(xiàn)。以Study接口為例,一個(gè)類實(shí)現(xiàn)了Study接口時(shí),只會(huì)強(qiáng)制要求實(shí)現(xiàn)readBook()函數(shù),而doHomework()可以自由的選擇是否實(shí)現(xiàn),如果實(shí)現(xiàn)就是使用實(shí)現(xiàn)后的邏輯,如果不實(shí)現(xiàn)則使用默認(rèn)的邏輯。
interface Study{
fun readBook()
fun doHomework(){
println("do homework default impl")
}
}
5.2 可見(jiàn)性修飾符
我們可以通過(guò)一個(gè)表格來(lái)了解Java中的修飾符與Kotlin中的修飾符的區(qū)別。
修飾符 JAVA Kotlin
public 所有類可見(jiàn) 所有類可見(jiàn)(默認(rèn))
protected 當(dāng)前類,子類,同 當(dāng)前類,子類可見(jiàn)
一包中的類可見(jiàn)
private 當(dāng)前類可見(jiàn) 當(dāng)前類可見(jiàn)
default 同一包路徑下的類 無(wú)
可見(jiàn)(默認(rèn))
internal 無(wú) 同一模塊中的類可見(jiàn)
6. 數(shù)據(jù)類與單例類
6.1 數(shù)據(jù)類
- 數(shù)據(jù)類一般都占據(jù)著很重要的角色,用于將服務(wù)器端或數(shù)據(jù)庫(kù)中的數(shù)據(jù)映射到內(nèi)存中,為編程邏輯提供數(shù)據(jù)模型的支持。
- Java中的數(shù)據(jù)類,也叫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中的數(shù)據(jù)類比較復(fù)雜,無(wú)意義代碼較多。Kotlin中數(shù)據(jù)類的實(shí)現(xiàn)方式則極其簡(jiǎn)單:
data class Phone(val brand:String,val price:Double)
- 是的,你沒(méi)有看錯(cuò),僅僅需要這一行代碼,就能實(shí)現(xiàn)Kotlin中的數(shù)據(jù)類。神奇的地方就在于
data這個(gè)關(guān)鍵字,在Kotlin中當(dāng)一個(gè)類前面聲明了data關(guān)鍵字時(shí),就表明你希望這個(gè)類是一個(gè)數(shù)據(jù)類。Kotlin中會(huì)跟主構(gòu)造函數(shù)中的參數(shù)幫你將equals()、hashCode()、toString()’等固定且無(wú)意義的方法自動(dòng)生成。并且,我們可以發(fā)現(xiàn)這個(gè)類是沒(méi)有大括號(hào)的,在Kotlin中,當(dāng)一個(gè)類中沒(méi)有任何代碼時(shí),還可以將大括號(hào)省略掉。
6.2 單例類
- Kotlin中將固定的,重復(fù)的邏輯隱藏了起來(lái),只暴露給我們最簡(jiǎn)單的用法。創(chuàng)建單例類時(shí),僅需要將
class更改為object即可。
object Singleton{
fun singletonTest)(){
//TODO
}
}
- 在Kotlin中我們不需要私有化構(gòu)造函數(shù),也不需要提供
getInstance()這種靜態(tài)方法,只需要把class關(guān)鍵字改為object即可。調(diào)用方法為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")
}
- 但對(duì)于Kotlin來(lái)說(shuō),上述這種方式比較繁瑣,Kotlin中內(nèi)置了
listOf()來(lái)簡(jiǎn)化初始化集合的方法。僅用一行代碼就可以實(shí)現(xiàn)集合的初始化操作。不過(guò)需要注意的是,listOf()函數(shù)創(chuàng)建的是不可變的集合,意思是被創(chuàng)建的集合無(wú)法進(jìn)行添加,刪除或修改操作。
val list = listOf("one","two","three")
- 如果我們需要?jiǎng)?chuàng)建一個(gè)可變集合的話,使用
mutableListOf()函數(shù)即可.。
val list = mutableListOf("one","two","three")
- 創(chuàng)建Set集合的方法與List集合幾乎一模一樣。只是將創(chuàng)建集合的方式更換為
setOf()與mutableSetOf()。需要注意的是由于set集合底層是使用hash映射機(jī)制來(lái)存儲(chǔ)的,因此set集合中的元素是無(wú)序的。
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中其實(shí)不建議使用
put()與get()方法來(lái)對(duì)Map進(jìn)行添加和讀取數(shù)據(jù)操作,而是更加推薦使用一種類似于數(shù)組下標(biāo)的語(yǔ)法結(jié)構(gòu)。
fun createMap2(){
val map = HashMap<String,Int>()
map["one"] = 1
map["two"] = 2
map["three"] = 3
}
- 但是Kotlin依然覺(jué)得這種方式太麻煩了,因此如同list與set一樣,Kotlin依然提供了一對(duì)
mapOf()與mutableMapOf()方法來(lái)簡(jiǎn)化Map。我們以mapOf()來(lái)舉例。
val map = mapOf("one" to 1,"two" to 2,"three" to 3)
- 遍歷map集合的方法依然是
forin。只不過(guò)需要把一對(duì)(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 集合的函數(shù)式API
7.2.1 Lambda表達(dá)式
1. 首先學(xué)習(xí)函數(shù)式API的語(yǔ)法結(jié)構(gòu),也就是Lamdba表達(dá)式的語(yǔ)法結(jié)構(gòu)。
- 按照我們?cè)趈ava中的思路,如果我們想要找出單詞最長(zhǎng)的水果名,這個(gè)算法該如何寫呢
fun maxLength(){
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit=""
for(fruit in list){
if (maxLengthFruit.length<fruit.length){
maxLengthFruit = fruit
}
}
}
- 但是在Kotlin中我們可以使用Lambda表達(dá)式來(lái)使這段代碼更簡(jiǎn)潔
fun maxLengthLambda() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy { it.length }
}
- 只用一行代碼就能完成這個(gè)功能
2. 理解Lambda表達(dá)式的簡(jiǎn)化步驟
(1)Lamda的定義:一小段可以作為參數(shù)傳遞的代碼
(2)語(yǔ)法結(jié)構(gòu):
{paramsName 1: paramsType,paramsName 2: paramsType -> funcBody}
- 首先,最外層是一層大括號(hào)
- 如果有參數(shù)傳入的話,我們還需要聲明參數(shù)列表
- 參數(shù)列表的結(jié)尾用
->表示參數(shù)列表的結(jié)束語(yǔ)函數(shù)體的開始 - 函數(shù)體中可以編寫任意行代碼(但不建議太長(zhǎng)),并且最后一行代碼會(huì)自動(dòng)作為L(zhǎng)ambda表達(dá)式的返回值
(3)正常的Lambda表達(dá)式
- 在剛剛的Lambda表達(dá)式中使用的
maxBy函數(shù)就是一個(gè)普通的函數(shù),只不過(guò)接收的是Lambda類型的參數(shù),并且在遍歷集合時(shí)將每次遍歷的值作為參數(shù)傳遞給Lambda表達(dá)式。這個(gè)函數(shù)的工作原理是:根據(jù)我們傳入的條件來(lái)遍歷集合,從而找到該條件下的最大值。
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var lambda = {fruit:String -> fruit.length}
var maxLengthFruit = list.maxBy(lambda)
}
(4) 開始簡(jiǎn)化
- 第一步,我們并不需要定義一個(gè)Lambda變量,可以直接將這個(gè)表達(dá)式傳入函數(shù)中
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy({fruit:String -> fruit.length})
}
- 第二步,在Kotlin中規(guī)定,當(dāng)Lambda表達(dá)式作為函數(shù)的最后一個(gè)參數(shù)時(shí),可以將其移到括號(hào)外邊
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy(){fruit:String -> fruit.length}
}
- 第三步,如果lambda表達(dá)式是函數(shù)的唯一一個(gè)參數(shù)時(shí),還可以將函數(shù)的括號(hào)省略
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy{fruit:String -> fruit.length}
}
- 第四步,由于Lambda具有出色的類型推導(dǎo)機(jī)制,因此大多數(shù)情況Lambda表達(dá)式中的參數(shù)列表其實(shí)在大多數(shù)情況下不必聲明參數(shù)類型。
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy{fruit -> fruit.length}
}
- 第五步,當(dāng)Lambda表達(dá)式中只有一個(gè)參數(shù)時(shí),也不必聲明參數(shù)名,而是可以直接使用
it來(lái)代替,那么代碼就會(huì)變成最初的那樣
fun maxLengthLambdaNormal() {
val list = listOf<String>("Apple","Banana","Pear")
var maxLengthFruit = list.maxBy{ it.length}
}
7.2.2 函數(shù)式API的相互配合
1. map函數(shù)與filter函數(shù)
- 集合中的map函數(shù)是非常常用的一種數(shù)據(jù)結(jié)構(gòu),用于將集合中的每個(gè)元素都映射成另外的值,映射的規(guī)則由Lambda表達(dá)式指定,最終形成一個(gè)新的集合。
- 舉個(gè)??;這樣會(huì)將原list中的單詞全部變?yōu)榇髮懩J?,除此之外,我們還可以將單詞全轉(zhuǎn)換為小寫,或只取單詞首字母,結(jié)果如何是根據(jù)Lambda中的規(guī)則來(lái)的。
val list = listOf<String>("Apple","Banana","Pear")
var newList = list.map{ it.toUpperCase()}
for(fruit in newList){
println(fruit)
}
- filter函數(shù)是用來(lái)過(guò)濾集合中的數(shù)據(jù)的,并將過(guò)濾后的數(shù)據(jù)返回為一個(gè)新的集合??梢詥为?dú)使用,也可以配合剛才的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函數(shù)
- any函數(shù)是用來(lái)判斷集合中是否至少存在一個(gè)元素滿足指定條件。
- all函數(shù)是用來(lái)判斷集合中是否所有元素都滿足指定條件。
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函數(shù)式API的使用
- 如果我們?cè)贙otlin代碼中調(diào)用了一個(gè)java方法,并且讓方法接收一個(gè)Java單抽象方法接口,這種情況下可以使用函數(shù)式API。
- 單抽象方法接口:接口中只有一個(gè)抽象方法。
- 以Java原生API中一個(gè)最常見(jiàn)的單抽象方法為例--Runnable接口。該接口中只有一個(gè)待實(shí)現(xiàn)的run()方法。在java中使用Runnable接口的方法是這樣的:
new Thread(new Runnable(){
@Override
public void run(){
//func body
}
}).start();
- 如果將Java版本的代碼改為Kotlin版本,那么結(jié)果是什么呢?
Thread(object:Runnable{
override fun run(){
println("Hello ")
}
}).start()
- 由于Kotlin完全舍棄了new關(guān)鍵字,因此創(chuàng)建匿名類實(shí)例的時(shí)候不能在使用new,而是改用了object關(guān)鍵字。但是目前Thread的構(gòu)造方法是符合Java函數(shù)式API的使用條件的,因此我們可以對(duì)其進(jìn)行精簡(jiǎn)。一下這段代碼就是精簡(jiǎn)的結(jié)果。因?yàn)镽unnable類中只有一個(gè)待實(shí)現(xiàn)方法,即便沒(méi)有顯示的重寫run()方法,Kotlin也能自動(dòng)明白R(shí)unnable后面的Lambda表達(dá)式就是要在run()方法中實(shí)現(xiàn)的內(nèi)容。
Thread(Runnable{
println("Hello ")
}).start()
- 另外,如果一個(gè)Java方法的參數(shù)列表不存在一個(gè)以上Java單抽象方法參數(shù)接口,我們還可以將這個(gè)接口省略。
Thread({
println("Hello ")
}).start()
- 精簡(jiǎn)還未結(jié)束!別忘了,當(dāng)Lambda表達(dá)式是方法的最后且唯一一個(gè)參數(shù)時(shí),可以將Lambda表達(dá)式移到方法括號(hào)外邊,且將方法的括號(hào)省略。那么最終結(jié)果就如下:
Thread{
println("Hello ")
}.start()
-
本小節(jié)中的Java函數(shù)式API的使用都限定于從Kotlin中調(diào)用Java方法,并且單抽象方法接口也必須是用Java語(yǔ)言定義的。在Kotlin中會(huì)經(jīng)常調(diào)用android的SDK,而因?yàn)閍ndroid中的SDK都是用Java來(lái)進(jìn)行編寫的,因此在調(diào)用這些SDK接口會(huì)經(jīng)常用到Java函數(shù)式API。舉個(gè)栗子,Android中有一個(gè)極為常用的點(diǎn)擊事件接口
OnClickListner。如果我們用Java代碼來(lái)注冊(cè)這個(gè)點(diǎn)擊事件,需要這么寫:
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
//funcbody
}
})
- 而用Kotlin代碼實(shí)現(xiàn)同樣的功能,就可以使用函數(shù)式API的思想來(lái)對(duì)上述代碼進(jìn)行簡(jiǎn)化,結(jié)果如下。是不是很簡(jiǎn)單
button.setOnClickListener{
//funcbody
}
8 空指針檢查
在剛開始的簡(jiǎn)介就說(shuō)過(guò),在Kotlin中幾乎杜絕了空指針問(wèn)題的出現(xiàn)。Kotlin利用編譯時(shí)判空檢查的機(jī)制幾乎杜絕了空指針異常,所謂的編譯時(shí)判空檢查機(jī)制就是將空指針異常的檢查從運(yùn)行時(shí)提前到了編譯期,如果程序存在空指針異常的風(fēng)險(xiǎn),那么在編譯時(shí)會(huì)自動(dòng)報(bào)錯(cuò)。
- 以之前的一個(gè)函數(shù)為例
fun doStudy(study:Study){
study.readBooks()
study.doHomework()
}
- 這段代碼看上去和Java版本的代碼并沒(méi)有什么區(qū)別,但是它是沒(méi)有空指針異常的風(fēng)險(xiǎn)的。因?yàn)镵otlin默認(rèn)所有的參數(shù)和變量都不可為空,所以這里傳入的study參數(shù)也一定不會(huì)為空。但是,這種方法雖然避免了空指針異常的出現(xiàn),又會(huì)導(dǎo)致另一個(gè)問(wèn)題,如果我們需要某個(gè)傳入的參數(shù)或變量為空的話該怎么辦呢?為了解決上面這個(gè)問(wèn)題,Kotlin提供了另外一套可為空的類型,只不過(guò)這套方法需要我們?cè)诰幾g期就將所有的潛在的空指針異常都處理掉,否則代碼將無(wú)法編譯通過(guò)。
- 可為空的類型系統(tǒng)是指:在類名后面加一個(gè)問(wèn)號(hào)。例如:
Int:表示不可為空的類型
Int?:表示可為空的類型
String:表示不可為空的類型
String?:表示可為空的類型
- 繼續(xù)拿上面的函數(shù)舉例:如果我們希望傳入的參數(shù)一可以為空,那么就應(yīng)該將參數(shù)的類型從
Study改為Study?。但是繼續(xù)以下的寫法的話又會(huì)報(bào)出錯(cuò)誤提示。
fun doStudy(study:Study?){
study.readBooks()
study.doHomework()
} - 理由很簡(jiǎn)單,由于我們將參數(shù)改為了可空的類型,那么調(diào)用參數(shù)的方法就會(huì)有可能造成空指針異常,因此Kotlin在這種情況下不允許編譯通過(guò)。處理方法為,把空指針異常都處理掉就可以了,做個(gè)判空處理。
fun doStudy(study:Study?){
if(study!=null){
study.readBooks()
study.doHomework()
}
}
8.1判空輔助工具
8.1.1 ?.操作符
這個(gè)操作符的作用非常好理解,就是當(dāng)對(duì)象不為空的時(shí)候正常調(diào)用相應(yīng)的方法,當(dāng)對(duì)象為空時(shí)則什么也不做。比如以下處理。
if(a!=null){
a.readBooks()
}
簡(jiǎn)化為
a?.readBooks()
8.1.2 ?:操作符
這個(gè)操作符的左右兩邊都接收一個(gè)表達(dá)式,如果左邊表達(dá)式不為空則返回左邊表達(dá)式的結(jié)果,否則就返回右邊表達(dá)式的結(jié)果。
val c = if(a!=null){
a
}else{
b
}
簡(jiǎn)化為
val c = a?:b
- 用一個(gè)例子函數(shù)來(lái)講上述兩個(gè)輔助工具結(jié)合一下。假如我們要獲取一個(gè)字符串的長(zhǎng)度,如果不用判空工具的話是如下這種寫法。
fun getTextLength(text:String?) : Int{
if(text!=null){
return text.length
}
return 0
}
- 如果要用操作符進(jìn)行簡(jiǎn)化的話,首先,text是可能為空的,因此我們?cè)谡{(diào)用其length字段時(shí)需要使用
?.操作符,可以簡(jiǎn)化為text?.length;其次,text?.length返回值為null,那我們就可以借用?:操作符使其返回值為0。
fun getTextLength(text:String?) = text?.length?:0
8.1.3 !!操作符
用一段代碼來(lái)解釋這個(gè)操作符的作用。我們先定義一個(gè)可為空的全局變量content,然后將其變?yōu)榇髮懩J?/p>
var content:String?="Hello"
fun main() {
if(content!=null){
printUpperCase()
}
}
fun printUpperCase(){
val upperCase = content.toUpperCase()
println(upperCase)
}
- 上述代碼看起來(lái)是沒(méi)有問(wèn)題的,但是遺憾的是這段代碼一定是無(wú)法正常運(yùn)行的。因?yàn)?code>printUpperCase()函數(shù)無(wú)法知道外部已經(jīng)對(duì)content進(jìn)行了非空檢查,所以編譯的時(shí)候會(huì)認(rèn)為存在空指針風(fēng)險(xiǎn),導(dǎo)致無(wú)法編譯通過(guò)。
- 這種情況下,就可以用到
!!操作符了,又名非空斷言工具,寫法是在對(duì)象的后面加上!!。這是一種有風(fēng)險(xiǎn)的寫法,意在告訴Kotlin,我非常確信這里的對(duì)象不會(huì)為空,所以不用幫我來(lái)做空指針檢查了。
fun printUpperCase(){
val upperCase = content!!.toUpperCase()
println(upperCase)
}
但是這并不是一種好的實(shí)現(xiàn)方法,因?yàn)槊恳淮问褂梅强諗嘌怨ぞ叩臅r(shí)候,就有可能出現(xiàn)空指針異常。所以在使用非空斷言工具的時(shí)候最好提醒一下自己,是不是有更好的實(shí)現(xiàn)方式。
8.1.4 let輔助工具
let既不是操作符,也不是什么關(guān)鍵字,而是一個(gè)函數(shù)。該函數(shù)提供了函數(shù)式API的編程接口,并將原始調(diào)用對(duì)象作為參數(shù)傳遞給Lambda表達(dá)式中。
obj.let{
obj ->
//編寫具體業(yè)務(wù)
}
- 這里調(diào)用了
obj對(duì)象的let函數(shù),然后Lambda表達(dá)式中的代碼會(huì)立即執(zhí)行,并且將這個(gè)obj對(duì)象本身還會(huì)作為參數(shù)傳遞到Lambda表達(dá)式中。
但是我們知道,這一小節(jié)是來(lái)介紹判空輔助工具的,那么
let與空指針檢查有什么關(guān)系呢?以之前的doStudy()函數(shù)舉例
fun doStudy(study:Study?){
study?.readBooks()
study?.doHomework()
}
- 細(xì)品的話,本來(lái)我們進(jìn)行一次if判斷就能隨意調(diào)用study對(duì)象的任何方法,但是受制于
?.操作符的限制,現(xiàn)在變成了每次調(diào)用study對(duì)象的方法都需要進(jìn)行一次if判斷。那么這個(gè)時(shí)候就可以結(jié)合let函數(shù)對(duì)代碼進(jìn)行優(yōu)化了。
fun doStudy(study:Study?){
study?.let{ stu ->
stu.doHomework()
stu.readBook()
}
}
0
- 在
?.操作符的作用下,對(duì)象為空時(shí)就什么都不做,不為空時(shí)則調(diào)用let函數(shù)將study對(duì)象本身作為參數(shù)傳遞到Lambda表達(dá)式中,此時(shí)study對(duì)象本身肯定不為空了。并且根據(jù)Lambda語(yǔ)法的特性,當(dāng)Lambda表達(dá)式的參數(shù)列表中只有一個(gè)參數(shù)時(shí),可以不用聲明參數(shù)名,直接使用it關(guān)鍵字來(lái)代替即可,我們可以進(jìn)一步簡(jiǎn)化代碼。
fun doStudy(study:Study?){
study?.let{
it.doHomework()
it.readBook()
}
}
9.Kotlin中的小技巧
9.1 字符串內(nèi)嵌表達(dá)式
Kotlin中添加了該功能,彌補(bǔ)上了java上相關(guān)功能的缺憾。不需要再?gòu)?fù)雜的拼接字符串了。
"hello , ${obj.name}.Nice to meet you"
- 可以看到,Kotlin中允許我們?cè)谧址星度?code>${}這種語(yǔ)法結(jié)構(gòu)的表達(dá)式,并在運(yùn)行時(shí)使用表達(dá)式的結(jié)果來(lái)替代這一部分。另外,當(dāng)表達(dá)式中僅有這一個(gè)變量的時(shí)候,還可以將兩邊大括號(hào)給省略掉。
"hello , $name.Nice to meet you"
- 加入我們要輸出一串帶變量的字符串,在原來(lái)的方法中,輸出的寫法應(yīng)該是這樣的
val name = a
val age = 10
println(“people(name = ” + name + ",age = " + age + ")")
- 使用字符串內(nèi)嵌表達(dá)式后,可以簡(jiǎn)化為
val name = a
val age = 10
println(“people(name =$name , age=$age)")
9.2 函數(shù)的默認(rèn)參數(shù)值
在前面講解次構(gòu)造函數(shù)用法的時(shí)候就提到過(guò),次構(gòu)造函數(shù)在Kotlin中很少用,因?yàn)镵otlin中提供了給函數(shù)設(shè)定默認(rèn)值的功能,在很大程度上能夠替代次構(gòu)造函數(shù)的作用。具體來(lái)講,就是在定義函數(shù)的時(shí)候給任意參數(shù)設(shè)定一個(gè)默認(rèn)值,這樣調(diào)用此函數(shù)的時(shí)候就不會(huì)強(qiáng)制要求調(diào)用方為此參數(shù)傳值,在沒(méi)有傳值的情況下會(huì)自動(dòng)使用參數(shù)的默認(rèn)值。
- 方法如下:
fun printParams(num:Int,str:String = "hello"){
println("num is $num,str is $str")
}
- 如果我們使用
printParams(10)來(lái)調(diào)用該方法,會(huì)得到如下結(jié)果。
num is 10,str is hello
- 但是上面的例子比較理想化,如果我們向要讓第一個(gè)參數(shù)設(shè)定默認(rèn)值,第二個(gè)參數(shù)使用賦值呢?
fun printParams(num:Int = 10,str:String){
println("num is $num,str is $str")
}
- 模仿剛剛的寫法肯定是不可以的,因?yàn)榫幾g器會(huì)認(rèn)為我們想把字符串賦值給第一個(gè)變量,從而報(bào)類型不匹配的錯(cuò)誤。而Kotlin提供的另一套比較神奇的機(jī)制,就是通過(guò)鍵值對(duì)的方式來(lái)傳遞參數(shù),不用像傳統(tǒng)方法那樣按照參數(shù)定義的順序來(lái)傳遞參數(shù)。我們可以寫成這樣:
printParams(str = "hello",num = 12)
- 此時(shí)哪個(gè)參數(shù)在前哪個(gè)參數(shù)在后都無(wú)所謂,Kotlin可以準(zhǔn)確的將參數(shù)匹配上。使用這種鍵值對(duì)的傳參方式之后,我們就可以省略num參數(shù)了,代碼如下:
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
那么為什么說(shuō)給函數(shù)參數(shù)設(shè)置默認(rèn)值可以很大程度上代替次構(gòu)造函數(shù)的作用的?
- 前邊學(xué)習(xí)次構(gòu)造函數(shù)的代碼
class Student(var sno:String,var grade:Int,name:String,age:Int) : Person(name,age){
//直接調(diào)用主構(gòu)造函數(shù)
constructor(name:String,age:Int):this("",0,name,age){
//TODO
}
//間接調(diào)用,調(diào)用上一個(gè)次構(gòu)造函數(shù),間接調(diào)用主構(gòu)造函數(shù)
constructor():this("Tom",2){
//TODO
}
}
- 次構(gòu)造函數(shù)在這里的作用是提供更少的參數(shù)來(lái)對(duì)Student類進(jìn)行實(shí)例化的方式。無(wú)參的次構(gòu)造函數(shù)會(huì)調(diào)用兩個(gè)參數(shù)的次構(gòu)造函數(shù),并將這兩個(gè)參數(shù)賦值成初始值。兩個(gè)參數(shù)的次構(gòu)造函數(shù)會(huì)調(diào)用4個(gè)參數(shù)的主構(gòu)造函數(shù),將缺失的參數(shù)賦值為初始值。但是學(xué)習(xí)了給參數(shù)設(shè)置默認(rèn)值,就完全用不到上述方式了。我們只需要編寫一個(gè)主構(gòu)造函數(shù),并且給參數(shù)設(shè)置默認(rèn)值的方式來(lái)實(shí)現(xiàn)。
class Student(var sno:String="",var grade:Int =0,name:String="",age:Int=0) : Person(name,age){}
9.3 Kotlin中使用findViewById()
- 加入要使用我們定義的一個(gè)button,在kotlin中的調(diào)用方法是這樣的
val button:Button = findViewById(R.id.button)
button.setOnClickListener{
//funcBody
}
findViewById()方法返回的是繼承View的泛型對(duì)象,因此Korlin無(wú)法自動(dòng)推斷出它是一個(gè)Button還是其他控件。因此我們需要將button變量顯示的聲明成Button類型。但是如果在布局文件中有十個(gè)控件,我們就需要重復(fù)調(diào)用十次findViewById()來(lái)獲取這些控件,這無(wú)疑非常麻煩。
- 但是Kotlin中不用再代碼中重復(fù)調(diào)用 findViewById()方法來(lái)獲取控件了,因?yàn)槭褂肒otlin編寫的安卓項(xiàng)目會(huì)在app.gradle文件的頭部默認(rèn)引入了一個(gè)插件,該插件會(huì)根據(jù)布局文件中定義的控件id自動(dòng)生成一個(gè)具有相同名稱的變量,我們?cè)贏ctivity中可以直接使用這個(gè)變量,而不需要再調(diào)用findViewById()方法了。
9.4 使用get和set方法的語(yǔ)法糖
- JavaBean
public class Book{
private int pages;
public int getPages(){
return pages;
}
public void setPages(int pages){
this.pages = pages;
}
}
- 在Java中調(diào)用Book類中的pages字段時(shí),需要使用getPages()和setPages(int pages)方法。而在Kotlin中調(diào)用這種語(yǔ)法結(jié)構(gòu)時(shí),可以使用一種更簡(jiǎn)單的寫法,比如用下面這種代碼來(lái)設(shè)置Book中的pages字段
val book = Book()
book.pages = 500
val bookPages = book.pages
- 這里看上去好像我們并沒(méi)有調(diào)用Book類中的setPages()與getPages()方法,而是直接對(duì)pages字段進(jìn)行了賦值和讀取。其實(shí)這就是Kotlin中的語(yǔ)法糖,他會(huì)在背后自動(dòng)將上述代碼轉(zhuǎn)換成調(diào)用setPages()方法和getPages()方法
10. 標(biāo)準(zhǔn)函數(shù)與靜態(tài)函數(shù)
10.1 標(biāo)準(zhǔn)函數(shù)
我們之前所學(xué)的let函數(shù),其實(shí)就是標(biāo)準(zhǔn)函數(shù),其主要作用就是配合
?.操作符進(jìn)行判空輔助處理。而標(biāo)準(zhǔn)函數(shù)其實(shí)是指在Standard.kt中定義的函數(shù),任何Kotlin代碼都可以自由的調(diào)用所有的標(biāo)準(zhǔn)函數(shù)。我們先主要掌握幾個(gè)常用的標(biāo)準(zhǔn)函數(shù)with,apply,run標(biāo)準(zhǔn)函數(shù)。
(1)with函數(shù)
- 參數(shù):第一個(gè)參數(shù)可以為任何對(duì)象,第二個(gè)參數(shù)是一個(gè)Lambda表達(dá)式。
- 原理:with函數(shù)會(huì)在Lambda表達(dá)式中提供第一個(gè)參數(shù)對(duì)象的上下文,并使用最后一行代碼作為返回值返回。以下面的代碼為例
val result = with(obj){
//這里是obj的上下文
value//with函數(shù)的返回值
}
- 作用:我們可以在連續(xù)調(diào)用同一個(gè)對(duì)象的多個(gè)方法時(shí)讓代碼變得更加精簡(jiǎn)。
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函數(shù)
該函數(shù)的用法和使用場(chǎng)景其實(shí)和with函數(shù)時(shí)非常類似的,只是在參數(shù)與調(diào)用方法上有所區(qū)別。
- 首先,run函數(shù)是不能直接調(diào)用的,而是一定要被某個(gè)函數(shù)所調(diào)用才行。
- 其次,run函數(shù)只接受一個(gè)Lambda參數(shù),并且會(huì)在Lambda表達(dá)式中提供調(diào)用對(duì)象的上下文。
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函數(shù)
apply函數(shù)與run函數(shù)也及其類似,需要被某個(gè)對(duì)象調(diào)用,但無(wú)法指定返回值,只能返回調(diào)用對(duì)象本身。
val list = listOf("one","two","three")
val result = StringBuilder().apply{
append("start \n")
for(fruit in list){
append(fruit + "\n")
}
}
println(result.toString()) //此時(shí)result為StringBuilder()對(duì)象
(3)repeat函數(shù)
可以直接被調(diào)用,不需要通過(guò)對(duì)象,接受兩個(gè)參數(shù),第一個(gè)參數(shù)為數(shù)值n,第二個(gè)參數(shù)為L(zhǎng)ambda表達(dá)式,然后會(huì)將Lambda表達(dá)式中的內(nèi)容執(zhí)行n次
10.2 靜態(tài)函數(shù)
靜態(tài)方法又叫做類方法,指的是那種不需要?jiǎng)?chuàng)建實(shí)例就能調(diào)用的方法。Java中定義靜態(tài)方法只需要在函數(shù)聲明時(shí)加上static關(guān)鍵字即可,但是Kotlin卻極度弱化了靜態(tài)方法這個(gè)概念。之所以要這樣設(shè)計(jì),是因?yàn)镵otlin提供了更好的語(yǔ)法特性,那就是單例類。
10.2.1 單例類
object util{
fun doAction(){
}
}
- 注意,doAction()方法實(shí)際上并不是靜態(tài)方法,只是可以使用類似于調(diào)用靜態(tài)方法的方式來(lái)調(diào)用,Util.doAction()。單例類的寫法會(huì)將整個(gè)類中的所有方法全都變成類似于靜態(tài)方法的調(diào)用方式,但是如果我們只想讓一個(gè)或幾個(gè)方法變成靜態(tài)方法的調(diào)用方式話,這樣就很不方便。這時(shí)就需要伴生關(guān)鍵字
companion object
10.2.2 伴生關(guān)鍵字companion object
class Util{
fun doAction1(){}
companion object{
fun doAction2(){}
}
}
- 以上兩個(gè)函數(shù)的調(diào)用方法分別為:
Util().doAction1(),Util.doAction2() - 但是doAction2()方法其實(shí)也并不是靜態(tài)方法,
companion object改關(guān)鍵字實(shí)際上會(huì)在Util類內(nèi)部創(chuàng)建一個(gè)伴生類,doAction2()方法就是定義在這個(gè)伴生類中的實(shí)例方法。只是Kotlin會(huì)保證Util類始終只會(huì)存在一個(gè)伴生類對(duì)象 - 由此可以看出,Kotlin中確實(shí)沒(méi)有可以直接定義靜態(tài)方法的關(guān)鍵字,但是提供了一些語(yǔ)法特性來(lái)支持類似于靜態(tài)方法調(diào)用的寫法。
10.2.3 注解@JvmStatic聲明靜態(tài)方法
之前也說(shuō)過(guò),單例類與
companion object都不是真正的單例類,而如果我們?cè)谶@些方法上加上@JvmStatic注解,kotlin會(huì)將這些方法編譯成真正的靜態(tài)方法
class Util{
fun doAction1(){
}
companion object{
// 經(jīng)過(guò)該關(guān)鍵字修飾后,doAction2()方法為真正的靜態(tài)方法
@JvmStatic
fun doAction2(){
}
}
}
@JvmStatic關(guān)鍵字只能加在單例類或companion object中的方法上,如果加在普通方法上會(huì)提示語(yǔ)法錯(cuò)誤
10.2.4 頂層方法
頂層方法指的是沒(méi)有定義在任何類中的方法,Kotlin會(huì)將所有的頂層方法全部編譯成靜態(tài)方法
//新建一個(gè)Kotlin文件,在該文件中直接定義方法
fun doAction1(){
}
- 如果是在Kotlin中調(diào)用的話,所有的頂層方法都可以在任何位置上被直接調(diào)用,不用管包名,實(shí)例,直接輸入doAction1()即可
- 如果是java中,是無(wú)法直接調(diào)用的,會(huì)找不到這個(gè)方法,因?yàn)镴ava中沒(méi)有頂層方法這個(gè)概念,kotlin會(huì)將頂層方法所在的文件創(chuàng)建一個(gè)類,比如該文件名為Help.kt,那么就會(huì)新建一個(gè)HelpKt.class的Java類,doAction1()就是以靜態(tài)方法寫在該類中的
11. 小總結(jié)
- 類型強(qiáng)制轉(zhuǎn)換符 為
as
12. 延遲初始化和密封類
12.1 對(duì)變量的延遲初始化
在開發(fā)中,如果類中存在很多全局變量實(shí)例,為了保證他們能滿足Kotlin中的空指針檢查語(yǔ)法標(biāo)準(zhǔn),我們不得不在代碼中坐很多非空判斷保護(hù)才行,比如
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 設(shè)置為全局變量,但是它的初始化工作是在 onCreate() 函數(shù)中進(jìn)行的,因此不得不先將 mTestClass 賦值為 null,同時(shí)把它的類型聲明成 TestClass?。
雖然在 onCreate() 函數(shù)中對(duì) mTestClass 進(jìn)行初始化,同時(shí)能確保 onClick() 函數(shù)必然在 onCreate() 函數(shù)之后才會(huì)調(diào)用,,但是在 onClick() 函數(shù)中調(diào)用 mTestClass 的任何函數(shù)時(shí)仍然要進(jìn)行判空處理才行,否則編譯肯定無(wú)法通過(guò)。
當(dāng)代碼中有了越來(lái)越多的全局變量實(shí)例時(shí),這個(gè)問(wèn)題就會(huì)變得越來(lái)越明顯,到時(shí)候可能必須編寫大量額外的判空處理代碼,只是為了滿足 Kotlin 編譯器的要求。解決辦法就是使用全局變量進(jìn)行延遲初始化。
- 延遲初始化的關(guān)鍵字為
lateinit,該關(guān)鍵字可以告訴編譯器,我會(huì)在晚些時(shí)候?qū)@個(gè)變量進(jìn)行初始化,那么這樣就不用一開始聲明的時(shí)候就將該變量賦值為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關(guān)鍵字不是沒(méi)有風(fēng)險(xiǎn)的,當(dāng)對(duì)一個(gè)全局變量使用lateinit關(guān)鍵字時(shí),一定要保證在任何情況,任何位置調(diào)用的時(shí)候,該關(guān)鍵字已經(jīng)被賦值了,否則程序一定會(huì)崩潰
另外,我們還可以通過(guò)代碼
isInitialized來(lái)判斷一個(gè)全局變量是否已經(jīng)被初始化了,這樣在某些時(shí)候能有效的避免對(duì)某一個(gè)變量重復(fù)的進(jìn)行初始化工作,比如
class TestActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var mTestClass: TestClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//判斷mTestClass是否已經(jīng)被初始化
//::mTestClass.isInitialized的寫法看上去有點(diǎn)奇怪,但這是固定的寫法
if (!::mTestClass.isInitialized) {
mTestClass = TestClass()
}
}
override fun onClick(v: View?) {
mTestClass.doXxx()
}
}
12.2 密封類
網(wǎng)上一篇對(duì)密封類介紹的比較詳細(xì)的文章
13. 擴(kuò)展函數(shù)與運(yùn)算符重載
13.1 擴(kuò)展函數(shù)
定義:表示即使在不修改某個(gè)類的源碼的情況下,仍然可以打開這個(gè)類,向該類添加新的函數(shù)
- 以一個(gè)功能為例,如果我們想統(tǒng)計(jì)字符串中字母的數(shù)量,那么我們?cè)撊绾螌?shí)現(xiàn)這個(gè)功能呢?
object StringUtil{
fun count(str:String?) : Int{
var count = 0
str?.let{
for(char in it){
if(char.isLetter()){
count++
}
}
return count
}
return 0
}
}
- 上面這種寫法沒(méi)有問(wèn)題,可以正常使用。但是有了擴(kuò)展函數(shù)后,我們就可以用擴(kuò)展函數(shù)將count()方法添加到String類中。
定義擴(kuò)展函數(shù)的語(yǔ)法結(jié)構(gòu),相比于定義一個(gè)普通的函數(shù),定義擴(kuò)展函數(shù)只需要在函數(shù)名的前面加上
類名.的語(yǔ)法結(jié)構(gòu),就表示將該函數(shù)添加到指定類中了
fun className.merthodName(param1:Int,param2:Int):Int{
return 0
}
- 使用擴(kuò)展函數(shù)將count()函數(shù)添加進(jìn)String類中的方法:向哪個(gè)類添加擴(kuò)展函數(shù)就定義一個(gè)同名的Kotlin文件,由于我們希望向String類中添加擴(kuò)展函數(shù),那么我們就需要先創(chuàng)建對(duì)應(yī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類的擴(kuò)展函數(shù),那么該函數(shù)就自動(dòng)擁有了String實(shí)例的上下文,因此該函數(shù)就不用接收一個(gè)字符串參數(shù)了,而是直接遍歷this即可,因?yàn)閠his就代表著字符串本身。而我們之后就可以使用該擴(kuò)展函數(shù)了
val count = "12s2w3e".count()
13.2 運(yùn)算符重載
- PS:本人覺(jué)得沒(méi)太大用,雖然簡(jiǎn)化了代碼量,但是增加了代碼閱讀的困難度,不適用于公共開發(fā)。
14. 高階函數(shù)
14.1 定義高階函數(shù)
高階函數(shù)與Lambda的關(guān)系是密不可分的。像map,rum,apply這種接收Lambda參數(shù)的函數(shù)可以被稱為具有函數(shù)式編程風(fēng)格的api,如果我們想自己定義函數(shù)式api,就得借助高階函數(shù)來(lái)實(shí)現(xiàn)。
什么是高階函數(shù)?
所謂高階函數(shù),就是一個(gè)函數(shù)接收另一個(gè)函數(shù)作為參數(shù),或者返回值的類型是另一個(gè)函數(shù),那么該函數(shù)就可以被稱為高階函數(shù)
- 高階函數(shù)需要以另一個(gè)參數(shù)作為參數(shù)或者是返回值,那么怎么以一個(gè)函數(shù)作為參數(shù)呢?這就涉及到Kotlin新增的另一個(gè)概念:函數(shù)類型。這個(gè)函數(shù)類型類似于整形,浮點(diǎn)型,布爾型,是Kotlin中新增的。定義一個(gè)函數(shù)類型的方式如下:
(String, Int) -> Unit
- 定義一個(gè)函數(shù)類型,最關(guān)鍵的是要聲明該函數(shù)接收什么參數(shù),與它的返回值類型是什么。因此,
->左邊表示聲明該函數(shù)需要傳入的參數(shù),多個(gè)參數(shù)之間使用逗號(hào)隔開,如果沒(méi)有參數(shù),使用一對(duì)空括號(hào)就可以了,->右邊表示聲明該函數(shù)的返回值是什么類型,如果沒(méi)有返回值就使用Unit,它大致相當(dāng)于Java中的void,以下邊函數(shù)為例
fun example(func:(String,Int)->Unit){
// 函數(shù)名 參 數(shù) 返回值
func("hello",123)
}
- 可以看到example()函數(shù)接收了一個(gè)函數(shù)類型的參數(shù),因此example()函數(shù)就是一個(gè)高階函數(shù)。調(diào)用一個(gè)函數(shù)類型的參數(shù),它的語(yǔ)法類似于調(diào)用一個(gè)普通的函數(shù)。但是上面這個(gè)例子沒(méi)有辦法直觀的體現(xiàn)出高階函數(shù)的作用,那么這種函數(shù)具體有什么用途呢?
- 簡(jiǎn)單概括一下高階函數(shù)的用途:高階函數(shù)允許讓函數(shù)類型的參數(shù)來(lái)決定函數(shù)的執(zhí)行邏輯,即使是在同一個(gè)高階函數(shù),只要傳入不同的函數(shù)類型參數(shù),那么它的執(zhí)行邏輯與最終的返回結(jié)果就可能是完全不同的。舉個(gè)下面這個(gè)栗子說(shuō)明。
定義一個(gè)num1Andnum2()的函數(shù),參數(shù)類型為兩個(gè)整形與一個(gè)函數(shù)類型。再定義兩個(gè)與其函數(shù)類型匹配的函數(shù)。
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函數(shù)中編寫如下代碼
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)
}
- 注意這里調(diào)用num1Andnum2()函數(shù)的方式,第三個(gè)參數(shù)使用了
::plus與::minus這種寫法,表示將這兩個(gè)函數(shù)作為參數(shù)傳遞給num1Andnum2()函數(shù),然后num1Andnum2()函數(shù)根據(jù)傳入的函數(shù)類型參數(shù)決定具體的計(jì)算邏輯。
這種寫法雖然可以正常工作,但是寫法是不是太復(fù)雜了,每次調(diào)用任何高階函數(shù)的時(shí)候都還需要定義一個(gè)與其函數(shù)類型相匹配到的函數(shù),沒(méi)有起到簡(jiǎn)化代碼的作用。因此,kotlin支持更多的方式來(lái)調(diào)用高階函數(shù),比如Lambda表達(dá)式,匿名函數(shù),成員引用等。其中Lambda表達(dá)式是最常見(jiàn)最普遍的高階函數(shù)調(diào)用方式
- 將上述方法用Lambda表達(dá)式來(lái)實(shí)現(xiàn),可以寫為:
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表達(dá)式同樣可以完整地表達(dá)一個(gè)函數(shù)的參數(shù)聲明和返回值聲明,但是寫法會(huì)更加簡(jiǎn)單
繼續(xù)探究高階函數(shù)的使用?;仡欀爸皩W(xué)習(xí)的apply函數(shù),該函數(shù)可以給Lambda表達(dá)式提供指定的上下文,當(dāng)需要連續(xù)調(diào)用同一個(gè)對(duì)象的多個(gè)方法時(shí),這個(gè)函數(shù)可以讓代碼變得更加精簡(jiǎn)。比如StringBuilder,學(xué)習(xí)了高階函數(shù)后我們就可以用高階函數(shù)來(lái)模仿一個(gè)類似的功能。
fun StringBuilder.build(block : StringBuilder.() -> Unit):StringBuilder{
block()
}
- 這里給StringBuilder定義了一個(gè)擴(kuò)展函數(shù),該擴(kuò)展函數(shù)接收一個(gè)函數(shù)類型參數(shù),返回值為StringBuilder類型。
- 注意,聲明該高階函數(shù)與之前的例子又有些不同:他在函數(shù)類型參數(shù)前面加了個(gè)
StringBuilder.,這是什么意思呢?在函數(shù)類型參數(shù)的前面加上ClassName,就表示這個(gè)函數(shù)類型參數(shù)定義在對(duì)應(yīng)的類中,這里就是講函數(shù)類型參數(shù)定義在StringBuilder中。 - 但是這樣子有什么好處呢?好處就是,當(dāng)我們調(diào)用build函數(shù)時(shí)傳入的Lambda表達(dá)式會(huì)自動(dòng)擁有StringBuilder的上下文,與apply函數(shù)非常相似。
14.2 內(nèi)聯(lián)函數(shù)
14.2.1 高階函數(shù)的實(shí)現(xiàn)原理
- 仍然用剛剛的 num1Andnum2函數(shù)舉例,代碼如下
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
}
- 上述代碼調(diào)用了 num1Andnum2函數(shù),并通過(guò)Lambda表達(dá)式指定對(duì)傳入的兩個(gè)整形參數(shù)進(jìn)行求和。但是上述調(diào)用方法再Kotlin中比較好理解,比較基礎(chǔ)的高階函數(shù)用法??墒荎otlin最終還是要編譯成Java字節(jié)碼的,但是Java中并沒(méi)有高階函數(shù)的概念,那么Kotlin是如何讓Java來(lái)支持這種高階函數(shù)的用法的?Kotlin強(qiáng)大的編譯器會(huì)將這些高階函數(shù)的語(yǔ)法轉(zhuǎn)換成Java支持的語(yǔ)法結(jié)構(gòu),上述的Kotlin代碼大致會(huì)被轉(zhuǎn)換成如下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表達(dá)式在底層被轉(zhuǎn)換為了匿名類的實(shí)現(xiàn)方式,每當(dāng)調(diào)用一次Lambda表達(dá)式,都會(huì)創(chuàng)建一個(gè)新的匿名對(duì)象,造成額外的性能開銷。為了解決這個(gè)問(wèn)題,Kotlin提供了內(nèi)聯(lián)函數(shù)的功能,可以將Lambda表達(dá)式帶來(lái)的運(yùn)行時(shí)開銷完全消除。
定義內(nèi)聯(lián)函數(shù)的方式很簡(jiǎn)單,定義高階函數(shù)時(shí)加上inline關(guān)鍵字的聲明即可。內(nèi)聯(lián)函數(shù)的工作原理就是,Kotlin編譯器會(huì)將內(nèi)聯(lián)函數(shù)中的代碼在編譯時(shí)自動(dòng)替換到調(diào)用它的地方
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
之前討論的情況是,一個(gè)高階函數(shù)只接受了一個(gè)函數(shù)類型參數(shù),如果一個(gè)高階函數(shù)中接受了兩個(gè)或更多的函數(shù)類型參數(shù),這是我們加上inline關(guān)鍵字,Kotlin編譯器會(huì)將所有引用的Lambda表達(dá)式全部替換,但是我們只想內(nèi)聯(lián)其中一個(gè)的話該怎么辦呢?這時(shí),我們可以使用
noinline關(guān)鍵字
inline fun inlineTest(block1 : () -> Unit , noline block2 : () -> Unit){}
- 原本block1與block2所引用函數(shù)類型都會(huì)被內(nèi)聯(lián),但我們?cè)赽lock2參數(shù)的前面加上noinline關(guān)鍵字,那么就只會(huì)對(duì)block1參數(shù)所引用的Lambda表達(dá)式進(jìn)行內(nèi)聯(lián)了
- 但是我們?cè)谏弦恍」?jié)已經(jīng)說(shuō)完內(nèi)聯(lián)的好處了,它可以減少系統(tǒng)的開銷,那么為什么我還用
noinline關(guān)鍵字取消內(nèi)聯(lián)呢?因?yàn)閮?nèi)聯(lián)的函數(shù)類型參數(shù)在編譯的時(shí)候會(huì)進(jìn)行代碼替換,因此它沒(méi)有真正的參數(shù)屬性,而非內(nèi)聯(lián)的函數(shù)類型參數(shù)可以自由地傳遞給其他函數(shù),因?yàn)樗褪且粋€(gè)真是的參數(shù),而內(nèi)聯(lián)的函數(shù)類型參數(shù)只允許傳遞給另外一個(gè)內(nèi)聯(lián)函數(shù),這也是它最大的局限性,因此可以這么理解內(nèi)聯(lián)函數(shù)=一段代碼,非內(nèi)聯(lián)函數(shù)=一個(gè)真正的函數(shù)參數(shù)。 - 另外,內(nèi)聯(lián)函數(shù)與非內(nèi)聯(lián)函數(shù)還有一個(gè)重要的區(qū)別,那就是內(nèi)聯(lián)函數(shù)所引用的Lambda表達(dá)式中可以使用
return關(guān)鍵字返回,非內(nèi)聯(lián)函數(shù)只能使用局部返回
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")
}
- 以上函數(shù)的結(jié)果為除了“l(fā)ambda end”這句沒(méi)有輸出,其余皆輸出。注意的是,Lambda表達(dá)式中是不允許直接使用return關(guān)鍵字的,代碼中的
return@printString代表局部返回 - 但是如果我們將printString()函數(shù)聲明成一個(gè)內(nèi)聯(lián)函數(shù),那么情況就不一樣了
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")
}
- 輸出的結(jié)果為除"lambda end"與"main end"外全打印。因?yàn)閮?nèi)聯(lián)函數(shù)本質(zhì)上是代碼替換,因此可以將上述函數(shù)寫為
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
- 將高階函數(shù)聲明為內(nèi)聯(lián)函數(shù)是一種良好的編程習(xí)慣,大多數(shù)高階函數(shù)都可以直接聲明為內(nèi)聯(lián)函數(shù)的,但是也有少數(shù)情況,如下。
inline fun runRunnable(block()->Unit){
val runnable = Runnable{//本質(zhì)上為匿名對(duì)象,只能使用局部返回
block()//內(nèi)聯(lián)函數(shù),可以使用return
}
runnable.run()
}
- 這個(gè)出現(xiàn)錯(cuò)誤的原因比較復(fù)雜。首先,在runRunnable()函數(shù)中,我們創(chuàng)建了一個(gè)Runnable對(duì)象,并在Runna ble的Lambda表達(dá)式中調(diào)用了傳入的函數(shù)類型參數(shù),之前也講過(guò),編譯器實(shí)際上會(huì)將Lambda表達(dá)式編譯為匿名內(nèi)部類的方式,也就是說(shuō)上述方法是在匿名對(duì)象中調(diào)用了傳入的函數(shù)類型參數(shù)。
- 內(nèi)聯(lián)函數(shù)所引用的Lambda表達(dá)式允許使用
return關(guān)鍵字進(jìn)行函數(shù)返回,但是由于我們是在匿名內(nèi)部類中調(diào)用的函數(shù)類型參數(shù),最多只能對(duì)匿名類中的函數(shù)調(diào)用進(jìn)行返回。因此如果我們?cè)诟唠A函數(shù)中創(chuàng)建了另外的Lambda表達(dá)式或者匿名類的實(shí)現(xiàn),并且在這些實(shí)現(xiàn)中調(diào)用函數(shù)類型參數(shù),此時(shí)再將高階函數(shù)聲明為內(nèi)聯(lián)函數(shù)就一定會(huì)出錯(cuò)。這時(shí)就需要使用crossinLine關(guān)鍵字。
inline fun runRunnable(crossinLine block()->Unit){
val runnable = Runnable{//本質(zhì)上為匿名對(duì)象,只能使用局部返回
block()//內(nèi)聯(lián)函數(shù),可以使用return
}
runnable.run()
}
- crossinLine關(guān)鍵字就像一個(gè)契約,保證在內(nèi)聯(lián)函數(shù)的Lambda表達(dá)式中一定不會(huì)使用return關(guān)鍵字,這樣就不會(huì)有矛盾了。