第8章 泛型
通常情況的類和函數(shù),我們只需要使用具體的類型即可:要么是基本類型,要么是自定義的類。但是在集合類的場景下,我們通常需要編寫可以應(yīng)用于多種類型的代碼,我們最簡單原始的做法是,針對每一種類型,寫一套刻板的代碼。這樣做,代碼復(fù)用率會很低,抽象也沒有做好。我們能不能把“類型”也抽象成參數(shù)呢?是的,當(dāng)然可以。
Java 5 中引入泛型機(jī)制,實(shí)現(xiàn)了“參數(shù)化類型”(Parameterized Type)。參數(shù)化類型,顧名思義就是將類型由原來的具體的類型參數(shù)化,類似于方法中的變量參數(shù),此時類型也定義成參數(shù)形式,我們稱之為類型參數(shù),然后在使用時傳入具體的類型(類型實(shí)參)。
我們知道,在數(shù)學(xué)中泛函是以函數(shù)為自變量的函數(shù)。類比的來理解,編程中的泛型就是以類型為變量的類型,即參數(shù)化類型。這樣的變量參數(shù)就叫類型參數(shù)(Type Parameters)。
本章我們來一起學(xué)習(xí)一下Kotlin泛型的相關(guān)知識。
8.1 為何引入泛型
《Java編程思想 》(第4版)中提到:有許多原因促成了泛型的出現(xiàn),而最引人注意的一個原因,就是為了創(chuàng)建容器類 (集合類)。
集合類可以說是我們在寫代碼過程中最最常用的類之一。我們先來看下沒有泛型之前,我們的集合類是怎樣持有對象的。在Java中,Object類是所有類的根類。為了集合類的通用性,把元素的類型定義為Object,當(dāng)放入具體的類型的時候,再作相應(yīng)的強(qiáng)制類型轉(zhuǎn)換。
這是一個示例代碼:
class RawArrayList {
public int length = 0;
private Object[] elements; // 把元素的類型定義為Object
public RawArrayList(int length) {
this.length = length;
this.elements = new Object[length];
}
public Object get(int index) {
return elements[index];
}
public void add(int index, Object element) {
elements[index] = element;
}
}
一個簡單的測試代碼如下
public class RawTypeDemo {
public static void main(String[] args) {
RawArrayList rawArrayList = new RawArrayList(4);
rawArrayList.add(0, "a");
rawArrayList.add(1, "b");
System.out.println(rawArrayList);
String a = (String)rawArrayList.get(0);
System.out.println(a);
String b = (String)rawArrayList.get(1);
System.out.println(b);
rawArrayList.add(2, 200);
rawArrayList.add(3, 300);
System.out.println(rawArrayList);
int c = (int)rawArrayList.get(2);
int d = (int)rawArrayList.get(3);
System.out.println(c);
System.out.println(d);
String x = (String)rawArrayList.get(2); //Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
System.out.println(x);
}
}
我們可以看出,在使用原生態(tài)類型(raw type)實(shí)現(xiàn)的集合類中,我們使用的是Object[]數(shù)組。這種實(shí)現(xiàn)方式,存在的問題有兩個:
向集合中添加對象元素的時候,沒有對元素的類型進(jìn)行檢查,也就是說,我們往集合中添加任意對象,編譯器都不會報(bào)錯。
當(dāng)我們從集合中獲取一個值的時候,我們不能都使用Object類型,需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換。而這個轉(zhuǎn)換過程由于在添加元素的時候沒有作任何的類型的限制跟檢查,所以容易出錯。例如上面代碼中的:
String x = (String)rawArrayList.get(2); //Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
對于這行代碼,編譯時不會報(bào)錯,但是運(yùn)行時會拋出類型轉(zhuǎn)換錯誤。能不能讓編譯器來解決這樣的樣板化的類型轉(zhuǎn)換代碼呢?當(dāng)我們向rawArrayList 添加元素的時候
rawArrayList.add(0, "a");
就限定其元素類型只能為String,那么在后面的獲取元素的時候,自動強(qiáng)制轉(zhuǎn)型為String 呢?
String a = (String)rawArrayList.get(0);
這個元素類型 String 的信息,我們存放到 一個“類型參數(shù)”中,然后在編譯器層面引入相應(yīng)的類型檢查和自動轉(zhuǎn)換機(jī)制,這樣就可以解決這個類型安全使用的問題。這也正是引入的泛型的基本思想。
泛型最主要的優(yōu)點(diǎn)就是讓編譯器追蹤參數(shù)類型,執(zhí)行類型檢查和類型轉(zhuǎn)換。因?yàn)橛删幾g器來保證類型轉(zhuǎn)換不會失敗。如果依賴我們程序員自己去追蹤對象類型和執(zhí)行轉(zhuǎn)換,那么運(yùn)行時產(chǎn)生的錯誤將很難去定位和調(diào)試,然而有了泛型,編譯器 可以幫助我們執(zhí)行大量的類型檢查,并且可以檢測出更多的編譯時錯誤。在這一點(diǎn)上,泛型跟我們第3章中所講到的“可空類型”實(shí)現(xiàn)的空指針安全,在思想上有著異曲同工之妙。
8.2 在類、接口和函數(shù)上使用泛型
泛型類、泛型接口和泛型方法具備可重用性、類型安全和高效等優(yōu)點(diǎn)。在集合類API中大量地使用了泛型。在Java 中我們可以為類、接口和方法分別定義泛型參數(shù),在Kotlin中也同樣支持。本節(jié)我們分別介紹Kotlin中的泛型接口、泛型類和泛型函數(shù)。
8.2.1 泛型接口
我們舉一個簡單的Kotlin泛型接口的例子。
interface Generator<T> { // 類型參數(shù)放在接口名稱后面: <T>
operator fun next(): T // 接口函數(shù)中直接使用類型 T
}
測試代碼
fun testGenerator() {
val gen = object : Generator<Int> { // 對象表達(dá)式
override fun next(): Int {
return Random().nextInt(10)
}
}
println(gen.next())
}
這里我們使用object 關(guān)鍵字來聲明一個Generator實(shí)現(xiàn)類,并在lambda表達(dá)式中實(shí)現(xiàn)了next() 函數(shù)。
Kotlin 中 Map 和 MutableMap 接口的定義也是一個典型的泛型接口的例子。
public interface Map<K, out V> {
...
public fun containsKey(key: K): Boolean
public fun containsValue(value: @UnsafeVariance V): Boolean
public operator fun get(key: K): V?
...
public val keys: Set<K>
public val values: Collection<V>
public val entries: Set<Map.Entry<K, V>>
}
public interface MutableMap<K, V> : Map<K, V> {
public fun put(key: K, value: V): V?
public fun remove(key: K): V?
public fun putAll(from: Map<out K, V>): Unit
...
}
例如,我們使用 mutableMapOf 函數(shù)來實(shí)例化一個可變Map
>>> val map = mutableMapOf<Int,String>(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}
其中,mutableMapOf 函數(shù)簽名如下
fun <K, V> mutableMapOf(vararg pairs: Pair<K, V>): MutableMap<K, V>
這里類型參數(shù) K,V 當(dāng)泛型類型被實(shí)例化和使用時,它將被一個實(shí)際的類型參數(shù)所替代。在 mutableMapOf<Int,String> 中,放置K, V 的位置被具體的Int 和 String 類型所替代。
泛型可以用來限制集合類持有的對象類型,這樣使得類型更加安全。當(dāng)我們在一個集合類里面放入了錯誤類型的對象,編譯器就會報(bào)錯:
>>> map.put("5","e")
error: type mismatch: inferred type is String but Int was expected
map.put("5","e")
^
Kotlin中有類型推斷的功能,有些類型參數(shù)可以直接省略不寫。mutableMapOf<Int,String> 后面的類型參數(shù) <Int,String> 可以省掉不寫:
>>> val map = mutableMapOf(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}
8.2.2 泛型類
我們直接聲明一個帶類型參數(shù)的 Container 類
class Container<K, V>(var key: K, var value: V)
為了方便測試,我們重寫 toString() 函數(shù)
class Container<K, V>(var key: K, var value: V){ // 在類名后面聲明泛型參數(shù)<K, V> , 多個泛型使用逗號隔開
override fun toString(): String {
return "Container(key=$key, value=$value)"
}
}
測試代碼
fun testContainer() {
val container = Container<Int, String>(1, "A") // <K, V> 被具體化為<Int, String>
println(container) // container = Container(key=1, value=A)
}
8.2.3 泛型函數(shù)
在泛型接口和泛型類中,我們都在類名和接口名后面聲明了泛型參數(shù)。而實(shí)際上,我們也可以直接在類或接口中的函數(shù),或者直接在包級函數(shù)中直接聲明泛型參數(shù)。代碼示例如下
class GenericClass {
fun <T> console(t: T) { // 類中的泛型函數(shù)
println(t)
}
}
interface GenericInterface {
fun <T> console(t: T) // 接口中的泛型函數(shù)
}
fun <T : Comparable<T>> gt(x: T, y: T): Boolean { // 包中的泛型函數(shù)
return x > y
}
8.3 類型上界
在上面的例子中,我們有看到 gt(x:T, y:T) 函數(shù)的簽名中有個 T : Comparable<T>
fun <T : Comparable<T>> gt(x: T, y: T): Boolean
這里的 T : Comparable<T> ,表示 Comparable<T>是類型 T 的上界。也就是告訴編譯器,類型參數(shù) T 代表的都是實(shí)現(xiàn)了 Comparable<T> 接口的類,這樣等于告訴編譯器它們都實(shí)現(xiàn)了compareTo方法。如果沒有這個類型上界聲明,我們就無法直接使用 compareTo ( > )操作符。也就是說,下面的代碼編譯不通過
fun <T> gt(x: T, y: T): Boolean {
return x > y // 編譯不通過
}
8.4 協(xié)變與逆變
我們來看一個問題場景。首先,我們有下面的存在父子關(guān)系的類型
open class Food
open class Fruit : Food()
class Apple : Fruit()
class Banana : Fruit()
class Grape : Fruit()
然后,我們有下面的兩個函數(shù)
object GenericTypeDemo {
fun addFruit(fruit: MutableList<Fruit>) {
// TODO
}
fun getFruit(fruit: MutableList<Fruit>) {
// TODO
}
}
這個時候,我們可以這樣調(diào)用上面的兩個函數(shù)
val fruits: MutableList<Fruit> = mutableListOf(Fruit(), Fruit(), Fruit())
GenericTypeDemo.addFruit(fruits)
GenericTypeDemo.getFruit(fruits)
現(xiàn)在,我們又有一個存放Apple的List
val apples: MutableList<Apple> = mutableListOf(Apple(), Apple(), Apple())
由于Kotlin中的泛型跟Java一樣是非協(xié)變的,下面的調(diào)用是編譯不通過的
GenericTypeDemo.addFruit(apples) // type mismatch
GenericTypeDemo.getFruit(apples) // type mismatch
如果沒有協(xié)變,那么我們不得不再添加兩個函數(shù)
object GenericTypeDemo {
fun addFruit(fruit: MutableList<Fruit>) {
// TODO
}
fun getFruit(fruit: MutableList<Fruit>) {
// TODO
}
fun addApple(apple: MutableList<Apple>) {
// TODO
}
fun getApple(apple: MutableList<Apple>) {
// TODO
}
}
我們一眼就能看出,這是重復(fù)的樣板代碼。我們能不能讓 MutableList<Fruit> 成為 MutableList<Apple> 的父類型呢? Java泛型中引入了類型通配符的概念來解決這個問題。Java 泛型的通配符有兩種形式:
子類型上界限定符 ? extends T 指定類型參數(shù)的上限(該類型必須是類型T或者它的子類型)。也就是說MutableList<? extends Fruit> 是 MutableList<Apple> 的父類型。 Kotlin中使用 MutableList<out Fruit> 來表示。
超類型下界限定符 ? super T 指定類型參數(shù)的下限(該類型必須是類型T或者它的父類型)。也就是說MutableList<? super Fruit> 是 MutableList<Object>的父類型。Kotlin中使用 MutableList<in Fruit> 來表示。
這里的問號 (?) , 我們稱之為類型通配符(Type Wildcard)。通配符在類型系統(tǒng)中具有重要的意義,它們?yōu)橐粋€泛型類所指定的類型集合提供了一個有用的類型范圍。
Number 類型(簡記為F) 是 Integer 類型(簡記為C)的父類型,我們把這種父子類型關(guān)系簡記為:C => F (C 繼承 F);而List<Number>, List<Integer>的代表的泛型類型信息,我們分別簡記為 f(F), f(C)。
那么我們可以這么來描述協(xié)變和逆變:
當(dāng) C => F 時, 如果有 f(C) => f(F), 那么 f 叫做協(xié)變;
當(dāng) C => F 時, 如果有 f(F) => f(C), 那么 f 叫做逆變。
如果上面兩種關(guān)系都不成立則叫做不變。
協(xié)變與逆變可以用下圖來簡單說明
協(xié)變和逆協(xié)變都是類型安全的。
8.4.1 協(xié)變
在Java中數(shù)組是協(xié)變的,下面的代碼是可以正確編譯運(yùn)行的:
Integer[] ints = new Integer[3];
ints[0] = 0;
ints[1] = 1;
ints[2] = 2;
Number[] numbers = new Number[3];
numbers = ints;
for (Number n : numbers) {
System.out.println(n);
}
在Java中,因?yàn)?Integer 是 Number 的子類型,數(shù)組類型 Integer[] 也是 Number[] 的子類型,因此在任何需要 Number[] 值的地方都可以提供一個 Integer[] 值。Java中數(shù)組協(xié)變的意思可以用下圖簡單說明
Java中泛型是非協(xié)變的。如下圖所示
也就是說, List<Integer> 不是 List<Number> 的子類型,試圖在要求 List<Number> 的位置提供 List<Integer> 是一個類型錯誤。下面的代碼,編譯器是會直接報(bào)錯的:
List<Integer> integerList = new ArrayList<>();
integerList.add(0);
integerList.add(1);
integerList.add(2);
List<Number> numberList = new ArrayList<>();
numberList = integerList; // 編譯錯誤:類型不兼容
編譯器報(bào)錯提示如下:
Java中泛型和數(shù)組的不同行為,的確引起了許多混亂。就算我們使用通配符,這樣寫:
List<? extends Number> list = new ArrayList<Number>();
list.add(new Integer(1)); //error
仍然是報(bào)錯的:
這通常會讓我們感到困惑:為什么Number的對象可以由Integer實(shí)例化,而ArrayList<Number>的對象卻不能由ArrayList<Integer>實(shí)例化?list中的<? extends Number>聲明其元素是Number或Number的派生類,為什么不能add Integer? 為了弄清楚這些問題,我們需要了解Java中的逆變和協(xié)變以及泛型中通配符用法。
List<? extends Number> list = new ArrayList<>();
這里的子類型 C 就是 Number類及其子類(例如Number、Integer、Float等) ,表示的是 Number 類或其子類。父類 F 就是上界通配符: ? extends Number。
當(dāng) C => F ,這個關(guān)系成立:f(C) => f(F) , 這就是協(xié)變。我們把 f(F) 具體化為 List<? extends Number>, f(C) 具體化為 List<Integer> 、List<Float>等。 協(xié)變代表的意義就是: List<? extends Number> 是 List<Integer> 、List<Float>等的父類型。如下圖所示
代碼示例
List<? extends Number> list1 = new ArrayList<Integer>();
List<? extends Number> list2 = new ArrayList<Float>();
但是這里不能向list1、list2添加除null以外的任意對象。
list1.add(null); // ok
list2.add(null);// ok
list1.add(new Integer(1)); // error
list2.add(new Float(1.1f)); // error
List<Integer>可以添加Interger及其子類;
List<Float>可以添加Float及其子類;
List<Integer>、List<Float>等都是 List<? extends Number>的子類型。
現(xiàn)在問題來了,如果能將Float的子類添加到 List<? extends Number>中,那么也能將Integer的子類添加到 List<? extends Number>中, 那么這時候 List<? extends Number> 里面將會持有各種Number子類型的對象(Byte,Integer,F(xiàn)loat,Double等)。而這個時候,當(dāng)我們再使用這個list的時候,元素的類型就會混亂。我們不知道哪個元素會是Integer或者Float 。Java為了保護(hù)其類型一致,禁止向List<? extends Number>添加任意對象,不過可以添加空對象null。
8.4.2 逆變
我們先用一段代碼舉例
List<? super Number> list = new ArrayList<Object>();
這里的子類型 C 是 ? super Number , 父類型 F 是 Number 的父類型(例如:Object類)。
當(dāng) C => F , 有 f(F) => f(C) , 這就是逆變。我們把 f (C) 具體化為 List<? super Number> ,f(F) 具體化為List<Object> 。逆變的意思就是說 List<? super Number> 是 List<Object> 的父類型。如下圖所示
代碼示例:
List<? super Number> list3 = new ArrayList<Number>();
List<? super Number> list4 = new ArrayList<Object>();
list3.add(new Integer(3));
list4.add(new Integer(4));
在逆變類型中,我們可以向其中添加元素。例如,我們可以向 List<? super Number > list4 變量中添加Number及其子類對象。
8.4.3 PECS
現(xiàn)在問題來了:我們什么時候用extends什么時候用super呢?《Effective Java》給出了答案:
PECS: producer-extends, consumer-super
下面我們通過實(shí)例來說明PECS的具體含義。
首先,我們聲明一個簡單的Stack 泛型類如下
public class Stack<E>{
public Stack();
public void push(E e):
public E pop();
public boolean isEmpty();
}
要實(shí)現(xiàn)pushAll(Iterable<E> src)方法,將src的元素逐一入棧
public void pushAll(Iterable<E> src){
for(E e : src)
push(e)
}
假設(shè)有一個Stack<Number>(類型參數(shù)E 具體化為 Number 類型)實(shí)例化的對象stack,src有 Iterable<Integer> 與 Iterable<Float>,那么在調(diào)用pushAll方法時會發(fā)生type mismatch錯誤,因?yàn)镴ava中泛型是不可變的,Iterable<Integer>與 Iterable<Float>都不是Iterable<Number>的子類型。
因此,pushAll(Iterable<E> src)方法簽名應(yīng)改為
// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e : src) // out T, 從src中讀取數(shù)據(jù),producer-extends
push(e);
}
這樣就實(shí)現(xiàn)了泛型的協(xié)變。同時,我們從src中讀取的數(shù)據(jù)都能保證是E類型及其子類型的對象。
現(xiàn)在,我們再看 popAll(Collection<E> dst)方法,該方法將Stack中的元素依次取出add到dst中,如果不用通配符實(shí)現(xiàn):
// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
同樣地,假設(shè)有一個實(shí)例化Stack<Number>的對象stack,dst為Collection<Object>;調(diào)用popAll方法是會發(fā)生type mismatch錯誤,因?yàn)镃ollection<Object>不是Collection<Number>的子類型。
因而,popAll(Collection<E> dst) 方法應(yīng)改為:
// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) { // 保證dst中的元素都是E類型或者E的父類型
while (!isEmpty())
dst.add(pop()); // in T, 向dst中寫入數(shù)據(jù), consumer-super
}
因?yàn)?pop() 返回的數(shù)據(jù)類型是E, 而dst中的元素都是E類型或者E的父類型,所以我們可以安全地寫入E類型的數(shù)據(jù)。
Naftalin與Wadler將PECS稱為 Get and Put Principle。
在java.util.Collections
的copy
方法中(JDK1.7)完美地詮釋了PECS:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator(); // in T, 寫入dest數(shù)據(jù)
ListIterator<? extends T> si=src.listIterator(); // out T, 讀取src數(shù)據(jù)
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
8.5 out T 與 in T
正如上文所講的,在 Java 泛型里,有通配符這種東西,我們要用? extends T
指定類型參數(shù)的上限,用 ? super T
指定類型參數(shù)的下限。
而Kotlin 拋棄了這個東西,直接實(shí)現(xiàn)了上文所講的PECS的規(guī)則。Kotlin 引入了投射類型 out T 代表生產(chǎn)者對象,投射類型 in T 代表消費(fèi)者對象。Kotlin使用了投射類型( projected type ) out T 和 in T 來實(shí)現(xiàn)了類型通配符同樣的功能。
我們用代碼示例簡單講解一下:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
ListIterator<? super T> di=dest.listIterator(); // in T, 寫入dest數(shù)據(jù)
ListIterator<? extends T> si=src.listIterator(); // out T, 讀取src數(shù)據(jù)
...
}
List<? super T> dest 是消費(fèi)數(shù)據(jù)的對象,數(shù)據(jù)會被寫入到 dest 對象中,這些數(shù)據(jù)該對象被“吃掉”了(Kotlin中叫in T
)。
List<? extends T> src 是生產(chǎn)提供數(shù)據(jù)的對象。src 會“吐出”數(shù)據(jù)(Kotlin中叫out T
)。
在Kotlin中,我們把那些只能保證讀取數(shù)據(jù)時類型安全的對象叫做生產(chǎn)者,用 out T
標(biāo)記;把那些只能保證寫入數(shù)據(jù)安全時類型安全的對象叫做消費(fèi)者,用 in T
標(biāo)記。
如果你覺得太晦澀難懂,就這么記吧:
out T
等價(jià)于? extends T
in T
等價(jià)于? super T
8.6 類型擦除
Java和Kotlin 的泛型實(shí)現(xiàn),都是采用了運(yùn)行時類型擦除的方式。也就是說,在運(yùn)行時,這些類型參數(shù)的信息將會被擦除。
泛型是在編譯器層次上實(shí)現(xiàn)的。生成的 class 字節(jié)碼文件中是不包含泛型中的類型信息的。例如在代碼中定義的List<Object>和List<String>等類型,在編譯之后都會變成List。JVM看到的只是List,而由泛型附加的類型信息對JVM來說是不可見的。
關(guān)于泛型的很多奇怪特性都與這個類型擦除的存在有關(guān),比如:泛型類并沒有自己獨(dú)有的Class類對象。比如Java中并不存在List<String>.class或是List<Integer>.class,而只有List.class。對應(yīng)地在Kotlin中并不存在MutableList<Fruit>::class, 而只有 MutableList::class 。
類型擦除的基本過程也比較簡單:
首先,找到用來替換類型參數(shù)的具體類。這個具體類一般是Object。如果指定了類型參數(shù)的上界的話,則使用這個上界。
其次,把代碼中的類型參數(shù)都替換成具體的類。同時去掉出現(xiàn)的類型聲明,即去掉<>的內(nèi)容。比如, T get() 就變成了Object get(), List<String> 就變成了List。
最后,根據(jù)需要生成一些橋接方法。這是由于擦除了類型之后的類可能缺少某些必須的方法。這個時候就由編譯器來動態(tài)生成這些方法。
當(dāng)了解了類型擦除機(jī)制之后,我們就會明白是編譯器承擔(dān)了全部的類型檢查工作。編譯器禁止某些泛型的使用方式,也正是為了確保類型的安全性。
本章小結(jié)
泛型是一個非常有用的東西。尤其在集合類中。我們可以發(fā)現(xiàn)大量的泛型代碼。有了泛型,我們可以擁有更強(qiáng)大更安全的類型檢查、無需手工進(jìn)行類型轉(zhuǎn)換,并且能夠開發(fā)更加通用的泛型算法。
Kotlin 開發(fā)者社區(qū)
國內(nèi)第一Kotlin 開發(fā)者社區(qū)公眾號,主要分享、交流 Kotlin 編程語言、Spring Boot、Android、React.js/Node.js、函數(shù)式編程、編程思想等相關(guān)主題。