kotlin相關(guān)

一、lateinit

變量的關(guān)鍵字,可以不用在定義變量的時候就設(shè)置初始值

二、原有項目一些涉及到apt的第三方庫,改為kotlin后,報錯,resource中沒有相關(guān)類

使用到apt相關(guān)的第三方,比如arouter,要使用kapt,但是如果你的項目用到了很多第三方,并且有些第三方不支持kapt的話就不行,比如lombok。有一種很土的辦法就是把kapt和java annotation配置分成兩個目錄,我沒試過感覺有點惡心。
但是如果支持kapt可以這改造一樣就能用:

1、apply plugin: 'kotlin-kapt'

2、有kotlin的代碼,javaCompileOptions改為kapt的

defaultConfig{
          ...
//        javaCompileOptions {
//            annotationProcessorOptions {
//                arguments = [ moduleName : project.getName() ]
//            }
//        }
        kapt {
            arguments {
                arg("moduleName", project.getName())
            }
        }
}

3、有kotlin的代碼,需要依賴配置修改annotationProcessor改為kapt

compile 'com.alibaba:arouter-api:1.3.1'
//    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
    kapt 'com.alibaba:arouter-compiler:1.1.4'

三、let、with、run和apply

對象?.let{} 方便不為空的時候用來使用這個對象,等同于省去if(null != 對象){}的判斷;
with(對象){} 方便一些配置信息,比如變量賦值,設(shè)置是否可以顯示,設(shè)置點擊事件等等,用來代替builder的鏈式調(diào)用,這對安卓開發(fā)中操作控件極其好用,因為控件沒有builder來讓你鏈式;
或者對一個對象連續(xù)多次操作后返回任意東西(lambda最后一行代碼返回值就是整個with返回值)都可以用with來簡化代碼。

with(bt) {
            this.visibility = View.VISIBLE
            this.text = "填充按鈕文字"
            this.onClick { bt.handleKeyboard() }
        }

對象.run 和with用法一樣,只不過with是傳對象進去,run是由對象.調(diào)用,返回值也是lambda最后一行代碼。
對象.apply 和run用法一樣,不過返回值不再是最后一行代碼了,而是返回調(diào)用對象本身。
with、run、apply都非常相似,僅有一點小區(qū)別,使用上靈活選擇即可。

四、標簽 @

@標簽 可以理解成標記一下來源
對于嵌套for循環(huán)來說,可以指定跳出哪一層循環(huán),比java好用,比如:

//用標簽來指定需要跳出哪個循環(huán),比java中好用
//    firstLoop@ for (i in 1..10) {
//        println("第一層循環(huán)i=${i}")
//        secondLoop@ for (j in 1..10) {
//            println("第二層循環(huán)j = ${j}")
//            if (j > 6) break@firstLoop
//            for (x in 1..6) {
//                if (x < 2) break@secondLoop
//            }
//        }
//    }

但是對于嵌套的lamda表達式foreach來說,用return+標簽并不是跳出標簽的foreach循環(huán),debug了一下,發(fā)現(xiàn)是continue,例子:

fun foo() {
    ints.forEach {
        if (it == 2) {
            println("滿足條件,直接下一次循環(huán)")
            return@forEach
        }
        println(it)
    }
    println("------foo")
}

打印結(jié)果是:
1
滿足條件,直接下一次循環(huán)
3
------foo

如果return不帶標簽,則是直接結(jié)束方法,例子:

fun foo() {
    ints.forEach {
        if (it == 2) {
            println("滿足條件,直接下一次循環(huán)")
            return
        }
        println(it)
    }
    println("------foo")
}

打印結(jié)果是:
1
滿足條件,直接下一次循環(huán)

五、有時候用print打印輸出的時候,控制臺會打印出一串“kotlin.Unit”

研究了一下和print中打印的內(nèi)容有關(guān),如果打印的是一個有返回值的方法,則輸出返回值,如果打印的是一個沒有返回值的方法,就會打印出一串“kotlin.Unit”,而不是什么都不打印,為什么呢?因為print調(diào)用Unit的toString方法, Unit的toString方法內(nèi)容:

public object Unit {
    override fun toString() = "kotlin.Unit"
}

六、final和open

類默認是final,如果需要被繼承,需要加open關(guān)鍵字
fun聲明的函數(shù)默認是final,如果需要被重寫,需要加open,子類重寫是用override關(guān)鍵字
為什么默認是final?因為kotlin這么設(shè)計就是為了不重蹈java覆轍。java中對final是不強制的,這其實是非常不安全的。java不強制,開發(fā)者就基本不會主動加final關(guān)鍵字,即使這個類一個子類都沒,在項目越來越大之后,這種不規(guī)范的寫法就變得很危險,你無法知道別人會不會去繼承這個類從而導致一些不可控的錯誤。

七、關(guān)于kotlin中方法和變量的override

方法:
Kotlin的繼承和實現(xiàn)中如果父類和接口有重復方法,使用super范型去選擇性地調(diào)用父類的實現(xiàn):
class C() : A() , B{
override fun f() {
super<A>.f()//調(diào)用 A.f()
super<B>.f()//調(diào)用 B.f()
}
}
和java區(qū)別比較大,java如果繼承的類和實現(xiàn)的接口中有相同方法,接口需要實現(xiàn)的方法默認會被父類實現(xiàn),子類可以繼續(xù)重寫父類這個方法;而kotlin一定需要子類去實現(xiàn)接口的方法。


