參考鏈接: Java中的協變返回類型
在《JAVA核心思想》這本書里,關于泛型的章節意外的很多,小小的泛型里其實有很多可以學習的內容,我總結下最近看書的成果。?
一. 泛型的好處和應用?
最基礎的用到泛型的地方無非是在容器里 使用泛型用以保證容器內數據的類型統一,所以我們先總結下泛型使用的好處:?
可以統一集合類型容器的存儲類型,防止在運行期出現類型裝換異常,增加編譯時類型的檢查解決重復代碼的編寫,能夠復用算法。可以起到重載的作用?
第二個作用很好理解,泛型 的 ‘泛’ 即代表著 ‘泛化’,不僅僅是保證容器的安全性,更重要的是減少類型的限制,讓我們編寫更加通用的代碼。我們舉個例子:? 在javaweb中,我們經常向前臺返回JSON對象信息,不同的業務可以會組裝不同的bean,為了節省提高代碼的復用性,我們可以這么寫一個類:?
public class ReturnObject<A, B> {
?
? ? public final A a;
? ? public final B b;
?
? ? public ReturnObject(A a, B b) {
? ? ? ? this.a = a;
? ? ? ? this.b = b;
? ? }
?
? ? public <A> A t(A a) {
? ? ? ? return a;
? ? }
?
? ? @Override
? ? public String toString() {
? ? ? ? return "ReturnObject{" +
? ? ? ? ? ? ? ? "a=" + a +
? ? ? ? ? ? ? ? ", b=" + b +
? ? ? ? ? ? ? ? '}';
? ? }
}?
這個類沒有具體的類型,意思也很簡單,就是不限定變量的類型,根據你業務的不同,你可以傳不同的類型進去(通過構造器),更方便的是,java泛型支持繼承,你可以隨意拓展你的業務字段,也就不再需要為了一種業務專門創建一個bean類了。?
// 業務字段拓展
public class ReturnObjectExtender<A, B, C> extends ReturnObject<A, B> {
?
? ? public final C c;
?
? ? public ReturnObjectExtender(A a, B b, C c) {
? ? ? ? super(a, b);
? ? ? ? this.c = c;
? ? }
?
? ? @Override
? ? public String toString() {
? ? ? ? return "ReturnObjectExtender{" +
? ? ? ? ? ? ? ? "c=" + c +
? ? ? ? ? ? ? ? ", a=" + a +
? ? ? ? ? ? ? ? ", b=" + b +
? ? ? ? ? ? ? ? '}';
? ? }
}?
二. 重要!泛型的擦除?
JAVA的泛型都是通過擦除來實現的,這句話的意思是 當你的程序真正跑起來的時候,任何具體的類型其實都已經被擦除了,所以在下面的例子中,輸出的結果是true,aClass和aClass1都是一樣的class生成的對象。?
Class aClass = new ArrayList<Integer>().getClass();
Class aClass1 = new ArrayList<String>().getClass();
?
System.out.println(aClass == aClass1);
?
擦除的負面效應直接體現在如果你寫下面這段代碼,T類型并不能認出你傳給它的是String類型,T直接會被Object替代,?
public class WildCardTest<T> {
?
? ? public void f(T t) {
? ? //這一段編譯報錯
? ? ? ? t.isEmpty();
? ? }
?
? ? public static void main(String[] args) {
? ? ? ? WildCardTest<String> stringWildCardTest = new WildCardTest<>();
? ? ? ? stringWildCardTest.f("");
? ? }
}?
要讓String調用它的isEmpty()方法,需要給泛型一個邊界,代碼只需要重新改一下,T extends String表明T可以是String類或是String的子類,如果傳入的沒有問題,那就可以調用isEmpty()方法。?
public class WildCardTest<T extends String> {
?
? ? public void f(T t) {
? ? ? ? t.isEmpty();
? ? }
?
? ? public static void main(String[] args) {
? ? ? ? WildCardTest<String> stringWildCardTest = new WildCardTest<>();
? ? ? ? stringWildCardTest.f("");
? ? }
}?
擦除是歷史遺留問題?
java的泛型不是從jdk1.0就出現的,為了跟以往沒有泛型代碼的源代碼兼容,例如List被擦除為List,而普通的類型變量在未指定邊界的時候被擦除為Object,從而實現泛型的功能并且向后兼容。?
三. 泛型的通配符(逆變與協變)?
這是兩個賦值,一個是數組,ArrayList因為是Collection的子類,所以數組向上轉型是可以的;另一個是帶泛型的ArrayList,帶ArrayList的泛型并不能賦值給帶Collection的泛型。?
Collection[] collections = new ArrayList[]{};
//泛型會報錯
ArrayList<Collection> collections1 = new ArrayList<ArrayList>();?
逆變,協變與不變?
為什么會導致這樣的差異呢?這里又引出了一個概念– 逆變,協變與不變。逆變與協變用來描述類型轉換后的繼承關系。?
f(?)是逆變(contravariant)的,當A≤B時有f(B)≤f(A)成立;
f(?)是協變(covariant)的,當A≤B時有成立f(A)≤f(B)成立;
f(?)是不變(invariant)的,當A≤B時上述兩個式子均不成立,即f(A)與f(B)相互之間沒有繼承關系。?
根據上面兩個賦值語句做解釋,A 是ArrayList ,B是Collection,所以B > A,這個沒有問題,然后 A[] 數組當作f(A),B[] 數組當作f(B),并且A[]可以賦值給B[]數組,說明 f(B) >=f(A),符合協變原則,所以數組是協變的。? 而泛型也套用這個規則,發現 泛型其實是不變的。?
Java中泛型是不變的,可有時需要實現逆變與協變,怎么辦呢?這時,通配符?派上了用場:?
// <? extends>實現了泛型的協變,它意味著某種Collection或Collection子類的類型,規定了該容器持有類型的上限,它是具體的類型,只是這個類型是什么沒有人關心? 比如:
List<? extends Collection> list = new ArrayList<ArrayList>();
?
// <? super>實現了泛型的逆變,它意味著某種ArrayList或ArrayList父類的類型,規定了該容器持有類型的下限,比如:
List<? super ArrayList> list = new ArrayList<Collection>();??
逆變,協變的應用?
在協變中,還是先以數組做舉例,協變中對持有對象的存入有嚴格限制:?
Collection[] collections = new ArrayList[2];
?
collections[0] = new ArrayList();
//這一步編譯沒問題。但是運行時發現數組里已經定好了只能存ArrayList類型,所以會拋ArrayStoreException
collections[1] = new LinkedList();?
所以在泛型的協變中,例如ArrayList的add方法是不能調用了,在編譯期間直接報錯。?
這是ArrayList 的add,get方法定義,當使用協變時,E e 會被直接替換成 ? extends E?
public boolean add(E e);
public E get(int index);?
具體事例:?
ArrayList<? extends Set> sets = new HashSet<>();
?
//因為 ? extends Set 編譯器不知道sets引用指向什么對象,有可能是 HashSet,可能是TreeSet,這種不確定性導致sets不能使用add方法。
//sets.add(new HashSet());
?
//能插入null值
sets.add(null);
?
//因為這個泛型參數的上限是Set,為了安全性,所以只返回set類型
Set set = sets.get(0);?
在逆變中,因為規定了泛型的下界,所以get set 方法的使用限制又有所不同:?
ArrayList<? super List> list = new ArrayList<Collection>();
?
//對于 add方法,只能放List或List的子類,因為list容器泛型參數都是List的父類 不會出現問題
list.add(new ArrayList());
//不能add HashSet
//list.add(new HashSet());
//不能add Object
//list.add(new Object());
?
//因為這個泛型參數的下限是List? ? 所以無法確定這是個什么類型,只能返回Object
Object object = list.get(0);?
無界通配符?
除了extends和super,還有一種 List<\?>這種通配符,代表著任何事物,但它與List不同的是:?
List<\Object> = List = ‘持有任何Object類型的原生List’List<\?>表示–’具有某種特定類型的非原生List,只是我們不知道那種類型是什么’。?
具體用代碼看出區別:?
ArrayList<Collection> collections2 = new ArrayList<>();
?
ArrayList<?> objects = new ArrayList<>();
//?代表持有某種特定類型 ,所以也可以是Collection,這種賦值時合法的
objects = collections2;
?
//?代表持有某種特定類型,d但是不確定具體哪種,所以只能返回Object
Object o = objects.get(0);
?
//?代表持有某種特定類型,但是什么類型編譯器并不知道,所以為了安全起見,不會讓你用add方法
//objects.add(new Object());
?
?
//可以add null
objects.add(null);
?
總結來說:?
要從泛型類取數據時,用extends;要往泛型類寫數據時,用super;既要取又要寫,就不用通配符(即extends與super都不用)。?
四. 總結?
這篇文章總結的是書里比較重要的知識點,跳過了簡單的應用,寫了那么多,感覺總結還是很有必要的,你第一遍看書也許概念會有些懵懂,但是再記錄總結下,你會解開第一遍看書時有點么棱兩可的知識點。