泛型(generics)的概念是Java SE5的重大變化之一。泛型實現了參數化型別(parameterized types)的概念,使代碼可以應用於多種類型。“泛型”這個術語的意思是:“適用於許多許多的類型”。
1 泛型方法
泛型方法與其所在的類是否是泛型沒在關係,即泛型方法所在的類以是泛型類也可以不是泛型類。
泛型方法使得該方法能夠獨立於類而產生變化 。
一個基本指導原則:無論何時,只要你能做到,你就應該盡量使用泛型方法。也就是說如果使用泛型方法可以取代將整個類泛型化,那麼就應該只使用泛型方法,因為它可以使事情更清楚明白。
對於一個static
方法而言,無法訪問泛型類的型別參數,所以,如果static
方法需要使用泛型能力,就必須使其成為泛型方法。
要定義泛型方法,只需將泛型參數列表置於傳回值之前。
1.1 型別參數推斷
使用泛型方法的時候,通常不必指明參數類型,因為編譯器會為我們找出具體的類型。這稱為型別參數推斷(type argument inference)。
1.1.1 顯式的類型說明
在點操作符與方法名之間插入角括弧,然後把類型置於角括弧內,即顯式的類型說明。
2 擦除的神秘之處
根據JDK文檔的描述,Class.getTypeParameters()
將“返回一個TypeVariable
對象數組,表示有泛型聲明的型別參數…..”,這好像是在暗示你可能發現參數類型的資訊,但是,正如你從輸出中看到,你能夠發現的只是用作參數預留位置的標識符,這並非有用的資訊。
因此,殘酷的現實是:在泛型代碼內部,無法獲得任何有關泛型參數類型的資訊。
因此,你可以知道諸如泛型參數標識符和泛型型別邊界這類資訊——你卻無法知道建立某個特定執行個體的實際的型別參數。……,在使用Java泛型工作時它是必須處理的最基本的問題。
Java泛型是使用擦除來實現的,這意味著當你在使用泛型時,任何具體的類型資訊都被擦除了,你唯一知道的就是你在使用一個對象。因此 List<String>
和 List<Integer>
在運行時事實上是相同的類型。這兩種形式都被擦除成它們的“原生類型,即 List
。
2.1 C++的方式
2.1.1 以下C++模板樣本:
它怎麼知道f()
方法是為型別參數T而存在的呢?當你執行個體化這個模板時,C++編譯器將進行檢查,因此在Manipulator<HasF>
被執行個體化的這一刻,它看到HasF
擁有一個方法f()
。如果情況並非如此,就會得到一個編譯期錯誤,這樣型別安全就得到了保障。
// Templates.cpp#include <iostream>using namespace std;template<class T> class Manipulator{ T obj;public: Manipulator(T x) { obj = x; } void manipulate() { obj.f(); }};class HasF{public: void f() { cout << "HasF::f()" << endl; }};int main(){ HasF hf; Manipulator<HasF> manipulator(hf); manipulator.manipulate();}
2.1.2 翻譯成Java,將不能編譯。
由於有了擦除,Java編譯器無法將manipulate()必須能夠在obj上調用f()這一需求映射到HasF擁有f()這一事實上。
2.2 泛型邊界
為了調用f(),我們必須協助泛型類,給定泛型類的邊界,以此告知編譯器只能接受遵循這個邊界的類型。由於有了邊界,下面的代碼就可以編譯了。
package net.mrliuli.generics.erase;/** * Created by li.liu on 2017/12/7. *//** * 由於有了擦除,Java編譯器無法將manipulate()必須能夠在obj上調用f()這一需求映射到HasF擁有f()這一事實上。 * @param <T> */class Manipulator<T>{ private T obj; public Manipulator(T x){ obj = x; } // Error: Cannot resolve method 'f()' //public void manipulate(){ obj.f(); }}/** * 為了調用f(),我們必須協助泛型類,給定泛型類的邊界,以此告知編譯器只能接受遵循這個邊界的類型。由於有了邊界,下面的代碼就可以編譯了。 * @param <T> */class Manipulator2<T extends HasF>{ private T obj; public Manipulator2(T x){ obj = x; } public void manipulate(){ obj.f(); }}public class Manipulation { public static void main(String[] args){ HasF hf = new HasF(); Manipulator<HasF> manipulator = new Manipulator<>(hf); //manipulator.manipulate(); Manipulator2<HasF> manipulator2 = new Manipulator2<>(hf); manipulator2.manipulate(); }}
2.3 擦除
我們說泛型型別參數將擦除到它的第一個邊界(它可能會有多個邊界),我們還提到了型別參數的擦除。編譯器實際上會把型別參數替換為它的擦除,就像上面的樣本一樣。T
擦除到了HasF
,就好像在類的聲明中用 HasF
替換了 T
一樣。
2.4 擦除的問題
擦除的核心動機是它使得泛化的用戶端可以用非泛化的類庫來使用,反之亦然,這經常被稱為遷移相容性。
因此,擦除主要的正當理由是從非泛化的代碼到泛化的代碼的轉變過程,以及在不破壞現有類庫的情況下,將泛型融入Java語言。
擦除的代碼是顯著的。
如果編寫了下面這樣的代碼:
class Foo<T>{ T var; }
那麼看起來當你在建立Foo
的執行個體時:
Foo<Cat> f = new Foo<Cat>();
class GenericBase<T>{}class Derived1<T> extends GenericBase<T>{}class Derived2 extends GenericBase{} // No warning
2.5 邊界處的動作
即使擦除在方法或類內部移除了有關實際類型的資訊,編譯器仍舊可以確保在方法或類中使用的類型的內部一致性。
因為擦除在方法體中移除了類型資訊,所以在運行時的問題就是邊界:即對象進入和離開方法的地點。這些正是編譯器在編譯期執行類型檢查並插入轉型代碼的地點。
在泛型中的所有動作都發生在邊界處——對傳遞進來的值進行額外的編譯期檢查,並插入對傳遞出去的值的轉型。這有助于澄清對擦除的混淆,記住,“邊界就是發生動作的地方。”
3 擦除的補償(Compensating for erasure)
有時必須通過引入類型標籤(type tag)來對擦除進行補償(compensating)。這意味著你需要顯示地傳遞你的類型的Class對象,以便你可以在類型運算式中使用它。
4 邊界(bound)
邊界使得你可以在用於泛型的參數類型上設定限制條件。儘管這使得你可以強制規定泛型可以應用的類型,但是其潛在的一個更重要的效果是你可以按照自己的邊界類型來調用方法。
因為擦除移除了類型資訊,所以,可以用無界泛型參數調用的方法只是那些可以用Object調用的方法。
但是,如果能夠將這個參數限制為某個類型子集,那麼你就可以用這些類型子集來調用方法。
萬用字元被限制為單一邊界。
5 萬用字元(wildcards)
// Compile Error: incompatible types:List<Fruit> list = new ArrayList<Apple>();
與數組不同,泛型沒有內建的協變類型。即*協變性對泛型不起作用。
package net.mrliuli.generics.wildcards;import java.util.*;/** * Created by leon on 2017/12/8. */public class GenericsAndCovariance { public static void main(String[] args){ // Compile Error: incompatible types: //List<Fruit> list = new ArrayList<Apple>(); // Wildcards allow covariance: List<? extends Fruit> flists = new ArrayList<Apple>(); // But, 編譯器並不知道flists持有什麼類型對象。實際上上面語句使得向上轉型,丟失掉了向List中傳遞任何對象的能力,甚至是傳遞Object也不行。 //flists.add(new Apple()); //flists.add(new Fruit()); //flists.add(new Object()); flists.add(null); // legal but uninteresting // We know that it returns at least Fruit: Fruit f = flists.get(0); }}
5.1 編譯器有多聰明
對於 List<? extends Fruit>
,set()
方法不能工作於 Apple
和 Fruit
,因為 set() 的參數也是 ? extends Furit
,這意味著它可以是任何事物,而編譯器無法驗證“任何事物”的型別安全。
但是,equals()
方法工作良好,因為它將接受Object類型而並非T類型的參數。因此,編譯器只關注傳遞進來和要返回的物件類型,它並不會分析代碼,以查看是否執行了任何實際的寫入和讀取操作。
5.2 逆變(Contravariance)
package net.mrliuli.generics.wildcards;import java.util.*;public class SuperTypeWildcards { /** * 超類型萬用字元使得可以向泛型容器寫入。超類型邊界放鬆了在可以向方法傳遞的參數上所作的限制。 * @param apples 參數apples是Apple的某種基底類型的List,這樣你就知道向其中添加Apple或Apple的子類型是安全的。 */ static void writeTo(List<? super Apple> apples){ apples.add(new Apple()); apples.add(new Jonathan()); //apples.add(new Fruit()); // Error }}
package net.mrliuli.generics.wildcards;import java.util.*;/** * Created by li.liu on 2017/12/8. */public class GenericWriting { static <T> void writeExact(List<T> list, T item){ list.add(item); } static List<Apple> appleList = new ArrayList<Apple>(); static List<Fruit> fruitList = new ArrayList<Fruit>(); static void f1(){ writeExact(appleList, new Apple()); writeExact(fruitList, new Apple()); } static <T> void writeWithWildcard(List<? super T> list, T item){ list.add(item); } static void f2(){ writeWithWildcard(appleList, new Apple()); writeWithWildcard(fruitList, new Apple()); } public static void main(String[] args){ f1(); f2(); }}
5.3 無界萬用字元(Unbounded wildcards)
原生泛型Holder
與Holder<?>
原生Holder
將持有任何類型的組合,而Holder<?>
將持有具有某種具體類型的同構集合,因此不能只是向其中傳遞Object。
5.4 捕獲轉換
以下樣本,被稱為捕獲轉換,因為未指定的萬用字元類型被捕獲,並被轉換為確切類型。參數類型在調用f2()
的過程中被捕獲,因此它可以在對f1()
的調用中被使用。
package net.mrliuli.generics.wildcards;/** * Created by leon on 2017/12/9. */public class CaptureConversion { static <T> void f1(Holder<T> holder){ T t = holder.get(); System.out.println(t.getClass().getSimpleName()); } static void f2(Holder<?> holder){ f1(holder); // Call with captured type } public static void main(String[] args){ Holder raw = new Holder<Integer>(1); f1(raw); f2(raw); Holder rawBasic = new Holder(); rawBasic.set(new Object()); f2(rawBasic); Holder<?> wildcarded = new Holder<Double>(1.0); f2(wildcarded); }}
6 問題
7 總結
我相信被稱為泛型的通用語言特性(並非必須是其在Java中的特定實現)的目的在於可表達性,而不僅僅是為了建立型別安全的容器。型別安全的容器是能夠建立更通用代碼這一能力所帶來的副作用。
泛型正如其名稱所暗示的:它是一種方法,通過它可以編寫出更“泛化”的代碼,這些代碼對於它們能夠作用的類型有更少的限制,因此單個的程式碼片段能夠應用到更多的類型上。
相關文章:
Java編程思想學習課時(一):第1~13、16章
Java編程思想學習課時(二)第14章-類型資訊