部落格地址:sguotao.top/Kotlin-2018…
一個生產環境問題引發的思考。
在JDK1.5之前,生產環境中總是會出現這樣類似的問題:
List list = new ArrayList ();
list.add ("foo");
list.add (new Integer (42)); // added by "mistake"
for (Iterator i = list.iterator (); i.hasNext (); ) {
String s = (String) i.next ();
// ClassCastException for Integer -> String
// work on `s'
System.out.println (s);
}
複製程式碼
由於add()接受Object型別,可能在開發除錯階段測試資料使用的都是String型別,直到上線到生產環境中,某次傳入了一個Integer型別資料,於是系統崩潰了……
為了解決此類的資料型別安全問題,Java在JDK1.5中提出了泛型,於是問題提早的在開發階段暴露出來:
// constructed generic type
List<String> list = new ArrayList<String> ();
list.add ("foo");
list.add (42); // error: cannot find symbol: method add(int)
for (String s : list)
System.out.println (s);
複製程式碼
除了解決資料型別安全問題,泛型的引入也更多的使用到設計模式當中。泛型的本質就是讓型別也變成引數。比如定義函式時宣告形參,在呼叫函式時傳入實參。型別的引數化也同樣,定義函式或類時宣告成泛型(泛型形參),在呼叫或例項化時傳入具體的型別(泛型實參)。
Java中的泛型
Java中泛型可以用在類、介面和方法中,分別稱為泛型類、泛型介面和泛型方法。下面分別來看一下。
泛型類
泛型應用在類的宣告中,稱為泛型類,其格式如下:
[訪問許可權] class 類名 <泛型,泛型……>{
……
}
複製程式碼
比如定義如下泛型類:
class CustomGenerics<V> { //泛型形參,常見的泛型形參標識如T、E、K、V等
private V value; //成員變數的型別為V,V是在例項化是外部傳入的。
CustomGenerics(V value) {
this.value = value;
}
public V getValue() {
return value;
}
}
複製程式碼
型別建立的格式如下:
類名<具體型別> 物件名稱 = new 類名<具體型別>()
複製程式碼
比如例項化上面定義的泛型類。
public class TestGenerics {
//在例項化泛型類時,指定泛型實參
CustomGenerics<String> cStr = new CustomGenerics<String>("sguotao");
CustomGenerics<Integer> cInt = new CustomGenerics<Integer>(9456);
}
複製程式碼
總結一下泛型類中的一些注意事項:
- 在例項化泛型類時,要指定泛型實參。(即需要指定具體型別)
- 指定的泛型實參型別只能是類型別,不能是基本資料型別。(不能是int,long,可以是Integer,Long等)
泛型介面
宣告泛型介面的格式與宣告泛型類相似:
interface 介面名<泛型,泛型……>{
……
}
複製程式碼
比如定義如下泛型介面。
interface CustomGenericsInterface<T> {
public T generate();
}
複製程式碼
在實現泛型介面的類中,指定泛型實參。
class CustomImpl implements CustomGenericsInterface<String> {
public String generate() {
return "hello";
}
}
複製程式碼
泛型方法
宣告泛型方法的格式:
[訪問許可權] <泛型> 返回值型別 方法名( 泛型 引數名)
複製程式碼
比如上面的泛型類中定義如下的泛型方法:
class CustomGenerics<V> { //泛型形參,常見的泛型形參標識如T、E、K、V等
private V value; //成員變數的型別為V,V是在例項化是外部傳入的。
CustomGenerics(V value) {
this.value = value;
}
public V getValue() {
return value;
}
//泛型方法
public <T> V genericMethod(T t1, V v1) { //泛型方法需要在返回值型別前有<泛型>的標記
//泛型方法中可以使用泛型方法宣告的泛型,也可以使用泛型類宣告的泛型
return value;
}
//泛型方法
public <V> void genericMethod(V v1) {//泛型方法中宣告的泛型引數V與泛型類中宣告的泛型T不是同一個
}
//泛型方法
public static <T> void genericStaticMethod(T t1) {//靜態的泛型方法無法使用泛型類中宣告的泛型
}
}
複製程式碼
總結一下泛型方法中的一些注意事項:
- 判斷一個方法是否為泛型方法最直接的方式,看方法返回值前是否有<泛型>的標記;
- 在泛型類中宣告的泛型方法,即可以使用泛型類中宣告的泛型,也可以使用泛型方法中宣告的泛型;比如上面示例中的泛型方法genericMethod(T t1, V v1) 。
- 如果泛型方法中宣告的泛型引數與泛型類中的泛型引數相同,那麼可以認為泛型方法中的泛型引數覆蓋了泛型類中的泛型引數,比如上面示例中的genericMethod(V v1)。
- 還有一點需要指出,泛型類中的使用了泛型引數,但是在返回值前沒有<泛型>標記的方法,不是泛型方法,比如上面示例中的getValue(),該方法只是使用了泛型引數,並不是泛型方法。
- 如果泛型方法是靜態方法,那麼此時泛型方法是無法使用泛型類中宣告的泛型,比如上面示例中的泛型方法genericStaticMethod(T t1),該方法只能使用泛型方法中的泛型T,無法使用泛型類中的泛型V。
泛型擦除
Java中的泛型是偽泛型,要了解偽泛型,先來了解什麼是真泛型?在C#中使用的泛型,就是真泛型。如在C#中定義泛型:
//泛型類
public class GenericClass<T>
{
T _t;
public GenericClass(T t)
{
_t = t;
}
public override string ToString()
{
return _t.ToString();
}
}
public class Program
{
static void Main(string[] args)
{
GenericClass<int> gInt = new GenericClass<int>(123456);
Console.WriteLine(gInt.GetType());
Console.WriteLine(gInt.ToString());
GenericClass<string> gStr = new GenericClass<string>("Test");
Console.WriteLine(gStr.GetType());
Console.WriteLine(gStr.ToString());
Console.Read();
}
}
複製程式碼
檢視輸出結果:
檢視IL發現: 而Java中的泛型只存在於編譯期,在生成的位元組碼檔案中是不包含任何泛型資訊的。比如下面的兩個方法,在位元組碼中具有相同的函式簽名。 使用泛型的時候加上的型別引數,會在編譯器在編譯的時候去掉,這個過程就稱為型別擦除。在C#裡面泛型無論在程式原始碼中、編譯後的IL中或是執行期的CLR中都是切實存在的,List與List就是兩個不同的型別,它們在系統執行期生成,有自己的虛方法表和型別資料。
在Java語言中的泛型則不一樣,它只在程式原始碼中存在,在編譯後的位元組碼檔案中,就已經被替換為原來的原始型別(Raw Type)了,並且在相應的地方插入了強制轉型程式碼,因此對於執行期的Java語言來說,ArrayList與ArrayList就是同一個型別。
萬用字元?及型變
是否有這樣的疑問,為什麼Number的物件可以由Integer例項化,而ArrayList的物件卻不能由ArrayList例項化?先來看下面的程式碼:
Number num = new Integer(1);
ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch
複製程式碼
Integer是Number的子類,所以Number物件可以由Integer例項化,這是Java多型的特性,那麼Integer是Number的子類,List是不是List 的父類呢?答案是否定的。List和List沒有繼承關係,看似需要宣告多個方法來接收這些不同的泛型實參了,這顯然與Java的多型性是相背離的。
這時,就需要一個在邏輯上可以用來表示同時是List和List 的父類的一個引用型別,由此,型別萬用字元應運而生。Java中的萬用字元用?來表示,萬用字元?可以認為是任意型別的父類,它是一個具體的型別,是泛型實參,這裡需要注意與泛型形參T、V的區別。
萬用字元的引入不只是解決了泛型實參之間的邏輯關係,更重要的一點,對泛型引入了邊界的概念。
萬用字元?的上界
萬用字元的上界使用<? extends T>的格式,表示類或者方法接收T或者T的子型別,比如:
List<? extends Number> list = new ArrayList<Number>();
複製程式碼
萬用字元?的上界,又可以稱為協變。
萬用字元?的下界
萬用字元的下界使用<? super T>的格式,表示類或者方法接收T或者T的父型別,比如:
List<? super Integer> list = new ArrayList<Number>();
複製程式碼
萬用字元?的下界,又稱為逆變。關於逆變和協變,下面詳細介紹:
協變和逆變
Java中的泛型是既不支援協變,也不支援逆變。那什麼是逆變,什麼是協變?簡單的說,協變就是定義了型別的上邊界,而逆變則定義了型別的下邊界。看一個協變的例子:
ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch
List<? extends Number> list = new ArrayList<Number>();
複製程式碼
? extends Number的含義是:接收Number的子類,也包括Number,作為泛型實參。再來看逆變。
在Java中是不能將父類的例項賦值給子類的變數,但是在泛型中可以通過萬用字元?來模擬逆變,比如:
List<? super Integer> list = new ArrayList<Number>();
複製程式碼
? super Integer的含義是:接收Integer的基類,也包括Integer本身作為泛型實參。
協變與逆變的數學定義:
逆變與協變用來描述型別轉換(type transformation)後的繼承關係,其定義:如果A、B表示型別,f(⋅)表示型別轉換,≤表示繼承關係(比如,A≤B表示A是由B派生出來的子類);
- f(⋅)是逆變(contravariant)的,當A≤B時有f(B)≤f(A)成立;
- f(⋅)是協變(covariant)的,當A≤B時有f(A)≤f(B)成立;
- f(⋅)是不變(invariant)的,當A≤B時上述兩個式子均不成立,即f(A)與f(B)相互之間沒有繼承關係。
什麼時候使用協變和逆變
什麼時候使用協變?extends,什麼時候使用逆變 ?super,在《Effective Java》中給出了一個PECS原則:
PECS:Producer extends,Customer super
當使用泛型類作為生產者,需要從泛型類中取資料時,使用extends,此時泛型類是協變的; 當使用泛型類作為消費者,需要往泛型類中寫資料時,使用suepr,此時泛型類是逆變的。 一個經典的案例就是Collections中的copy方法。
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();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
複製程式碼
copy方法實現從源src到目的dest的複製,源src可以看作是生產者,使用協變,目的dest可以看作是消費者,使用逆變。
Kotlin中的泛型
前面用了大量的篇幅來介紹Java中的泛型,其實瞭解了Java中的泛型,就會使用Kotlin中的泛型,區別僅僅是寫法和關鍵字上的區別。
Kotlin中的泛型方法,比如:
class GenericKotlin<T>(var value: T) {//宣告泛型類
//宣告泛型方法
public fun <V> genericMethod(t: T, v: V): Unit {
}
}
//宣告泛型介面
interface GenericKotlinInterface<T> {
public fun generate(): T
}
複製程式碼
Kotlin中的型變
先來看一下Kotlin中的型變:
fun main(args: Array<String>) {
//協變
val list: List<Number> = listOf(1, 2, 3, 4)
//逆變
val comparable: Comparable<Int> = object : Comparable<Any> {
override fun compareTo(other: Any): Int {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}
複製程式碼
我們看一下List的宣告原始碼:
public interface List<out E> : Collection<E> {
……
public operator fun get(index: Int): E
// Search Operations
/**
* Returns the index of the first occurrence of the specified element in the list, or -1 if the specified
* element is not contained in the list.
*/
public fun indexOf(element: @UnsafeVariance E): Int
複製程式碼
Kotlin中的協變不再是? extends,而是使用out關鍵字,語義更加貼切,生產者生產產品使用out。泛型既可以作為函式的引數,也可以作為函式的返回值。當泛型作為函式的返回值時,稱為協變點,當泛型作為函式引數時,稱為逆變點。
再來看List的原始碼,這裡的List是隻讀的List,使用out關鍵字修飾泛型,這裡將泛型E作為協變來使用,也就是當做函式的返回值。但是原始碼中也將E作為函式的引數使用,即當做逆變來使用,由於函式(比如indexOf)並不會修改List,所以加註解@UnsafeVariance來修飾。
再來看Comparable的原始碼:
public interface Comparable<in T> {
/**
* Compares this object with the specified object for order. Returns zero if this object is equal
* to the specified [other] object, a negative number if it's less than [other], or a positive number
* if it's greater than [other].
*/
public operator fun compareTo(other: T): Int
}
複製程式碼
Kotlin中的逆變也不再是? super,而是使用關鍵字in,消費者消費產品使用in。Comparable中的泛型被宣告為逆變,也就說Comparable中泛型T被當做函式的引數。
最後總結一下,泛型既可以作為函式的返回值,也可以作為函式的引數。當作為函式的返回值時,泛型是協變的,使用out修飾;當作為函式的引數時,泛型是逆變的,使用in修飾。
- 在泛型形參前面加上out關鍵字,表示泛型的協變,作為返回值,為只讀型別,泛型引數的繼承關係與類的繼承關係保持一致,比如List和List;
- 在泛型引數前面加上in表示逆變,表示泛型的逆變,作為函式的引數,為只寫型別,泛型引數的繼承關係與類的繼承關係相反,比如Comparable和Comparable。
星投影
Kotlin中的星投影,用符號來表示,作用類似於Java中的萬用字元?,比如當不確認泛型型別時,可以使用來代替。需要注意的時,*只能出現在泛型形參的位置,不能作為在泛型實參。
//星投影
val list: MutableList<*> = ArrayList<Number>()
複製程式碼