博客地址:sguotao.top/Kotlin-2018…html
一個生產環境問題引起的思考。java
在JDK1.5以前,生產環境中老是會出現這樣相似的問題:c#
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);
複製代碼
除了解決數據類型安全問題,泛型的引入也更多的使用到設計模式當中。泛型的本質就是讓類型也變成參數。好比定義函數時聲明形參,在調用函數時傳入實參。類型的參數化也一樣,定義函數或類時聲明成泛型(泛型形參),在調用或實例化時傳入具體的類型(泛型實參)。less
Java中泛型能夠用在類、接口和方法中,分別稱爲泛型類、泛型接口和泛型方法。下面分別來看一下。dom
泛型應用在類的聲明中,稱爲泛型類,其格式以下:ide
[訪問權限] 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);
}
複製代碼
總結一下泛型類中的一些注意事項:
聲明泛型接口的格式與聲明泛型類類似:
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) {//靜態的泛型方法沒法使用泛型類中聲明的泛型
}
}
複製代碼
總結一下泛型方法中的一些注意事項:
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能夠看做是消費者,使用逆變。
前面用了大量的篇幅來介紹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中的型變:
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修飾。
Kotlin中的星投影,用符號來表示,做用相似於Java中的通配符?,好比當不確認泛型類型時,可使用來代替。須要注意的時,*只能出如今泛型形參的位置,不能做爲在泛型實參。
//星投影
val list: MutableList<*> = ArrayList<Number>()
複製代碼