Kotlin開發筆記:集合和逆變協變
Kotlin中的集合
基本的集合類型
Kotlin中的集合類型和Java差不多,不過有些在名稱上可能有出入,下面是Kotlin中的一些基本集合類型:
類型 | 介紹 |
---|---|
Pair | 兩個值的元組 |
Triple | 三個值的元組 |
Array | 經過索引的,固定大小的對象和基元集合 |
List | 有序的對象集合 |
Set | 無序的對象集合 |
Map | 鍵值對對象集合 |
Kotlin中的視圖
在Kotlin引入了視圖的概念,簡而言之,不同的視圖類型會賦予我們對操作集合的不同權限。Kotlin中有兩種不同的視圖:只讀或不可變視圖,以及讀寫或可變視圖。
比如對于List來說,有兩種視圖,分別是List和MutableList,前者提供只讀視圖,后者提供讀寫視圖。當我們用List視圖時將無法修改列表,而用MutableList就可以。
var li = listOf(1,2,3) as MutableListli.add(5)
比如我們運行上述代碼就會報錯,因為listOf函數會產生List視圖的集合,這將導致我們無法修改列表,如果我們要續寫就需要產生讀寫視圖的列表:
var li = mutableListOf(1,2,3) li.add(5)
不過本質上這兩種視圖都是對List的引用,比如說我們可以用List視圖引用同一個ArrayList:
val ar = arrayListOf(1,2,3,4)val li:List<Int> = arval li1:MutableList<Int> = ar
不過List視圖的引用將無法修改列表本身。
Kotlin中的一些技巧
使用listOf等函數快速創建集合
這個其實在上面給的例子里已經體現了,我們可以使用arrayListOf,listOf等函數快速創建出我們想要的集合而無需再用構造函數。
使用to和mapOf快速創建表
Kotlin中提供了一個to拓展函數,這個函數將生成一個Pair類型的對象,比如
val p1 = "age" to 18
將會創建一個First為"age",Second為18的Pair對象。而這個對象又可以用于mapOf函數。這樣我們就可以快速創建一個map,比如:
val mMap = mapOf("age" to 18,"code" to 10086)
這樣就創建了一個鍵值對為< String , Int >類型的map,其中to之前的為Key,之后的為Value。
同時獲取索引和值
在Java中,如果我們想要同時獲取一個List的索引和值的話可能需要遍歷或者采取別的手段來達到這個目的,而在Kotlin中,我們可以用解構來實現這個目的:
fun main() {val li = listOf("jack","anderson")for((index,value) in li.withIndex()){println("index : $index, value:$value")}
}
withIndex將返回一個包含鍵值對的對象,我們將其解構出來就可以同時獲得索引和值了。
創建有規律的數組
接下來介紹的是如何創建出一個有規律的數字,比如我們可以創建出一個物的倍數的數組:
fun main() {val li = Array(5){index -> index * 5}for(value in li){println(value)}
}
Array括號后面的5是元素個數,index下標是從0開始,我們可以打印出值:
成功創建了一個包含五的倍數的數組,利用這個技巧我們再加上Array內置的一些方法,就可以實現許多計算,比如我們想要計算從1到5的平方和的話就可以直接這樣寫:
fun main() {val li = Array(5){index -> (index+1) * (index+1)}.sum()println(li)
}
使用in
在Java中如果我們想要判斷一個元素是否在一個集合中,一般會使用contains方法,不過在Kotlin中提供了in運算符實現了同樣的效果:
fun main() {val li = Array(5){index -> index*5}println(0 in li)
}
實際上在迭代時我們會用到in運算符也是這樣。
Kotlin中的逆變和協變
什么是逆變和協變
首先我們需要介紹逆變和協變的概念,協變和逆變都是術語,前者指能夠使用比原始指定的派生類型的派生程度更大(更具體的)的類型,后者指能夠使用比原始指定的派生類型的派生程度更小(不太具體的)的類型。
以我的理解,協變應該接近于extend,而逆變接近于super。
默認情況下,在Java中泛型強制實行類型不變性–也就是說,如果泛型函數期望一個參數類型T,則不允許替換基類型T或者派生類型T,類型必須是完全預期的類型
實際上,在Java中我也沒有對通配符和一般的泛型T的區別和相同有什么很深的理解。我的理解是,通配符?代表不確定的類型,泛型類型T代表確定的類型。
類型不變性
這里再介紹一下類型不變性,當一個方法接收到一個類型為T的對象(確定對象,不是泛型對象)時,我們可以傳入為T類型或者是T的子類的對象。比如如果一個方法接收一個Animal類型的對象,那么身為Animal子類的Cat類型的對象也可以被傳進去。
但是,如果這個方法接收的是一個泛型類型為T的對象,那么將不允許傳遞派生類型為T的泛型對象。比如,如果可以傳遞List< Animal >類型的對象,那么將不允許傳入List< Cat >類型的對象,這和Java中的類型擦除有關。
書上的一個例子我覺得很形象,比如說我們創建一個Fruit類和兩個繼承它的類還有一個接收水果的方法:
open class Fruit
class Orange:Fruit()
class Banana:Fruit()fun receiveFruits(fruits:Array<Fruit>){println("水果的數量是${fruits.size}")
}
這個方法可以接受泛型類型為Fruit的數組,如果我們傳入Orange或者Banana會怎么樣呢?
可以看到,編譯器提示類型不匹配了。香蕉是從水果繼承而來的,但是顯然一籃子香蕉不是從一籃子水果繼承而來的。
不過一旦我們用list視圖來操作,上述代碼就不會報錯了:
這是因為List視圖只允許我們進行讀而不允許我們進行寫,這樣是安全的。在Kotlin中,這個效果是由于List視圖是out修飾的,我們將在后面的協變中介紹。
使用協變
上邊介紹到了,一旦我們使用List視圖,那么receiveFruits方法就可以被調用了,這正是由于使用了協變的原因。接下來我們創建一個方法來模擬協變的使用場景,比如我們想要把一個Fruit的Array復制到另一個Fruit的Array中:
fun copyFromTo(from:Array<Fruit>,to:Array<Fruit>){for(i in 0 until from.size){to[i] = from[i]}
}
這種情況下我們顯然不能傳入除Fruit類之外的泛型類,比如:
編譯器是不會允許我們傳入泛型類型為Banana的參數給from的,這個時候我們只需要修改一下這個函數,在傳入的from參數處使用協變即可:
fun copyFromTo(from:Array<out Fruit>,to:Array<Fruit>){for(i in 0 until from.size){to[i] = from[i]}
}
這樣編譯器就不會報錯了。要理解這個協變的含義我們可以從編譯器為什么不讓我們傳入Banana類型的參數看。如果我們可以傳入Banana類型的參數,我們就有可能對Banana執行一些Fruit層面的指令。
舉個例子來說,大部分水果沖洗完成之后就可以直接食用了,但是香蕉的果皮較厚,我們就不能直接食用,在這之前還需要剝皮。身為子類的Banana🍌肯定是有其特殊之處的,不能用基類Fruit的一些操作直接用在Banana上。但是如果我們不對這個Banana進行操作的話,那么就不會有什么大問題了,這就是協變的含義。
這里對copyFromTo方法的from參數加上out參數后就說明我們不會對這個from參數進行任何方法的調用了,我們只是單單讀取這個參數,這樣編譯器就允許我們傳入Fruit的子類的泛型類型了,換言之,我們就實現了協變。這種在使用泛型類型時使用協變的行為稱之為“使用點型變”。
使用逆變
與協變相對的就是逆變了,如果說協變是只讀不寫的話,那么逆變就是只寫不讀。實際上也確實是這樣,使用逆變將允許我們在該參數上進行設置值的方法調用,而不允許讀取的方法。
我們依舊以上面的copyFromTo方法為例,現在我們希望可以將任意Fruit或者Fruit子類的元素復制到Fruit或Fruit超類的集合中,比如說我們傳一個Any類的參數:
顯然由于類型不變性這樣是行不通的,在這里我們再次對copyFromTo方法做修改,這次我們對to參數使用逆變:
fun copyFromTo(from:Array<out Fruit>,to:Array<in Fruit>){for(i in 0 until from.size){to[i] = from[i]}
}
這樣編譯器就允許我們這樣調用了。
使用Where的參數類型約束
這部分內容說白了就是約束泛型類型的范圍,比如說我們有一個方法需要傳入一個泛型類,這個泛型類需要實現AutoCloseable接口,那么我們就可以這樣寫
fun <T:AutoCloseable> useAndClose(input:T)
{input.close()
}
實際上上面的和Java中的寫法也差不多,不過如果是一個泛型需要實現多個接口的話就不能這么寫了,需要我們用where參數進行約束:
fun <T> useAndClose(input:T)where T:AutoCloseable,T:Appendable
{input.append("haha")input.close()
}
where約束跟在參數列表后面,花括號前面。約束參數中用逗號分隔。
星投影
星投影用<*>定義參數類型,它是指定泛型只讀類型和原始類型的Kotlin等效物,**簡單來說,我們可以用星投影捕獲泛型類型,但是我們只能對捕獲的泛型類型進行讀取而不能修改。**當你想表達對類型不太了解但有希望類型安全時,請使用星投影,星投影只允許讀出而不允許寫入,比如:
fun printValues(values:Array<*>){for(value in values){println(value)}
}
在這個方法中我們用星投影捕獲了泛型類型,但是我們只能讀取values值不能寫入或者更改values值,實際上就相當于out T,但是寫起來更簡潔。