一、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)接口的方法。
變量:
因為kotlin的繼承不允許子類有和父類一樣的變量名。。。除非父類里面變量是private或者子類override這個變量。。。
屬性的繼承這里有一個特別要注意的點,否則一不小心就空指針:
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對應的常量池里的常量信息:
可以看到方法的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的兩個地方注釋了)
可以看到之前是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工程就無法使用這個類了。