image.png

變量:
因為kotlin的繼承不允許子類有和父類一樣的變量名。。。除非父類里面變量是private或者子類override這個變量。。。


image.png

image.png

image.png

屬性的繼承這里有一個特別要注意的點,否則一不小心就空指針:



IDE報的是:Accessing non-final property name in constructor
不繼承就沒事


八、kotlin中的接口與java中的接口

Kotlin 接口與 Java 8 類似,使用 interface 關(guān)鍵字定義接口,允許方法有默認實現(xiàn)接口中的屬性只能是抽象的,不允許初始化值,接口不會保存屬性值,實現(xiàn)接口時,必須重寫屬性,,這和java中不同,java中接口中定義的屬性都是常量

九、kotlin中的擴展

Kotlin中可以很方便的對一個類的屬性或方法進行擴展,不用像java一樣使用繼承或者裝飾模式Decorator去實現(xiàn)。擴展不會對原有類進行修改,注意這不是修改,只是一種靜態(tài)的行為。
在調(diào)用擴展函數(shù)時,具體被調(diào)用的的是哪一個函數(shù),由調(diào)用函數(shù)的的對象表達式來決定的,而不是動態(tài)的類型決定的,這和java中方法的靜態(tài)分配是一樣的。
先舉一個kotlin例子:

open class C
class D : C()

//擴展C
fun C.foo() = "c"

//擴展D
fun D.foo() = "d"

//方法入?yún)
fun printFoo(c: C) {
    println(c.foo())
}

fun main() {
    //實際傳入D實例
    printFoo(D())
}

打印結(jié)果:c

再來一個java的例子對比一下:

public class MyTest5 {

    //方法的入?yún)㈩愋途褪庆o態(tài)類型,編譯期就可以完全確定
    public void test(Grandpa grandpa) {
        System.out.println("grandpa");
    }

    public void test(Father father) {
        System.out.println("father");
    }

    public void test(Son son) {
        System.out.println("son");
    }

    public static void main(String[] args) {
        Grandpa g1 = new Father();
        Grandpa g2 = new Son();

        MyTest5 myTest5 = new MyTest5();
        myTest5.test(g1);
        myTest5.test(g2);
    }
}
class Grandpa {
}
class Father extends Grandpa {
}
class Son extends Father {
}

打印結(jié)果:
//grandpa
//grandpa

我們從字節(jié)碼上分析一下:
main方法的Code屬性字節(jié)碼為:

0 new #7 <com/xuchun/bytecode/Father>
 3 dup
 4 invokespecial #8 <com/xuchun/bytecode/Father.<init>>
 7 astore_1
 8 new #9 <com/xuchun/bytecode/Son>
11 dup
12 invokespecial #10 <com/xuchun/bytecode/Son.<init>>
15 astore_2
16 new #11 <com/xuchun/bytecode/MyTest5>
19 dup
20 invokespecial #12 <com/xuchun/bytecode/MyTest5.<init>>
23 astore_3
24 aload_3
25 aload_1
26 invokevirtual #13 <com/xuchun/bytecode/MyTest5.test>
29 aload_3
30 aload_2
31 invokevirtual #13 <com/xuchun/bytecode/MyTest5.test>
34 return

看26、31行,invokevirtual 指令的意思是調(diào)用虛方法(存在運行期動態(tài)查找的過程),調(diào)用誰的方法呢,是com/xuchun/bytecode/MyTest5.test方法,MyTest5里有三個test方法,是哪個呢,再看#13對應的常量池里的常量信息:


image.png

可以看到方法的Name是test,參數(shù)類型是Lcom/xuchun/bytecode/Grandpa;方法返回值是void,同樣是方法的靜態(tài)分配。
靜態(tài)類型是不會變化,但是實際類型是可以再運行期間變化的,這也是多態(tài)的體現(xiàn)。

在舉個例子加深記憶:

//擴展函數(shù)可以被申明為open,可以被其子類覆寫,擴展對于被擴展函數(shù)的類是靜態(tài)的,但是對于擴展方是虛擬的。

open class D
class D1 : D()

open class C {
    open fun D.foo() {
        println("D.foo in C")
    }

    open fun D1.foo() {
        println("D1.foo in C")
    }

    fun caller(d: D) {
        d.foo()//調(diào)用擴展函數(shù)
    }
}

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    }

    override fun D1.foo() {
        println("D1.foo in D1")
    }
}

fun main() {
    C().caller(D())//D.foo in C
    C1().caller(D())//D.foo in C1
    C().caller(D1())//D.foo in C
    C1().caller(D1())//D.foo in C1
}

擴展方法中的this:
擴展方法中的this就是被擴展的對象實例:

fun User.printName() {
    println(name)
}

fun User.cName(n: String) :User{
    name = n
    return this
}

fun main() {
    User("測試擴展函數(shù)").cName("用擴展方法重新給變量賦值").printName()
}

輸出:用擴展方法重新給變量賦值

十、用kotlin創(chuàng)建的類或者接口,java調(diào)用時報找不到

檢查你的kotlin類或者接口中的第一行有沒有特別的符號,比如:`
原因是可能你用了關(guān)鍵字作為文件夾名稱,這個文件夾中的類或者接口不會報錯,kotlin會自動把第一行的package翻譯成kotlin不報錯的形式,比如你用interface做了文件夾的名稱,下面的接口第一行:package com.xuchun.floatingview.`interface`,這樣的話kotlin之間互相可以使用沒問題,java來使用就不行了。
解決方法:不用關(guān)鍵字做文件夾名稱。

十一、kotlin中單例怎么寫:

class Floater private constructor() : IFloater {
    companion object {
        val instance: Floater by lazy {
            Single.instance
        }
    }

    private object Single {
        val instance = Floater()
    }
}

kotlin調(diào)用:Floater.instance
java調(diào)用:Floater.Companion.getInstance()

十二、kotlin中的泛型

1、泛型約束:
對泛型的上界進行約束可以讓你可以把泛型當做它的上界類型,從而直接調(diào)用上界類型的方法,很快樂

fun <T :Number> oneHalf(value:T):Double{
    return value.toDouble()//直接就可以用Number的方法
}

所以當你如果定義多個約束,你就可以獲得多倍快樂:

fun <T> ensureTrailingPeriod(seq:T) where T:CharSequence,T:Appendable{
    //CharSequence和Appendable的方法你都可以直接用
}

快樂的代價就是要守規(guī)矩:這里表示你的seq實際傳入的類型必須要同時實現(xiàn)T:CharSequence和T:Appendable。
需要注意的是:kotlin中沒有指定上界的泛型會有一個默認上界:Any? ,此時你的泛型參數(shù)是可空的,即使并沒有在T后面寫問號標記,如果此時想要設(shè)為不為空,就顯示的設(shè)定上界為Any替換掉默認的Any?即可。

2、泛型型變:
先看一下java中泛型的型變:
型變簡單理解就是類型的變化。一個類型可能有子類型,可能有父類型,在不同情況下,類型的變化是有一定規(guī)則的,不是隨心所欲的。
那么逆變與協(xié)變是什么呢?是用來描述類型變換后繼承關(guān)系,并且有一個公式可以套用:
如果??、??表示類型,??(?)表示類型轉(zhuǎn)換,≤表示繼承關(guān)系(比如,??≤??表示??是??的子類):
??(?)是逆變(contravariant)的,當??≤??時有??(??)≤??(??)成立;
??(?)是協(xié)變(covariant)的,當??≤??時有??(??)≤??(??)成立;
??(?)是不變(invariant)的,當??≤??時上述兩個式子均不成立,即??(??)與??(??)相互之間沒有繼承關(guān)系。
換句話說,你如果想讓你的泛型是可以變化的,那就必須要用逆變或者協(xié)變。老師敲黑板:注意,我要變型了!
上面公式看不懂沒關(guān)系,直接看例子:
舉一個不規(guī)范但就是直觀的簡單例子:

public static class 爺爺 {
}
public static class 父親 extends 爺爺 {
}
public static class 兒子 extends 父親 {
}
public static class 孫子 extends 兒子 {
}

然后定一個List變量,聲明列表容器接收兒子類型

List<兒子> list = new ArrayList<兒子>();

這樣定義,編譯和運行都不會報錯,IDE甚至還好心提示你:Explicit type argument 兒子 can ben replaced with<>,什么意思呢,就是對你說,她很聰明的,你聲明的時候已經(jīng)明確告訴她類型了,后面實例化的時候就不用再寫一遍類型了。
既然IDE都這么提示我了,那我只能......偏不,我就寫,我還寫個不一樣的,比如:

List<兒子> list = new ArrayList<父親>();

這次IDE直接報錯了:incompatible types:List<兒子>,ArrayList<父親>
這句英文什么意思呢,就是IDE罵人了:讓你系安全帶你不系,你xx!
不好意思翻譯錯了,實際意思說的是:這兩個類型是矛盾的!
我們帶入上面的公式,得到??(兒子) = ArrayList<兒子>,??(父親) = ArrayList<父親>,如果泛型是逆變,則ArrayList<兒子>是ArrayList<父親>的父類,上面的例子報錯已經(jīng)證明了,ArrayList<兒子>并不是ArrayList<父親>的父類型,同樣泛型也不是協(xié)變,實際上泛型沒有任何繼承關(guān)系,也就是說泛型是不變的。
那怎么改呢?怎么申明類型才能又接收兒子又接受父親呢?這樣:

List<? super 兒子> list = new ArrayList<父親>();

這個類型不知道到底是兒子還是爸爸,所以寫成”?“(java通配符,代表任何類型),"? super 兒子"就表示這個類型可以是兒子或者是兒子的父類,那誰是兒子的父類呢,爸爸和爺爺,所以把爺爺捉過來放進去也沒問題。(爺爺說:莫挨老子)
也就是說,泛型是不變的,但是我們用別的辦法實現(xiàn)了泛型的逆變。

List<? super 兒子> list = new ArrayList<爺爺>();

“? super” 就實現(xiàn)了泛型的”逆變“,

那現(xiàn)在孫子還沒用上呢,再改一下:

List<兒子> list = new ArrayList<孫子>();

果然不出所料,IDE又開罵了:你XX。
不對啊,兒子是孫子的父類,正常情況下,是可以聲明一個父類變量給他賦值子類對象呀,比如兒子 erzi = new 孫子()。但是編譯器已經(jīng)報錯告訴你了
List<兒子>和 ArrayList<孫子>類型是矛盾的!也就是說兒子是孫子的父類,不代表List<兒子>就是 List<孫子>的父類,所以沒有繼承關(guān)系當然不能類型轉(zhuǎn)換,這里又驗證了一遍泛型是不變的。
趕緊改吧:

List<? extends 兒子> list = new ArrayList<孫子>();

不報錯了,"? extends 兒子"就表示這個類型可以是兒子或者兒子的子類。孫子是兒子的子類,所以沒問題。這就實現(xiàn)了泛型的”協(xié)變“。

上面的例子只做了賦值操作,在使用了協(xié)變或逆變后都可以讓賦值操作編譯正確。
但是當你想往list里存數(shù)據(jù)時,比如:

List<? extends 兒子> list = new ArrayList<孫子>();
孫子 sunzi =  new 孫子();
list.add(sunzi);

編譯會報如下錯誤:

Error:(40, 13) java: 對于add(decorator.MainTest.孫子), 找不到合適的方法
    方法 java.util.Collection.add(capture#1, 共 ? extends decorator.MainTest.兒子)不適用
      (參數(shù)不匹配; decorator.MainTest.孫子無法轉(zhuǎn)換為capture#1, 共 ? extends decorator.MainTest.兒子)
    方法 java.util.List.add(capture#1, 共 ? extends decorator.MainTest.兒子)不適用
      (參數(shù)不匹配; decorator.MainTest.孫子無法轉(zhuǎn)換為capture#1, 共 ? extends decorator.MainTest.兒子)

意思就是不能把孫子類型存到list中。實際上這個list不能存除了null之外的任何類型,包括兒子。也就是說List<? extends 兒子>喪失了”寫“的能力!
相對應的:

List<? super 兒子> list = new ArrayList<父親>();
父親 fuqin =  new 父親();
 list.add(fuqin);

一樣會報上面的錯誤,但是和? extends有點區(qū)別的是,這個list可以存null和兒子類型及其子類型(孫子)!
不信我們操作一下:

兒子 erzi =  new 兒子();
孫子 sunzi =  new 孫子();
list.add(erzi);
list.add(sunzi);
list.add(null);
list.forEach(System.out::println);//打印一下

打印結(jié)果:
decorator.MainTest$兒子@7ef20235
decorator.MainTest$孫子@27d6c5e0
null

奇怪了,定義的類型明明是兒子和兒子的父類,不能往里添加父親就算了,但是為啥可以往里添加兒子和兒子的子類?!

下面來探究為什么這兩個list不能完整的使用add方法,甚至不能使用add方法。
先打印下他們倆的類型:

List<? super 兒子> list = new ArrayList<父親>();
List<? extends 兒子> list2 = new ArrayList<孫子>();
System.out.println("list的類型是:" + list.getClass());
System.out.println("list2的類型是:" + list2.getClass());

打印結(jié)果:
list的類型是:class java.util.ArrayList
list2的類型是:class java.util.ArrayList

他兩都是ArrayList類型!<父親>,<孫子>這些都沒了,那我還在上面費勁吧啦的定義類型干什么!
那我們指定的類型去哪了呢?會不會在List內(nèi)部記錄了這個類型。

Class c = list.getClass();
Field[] fields = c.getDeclaredFields();
for (Field f : fields) {
       System.out.println("屬性名= " + f.getName() + "  屬性類型 = " + f.getType().getName());
}

打印結(jié)果:
屬性名= serialVersionUID 屬性類型 = long
屬性名= DEFAULT_CAPACITY 屬性類型 = int
屬性名= EMPTY_ELEMENTDATA 屬性類型 = [Ljava.lang.Object;
屬性名= DEFAULTCAPACITY_EMPTY_ELEMENTDATA 屬性類型 = [Ljava.lang.Object;
屬性名= elementData 屬性類型 = [Ljava.lang.Object;
屬性名= size 屬性類型 = int
屬性名= MAX_ARRAY_SIZE 屬性類型 = int

怎么肥事,elementData類型都是Object。也就是說這個list實際是可以存任意類型的!換句話說泛型的類型被抹去了,變成了Object(這也是為什么泛型不能是基本類型的原因,想存基本類型也只能用它的包裝類)。雖然編譯期在我們寫代碼的時候會檢查提示錯誤,但是我們可以用反射繞過檢查試一下:

 List<? extends 兒子> list2 = new ArrayList<孫子>();
 孫子 sunzi = new 孫子();
//        list2.add(sunzi);//會報錯
 list2.getClass().getMethod("add",Object.class).invoke(list2,sunzi);
 System.out.println(list2.get(0));

打印結(jié)果:
decorator.MainTest$孫子@5e2de80c

說明確實可以存進去,并且,還可以突破? extends 兒子這個限制,把兒子的父類傳進去都可以:

父親 fuqin =  new 父親();
 list2.getClass().getMethod("add",Object.class).invoke(list2,fuqin);
 System.out.println(list2.get(1));

打印結(jié)果:
decorator.MainTest$父親@5e2de80c

這不僅能驗證運行期間可以存任意類型,而且還能說明,編譯器對我們編寫的代碼,是先檢查我們定義的泛型的類型,然后再去編譯成可以存任意類型的,也就是對泛型的類型,編譯器是先“檢查”后“編譯并抹去類型”。
看一下編譯后生成的字節(jié)碼文件局部變量表,也沒有任何指定的泛型信息。

這里其實是java語言的一個特性,那就是java中的泛型是個偽泛型,編譯后泛型信息就沒了,只剩下了原始類型(原始類型是什么一會說),這個過程叫做”類型擦除”。
為什么要弄這個類型擦除呢,因為java5之前是沒有泛型的,也就是說list的add可以放任何類型,那么java5之后為了既能向下兼容,又要解決類型安全和類型自動轉(zhuǎn)換的問題,于是就設(shè)計成了類型擦除。
可是類型都被擦除了,我們調(diào)用add方法編譯器還會給我們報錯呢,原因上面我們已經(jīng)驗證過了:編譯器是先檢查后編譯擦除的。這其實也是泛型出現(xiàn)的一個原因:把對類型的檢查提前編譯之前,來確保類型安全,要知道泛型沒出現(xiàn)之前,list的add可以放任何類型,是非常不安全的。
kotlin和java一樣,也有類型擦除,所以你在運行時是沒法檢查你的泛型的:

 if(value instanceof List<String>)//java寫法:報錯
if(value is List<String>)//kotlin寫法:報錯

正確寫法就是java用不指定泛型實際類型或者使用通配符,kotlin用投影語法星號:

if(value instanceof List)//java寫法1
if(value instanceof List<?>)//java寫法2
if(value is List<*>)//kotlin寫法

到這里我們就知道了,設(shè)置的泛型類型其實并不會被帶到運行期,只是為了編譯前的一個安全檢查,所以add方法為什么會報錯實際和編譯器的檢查規(guī)則有關(guān):
1、當定義為List<? extends XXX>時,也就是對加入的元素進行了上限限制,表示可以加入的元素是XXX和XXX的子類,此時編譯器是不知道這個類型具體是哪一個的,編譯器是很怕死的,于是為了類型安全和類型自動轉(zhuǎn)換,編譯器就禁止add除了null以外任何類型,舉個例子:Integer和Double都extends了Number,那么當list定義為List<? extends Number>時,add(100)是禁止的,因為你這個100到底是是Integer還是Double?
那可能會疑惑,add都不能用了,那肯定也沒元素能取出來了,那這個list有什么意義呢,別忘了它是可以被賦值并取出元素的:

List<? extends 兒子> list2 = new ArrayList<孫子>();
List<孫子> list3 = new ArrayList<>();
孫子 sunzi = new 孫子();
list3.add(sunzi);
list2 = list3;
System.out.println(list2.get(0) );
//打印:decorator.MainTest$孫子@60e53b93

也就是說? extends這個限定是具有只讀特性的!
2、當定義為List<? super XXX>時,也就是對加入的元素進行了下限限制,此時可以加入的元素是XXX和XXX的父類,XXX的父類可能很多,鬼知道你要傳哪一個,因此此時編譯器還是不知道你傳的具體類型是哪一個,所以不允許add這個XXX類的父類,即使是Object這個上帝父類也不行,那么為什么允許add這個XXX類的子類呢?因為java中繼承的特性,XXX類的子類可以被看做XXX類,所以可以被當做XXX存放進去,只要不是XXX類的父類就行,因為編譯器不知道你要放哪個父類,它怕死啊。

那么原始類型是什么呢?就是泛型被擦除后的類型(如果沒有限定就是Object,有限定就是限定后的第一個)因為字節(jié)碼文件是被類型擦除后的,所以我們看一下字節(jié)碼文件:

List<? super 兒子> list = new ArrayList<父親>();
List<? extends 兒子> list2 = new ArrayList<孫子>();

因為兩個list我是直接定義在main方法中,所以去找一下main方法的局部泛型變量表(LocalVariableTypeTable這個表是專門保存泛型變量簽名的)看一下這兩個list的原始類型:



這里類型沒有顯示完整,但是告訴我們對應的是51和52索引的常量,我們跳轉(zhuǎn)過去看一下:




其中“兒子”就是原始類型:

“+”表示的就是“? extends”,表示上限限定,不可寫;
“-”表示的就是“? super”,表示下限限定,可寫入其和其子類。

在原始類型這一塊,kotlin和java使用上有一些不同,因為kotlin一開始就被設(shè)計成是有泛型概念的,所以kotlin中泛型定義是不支持把泛型定義成不指定類型的,你必須要指定泛型類型,舉例:
java中可以不指定泛型類型,表示這個列表中可以存放任意類型:

List numberList = new ArrayList<>(); //編譯通過

而kotlin不能這么寫,必須指定泛型類型:

val numberList:MutableList = mutableListOf() //編譯報錯:One type argument expected for interface MutableList<E>

val numberList:MutableList<Number> = mutableListOf()  //正確寫法
val numberList = mutableListOf<Number>() //正確寫法

在kotlin中,消費者(逆變)用關(guān)鍵字in,相當于java中的super;生產(chǎn)者(協(xié)變)用關(guān)鍵字out,相當于java中的extends。
(對于in和out兩個關(guān)鍵字,個人記憶的方式:協(xié)變是生產(chǎn)者,生產(chǎn)者是輸出生成的東西,所以是out,相反逆變是消費者,消費者是消費進來的東西,就是in。其實out和in分別也對應著函數(shù)的返回值位置和入?yún)⑽恢茫@是編譯器強制限制的)
最后再強調(diào)一下,協(xié)變不可寫,逆變可寫。需要從泛型“讀”用協(xié)變,需要往泛型“寫”用逆變。
來對比一下java中的List<E>和kotlin中List<out E>:
從類定義上可以看出java中的List是泛型不變的,所以可讀可寫,而kotlin中的List是協(xié)變的,所以只讀,看一下類結(jié)構(gòu):
java.util.List:



確實是可讀可寫;
kotlin.collections.List:



只有讀的方法,沒有寫(add/set/remov等)的方法。
所以在kotlin中你如果想讓你的列表可以動態(tài)增減數(shù)據(jù)就不能用List,而需要用無型變的MutableList。

3、kotlin中的泛型實化(泛型具體化reified)
泛型實化或者叫泛型具體化,是kotlin中對泛型擴展出的一種能力,優(yōu)點是讓原本會報錯的一些便捷寫法變成可能,比如:a as T,T::class.java
,直接的好處就是可以讓你的代碼寫起來更便捷。舉例:
比如請求網(wǎng)絡(luò),使用了Retrofit,一般都會寫一個Retrofit的單例類,對外提供一個方法,傳入某個ServiceApi的類型來生成一個serviceApi的對象。

object ServiceCreator {
    private const val BASE_URL = ""
    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    fun <T> create(serviceClass: Class<T>): T {
        return retrofit.create(serviceClass)
    }
}

外部在使用的時候就寫成:

ServiceCreator.create(AppService::class.java)

這樣寫不夠便捷,::class.java這么一大段實際都是為了機器更方便閱讀,對人來說不直觀,利用泛型具體化來讓它更便捷和直觀:
再寫一個方法:(泛型具體化的語法:inline和reified關(guān)鍵字)

inline fun <reified T> create():T = create(T::class.java)

外部在使用的時候就寫成:

ServiceCreator.create<AppService>()

十三: Lambda

正常方法的入?yún)⒍际且粋€個變量,lambda用白話說就是可以當方法入?yún)⒌拇a塊。(當然你也可以把lambda表達式賦值給一個變/常量)

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

首先是一個大括號包裹全部,內(nèi)部是參數(shù)列表,-> 符號表示參數(shù)列表結(jié)束,后面緊跟函數(shù)體,并且函數(shù)體中最后一行代碼就是這個lambda表達式的返回值。

如果定一個lambda表達式的變/常量,它的類型就是很長一串:(參數(shù)1類型,參數(shù)2類型)->最后一行代碼返回值類型

這其實是kotlin中的一個概念,叫做”函數(shù)類型“,是一種特殊的類型,用于高階函數(shù),簡單的理解就是這個類型聲明了一個函數(shù)的出入?yún)㈩愋头謩e是什么。
舉例:
已有一個列表:val list: List<String> = listOf<String>("Apple", "Banana", "Orange", "Pear")
需求:定義一個取長度的Lambda并且用一個常量保存它:

val lengthLambda = {fruit:String -> fruit.length}//類型:(String)->Int

lengthLambda是常量名,大括號中有一個參數(shù)fruit,參數(shù)類型是String,函數(shù)體只有一段代碼,fruit.length,所以lambda的返回值就是String。
現(xiàn)在需要寫一個方法,接收一個lambda當做入?yún)ⅲ祷匾粋€列表中長度最大的值:
思路:
因為涉及到列表循環(huán),所以我們直接用Iterable擴展函數(shù);又因為涉及到比較,所以用于比較的那個類型一定會繼承Comparable;我們用泛型來使方法適用于更多場景:

fun <T,R : Comparable<R>> Iterable<T>.myTestMaxBy(selector: (T) -> R) : T?

上面是我們的方法定義,首先是fun關(guān)鍵字,表示這是一個方法;
接著<>表示這個方法是一個泛型方法,尖括號里面的內(nèi)容:T,R : Comparable<R>表示泛型有兩個類型:T和R,其中R是Comparable的子類,表示它可以用Comparable的方法,這兩個類型放在我們上面的場景中分別表示的就是list的泛型類型(水果名字String)和用于比較的類型(水果名字長度Int);
再往后Iterable<T>.myTestMaxBy 表示myTestMaxBy這個方法是對Iterable類的擴展;
繼續(xù)往后,方法入?yún)⑹顷P(guān)鍵,我們需要定義的是一個lambda入?yún)ⅲ窃趺磳懩兀亢芎唵危凑5膮?shù)名:參數(shù)類型 這種格式寫即可。所以參數(shù)名我們叫selector,那么參數(shù)類型是什么?它的類型按我們上面定義的lengthLambda常量可以推導出是:(T) ->R;
最后這個方法需要返回列表里面長度最大的那個值,所以返回值類型就是列表的泛型T,允許為空T?。
(myTestMaxBy方法的入?yún)⑹且粋€函數(shù)類型,說明它是一個高階函數(shù))
具體實現(xiàn):

fun <T, R : Comparable<R>> Iterable<T>.myTestMaxBy(selector: (T) -> R): T? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null//如果集合中沒有元素,直接返回null
    var maxElement = iterator.next()
    if (!iterator.hasNext()) return maxElement//如果集合中只有一個元素。返回它
    var maxValue = selector(maxElement)
    do {
        val element = iterator.next()
        val value = selector(element)
        if (value > maxValue) {
            maxElement = element
            maxValue = value
        }
    } while (iterator.hasNext())
    return maxElement
}

其中if里面的比較就用到了Comparable方法的compareTo方法,可能有人說沒看到compareTo呀,那是因為compareTo是一個operator方法,我們在用大于小于符號的時候其實就是再調(diào)用這個方法,不信你別讓R繼承Comparable,if那里就報錯了。
最后可以把我們定義的lambda常量傳入到這個方法中:

val lambda:(String)->Int = { fruit: String -> fruit.length }
val maxLength :String?= list.myTestMaxBy(lambda)
println("列表里名字最長的水果 = ${maxLength}")//Banana

這個方法實際上和kotlin自帶的集合函數(shù)式API一樣:_Collections.kt:maxBy
上面的寫法可以簡化一下,因為一開始我們就說,lambda就是一種可以當入?yún)⒌拇a塊,所以不需要定義一個常量來保存它,直接把它全部復制往方法里一傳就完事了:

val maxLength = list.myTestMaxBy({ fruit: String -> fruit.length })

此時IDE會給你彈出一個建議:Lambda argument should be moved out of parentheses,意思是Lambda參數(shù)應該移到圓括號外面,實際上這是Kotlin中一個規(guī)定:當Lambda參數(shù)是函數(shù)最后一個參數(shù)時候,可以把Lambda移到括號外面:

val maxLength = list.myTestMaxBy(){ fruit: String -> fruit.length }

此時括號里沒有任何參數(shù)定義,括號也可以省了。
又因為Kotlin中類型會自動推導,所以fruit的類型也不用寫:

val maxLength = list.myTestMaxBy { fruit -> fruit.length }

kotlin中還有一個特性:當lambda表達式的參數(shù)列表只有一個時,參數(shù)定義都不用寫,可以用關(guān)鍵字it代替,當然這個隨便你,你要是覺得定義一些參數(shù)名更直觀,就保留好了:

val maxLength = list.myTestMaxBy { it.length }

可以嘗試自己實現(xiàn)一下集合的另一個API:map。
注意其中涉及到對集合的寫入,所以需要用到泛型逆變。逆變的原理在這篇文章第#十二。
列出幾個常用的集合函數(shù)式API:
map:把集合元素根據(jù)條件轉(zhuǎn)為另一種元素排出,和JAVA8 STREAM里的map一樣。
filter:返回符合過濾條件的元素。
any:判斷集合中是否至少存在一個元素滿足條件,返回boolean。
all:判斷集合中是否所有元素都滿足條件,返回boolean。

經(jīng)常能見到高階函數(shù)這樣定義:

fun SharedPreferences.edit(commit: Boolean = false, action: SharedPreferences.Editor.() -> Unit)

一、這個函數(shù)類型前面有一個“SharedPreferences.Editor.”,
1、含義和優(yōu)點:首先,這也是函數(shù)類型定義的一種語法規(guī)則。
這表示把函數(shù)類型定義在了SharedPreferences.Editor這個類中,并且這個函數(shù)類型內(nèi)部會自動擁有這個類的上下文。這是這種寫法的一個優(yōu)點,讓你可以在lambda中通過this(可省略)直接調(diào)用這個類的所有可用方法。
(看起來有點像擴展函數(shù),但是其實不是,你沒法在這個高階函數(shù)之外調(diào)用這個函數(shù)類型,因為它始終本身就是個特殊類型(函數(shù)類型))。
調(diào)用這個高階函數(shù):


在使用的地方看到IDE提示的this類型就是SharedPreferences.Editor類。
(此時lambda中如果用it調(diào)用可以嗎?答案是不行。后續(xù)分析會用到這個結(jié)論)


2、使用:用這種語法來定義函數(shù)類型聲明時,高階函數(shù)內(nèi)部調(diào)用它時有兩種寫法:

fun SharedPreferences.edit(commit: Boolean = false, action: SharedPreferences.Editor.() -> Unit) {
    val editor = this.edit()
    action(editor)//這樣調(diào)用沒問題
    editor.action()//這樣調(diào)用沒問題
}

3、原理:可以看到上面兩種調(diào)用方式,一個有入?yún)⒁粋€沒有入?yún)ⅲ覀兌x的時候也是一個空的括號,那么它到底有沒有入?yún)⒛兀繉嶋H上是有的,看一下反編譯后的代碼:


首先看到原本函數(shù)類型的位置現(xiàn)在是一個接口類型Function1:

public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}

這個接口只有一個函數(shù),這個函數(shù)只有1個入?yún)ⅲ‵unction2表示有2個入?yún)ⅲ渌麛?shù)字同理類推),并且是個泛型接口,定義了兩個泛型P1和R,分別用在了invoke方法的入?yún)㈩愋秃头祷仡愋汀5且驗樽止?jié)碼的類型擦除機制導致這里是看不到具體類型(不知道類型擦除機制的,往上看第十二條)
然后兩個調(diào)用的位置實際最后都被轉(zhuǎn)換成了調(diào)用Function1的invoke方法。實際入?yún)⒕褪撬诘念惖膶嵗龑ο螅ǚ祷貐?shù)是Unit)。
用大白話說就是你定義的函數(shù)類型被Function1類型替代了,你的函數(shù)類型調(diào)用的地方被Function1的invoke方法替代了。
再反編譯調(diào)用這個高階函數(shù)的SpUtil類:


調(diào)用SharedPreferences.edit時,new了一個 Function1對象進去,并且實現(xiàn)了invoke方法,invoke方法的入?yún)⒈粡娹D(zhuǎn)成了Editor類型使用,最后返回一個Unit對象,invoke方法內(nèi)部又調(diào)用了一個final方法(橋接),這個方法接收一個Editor類型,內(nèi)部的邏輯就是我們寫在lambda中的邏輯,一模一樣。
用大白話說就是你在lambda中寫的邏輯都被封裝成另一個方法,在invoke中被調(diào)用了。

二、在函數(shù)作用不變的前提下,如果換一種定義方式呢?
1、定義:

fun SharedPreferences.edit(commit: Boolean = false, action: (SharedPreferences.Editor) -> Unit) {
    val editor = this.edit()
    action(editor)//這樣調(diào)用沒問題
    editor.action()//這樣調(diào)用不行
}

2、和上一種寫法的區(qū)別:
這時候的action函數(shù)類型是(SharedPreferences.Editor) -> Unit,和上面寫法的區(qū)別是沒有把這個函數(shù)類型指定在某個類中了,那說明lambda中不可能在有這個類的上下文了,并且調(diào)用action的時候也只有傳入一個SharedPreferences.Editor類型的參數(shù)才能正確編譯了。
看一下反編譯:



和上一個寫法反編譯后的邏輯沒區(qū)別,同樣是調(diào)用Function1的invoke方法,傳入一個Editor實例。

那看一下調(diào)用這個高階函數(shù)的地方有沒有什么變化:



變化很大,原先的this已經(jīng)變成了it,雖然類型都還是SharedPreferences.Editor,但是已經(jīng)享受不到this帶來的省略寫法了,putString和putInt已然飄紅,需要用it.來調(diào)用它們。



把這個反編譯看一下:

和上一個寫法沒有本質(zhì)區(qū)別。
所以這種寫法和上一種寫法除了在你寫lambda內(nèi)部邏輯時有些區(qū)別(第一種寫法可以使用this,寫起來更方便),其他沒有區(qū)別。

三、現(xiàn)在想把第一和第二種寫法結(jié)合在一起,也就是在第二種寫法的基礎(chǔ)上,同時把這個函數(shù)類型給定義到SharedPreferences.Editor類中:



可以看到在使用action的兩個地方都報錯了:No value passed for parameter 'p2',意思是參數(shù)2沒有傳值。
啥也不管了直接看反編譯:(先把使用action的兩個地方注釋了)


image.png

可以看到之前是Function1的入?yún)⒆兂闪薋unction2:
/** A function that takes 2 arguments. */
public interface Function2<in P1, in P2, out R> : Function<R> {
    /** Invokes the function with the specified arguments. */
    public operator fun invoke(p1: P1, p2: P2): R
}

Function2這個接口的invoke方法有兩個入?yún)ⅲ琾1和p2,所以當我們使用action(editor)時會提示我們參數(shù)2沒有傳值,那我們給傳一下參數(shù)2:



這就不用反編譯看了吧,這兩個使用action的地方肯定都會轉(zhuǎn)變成action.invoke(editor, editor);
那么在使用這個高階函數(shù)的地方,lambda中是this還是it呢?
用it:



用this:

都可以!這和第一和第二種寫法就有區(qū)別了,第一種寫法只能用this,第二種寫法只能用it。
這樣其實沒啥意義,只是為了分析寫法的區(qū)別。/笑哭

四、現(xiàn)在還是用第三種寫法,但是我不把函數(shù)類型定義到Editor類中,我給它換個家,給它定義到String中,看看會咋樣:



反編譯:



這其實可以得到一個結(jié)論:如果這個函數(shù)類型被指定到了某一個類中,那么編譯后invoke的第一個入?yún)⒍际沁@個類的實例。
注意,下面開始好玩了:

在使用這個高階函數(shù)的地方,lambda中this和it兩種方式還可以使用嗎?可以使用的話this和it還是同一個類型嗎?
使用it:




可以看到it是Editor類型。
使用this:


可以看到this是String類型,并且Function2傳的是一個null的實例,那這樣的話lambda中的內(nèi)容是不是沒有被執(zhí)行?并不是,會被執(zhí)行,不信你打個log看一下。

明明傳進去是(Function2)null.INSTANCE,invoke方法都被看到被覆寫,怎么就執(zhí)行了呢?我也不知道,有知道的希望評論回復我,感謝!

最后總結(jié)下,在不涉及泛型的情況下還是用第一種(也即是把函數(shù)類型指定到某一個類中)的寫法既規(guī)范又簡便,使用起來更方便。

十四、密封類的作用

當你使用when時,kotlin語法會強制要求你寫else,即使你能確定這個else永遠用不上,這樣不方便的同時會有一個很大的風險:當你新增了一個條件,但是忘記在when對應的地方添加對應條件分支,編譯器也不會提醒你,這時候你新增的條件就會走到else中,這不是我們想要的,這個問題的本質(zhì)就是這個else,如果不用寫它就不會有這個問題,并且我還想要編譯器可以提醒我去在when中添加對應條件分支,這時候就可以用密封類來解決這個問題。
當when中傳入的是一個密封類,語法就允許我們不用寫else,并且當你新增一個密封類的子類時,編譯器會報錯,提醒你要在when中增加對應的條件分支。

十五、可見性控制

什么叫可見性,舉個安卓源碼中的例子:

ActivityThread是一個public的類,但是應用層開發(fā)者卻訪問不到這個類,因為用了可見性注解修飾@hide,表示其不作為對外Api被訪問。
在kotlin中對應internal關(guān)鍵字,比如在某個module中給某個類加了internal關(guān)鍵字,module中可以用這個類,但是在你的app工程就無法使用這個類了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370