一、前言
最近依然在看《Java編程思想》這本書,說實話,非常晦澀難懂,除了講的比較深入外,翻譯太爛也是看不懂的一個重要原因。今天在看泛型這一章,也算是有些收獲吧,所以寫篇博客,記錄一下其中比較容易遺忘的一個知識點:在泛型中,extends和super關鍵字的含義和用法。
二、描述
學過Java的人應該都知道,extends和super這兩個關鍵字的最常見的用法:
extends
:讓一個類繼承另外一個類;super
:指向父類對象的引用;
但是在泛型中,這兩個關鍵字都被重載,有了新的含義和用法;
三、解析
1、extends在泛型中的基本使用
我們通過一段代碼進行講解:
// 在泛型中使用extends:<T extends Number>
public class Test <T extends Number> {
public static void main(String[] args) {
// 正確使用:Number、Integer、Double、Byte 均屬於 Number
Test<Number> t = new Test<>();
Test<Integer> t1 = new Test<>();
Test<Double> t2 = new Test<>();
Test<Byte> t3 = new Test<>();
// 錯誤使用:String不屬於Number
// Test<String> t4 = new Test<>();
}
}
看上面的代碼,我們聲明了一個類Test,它有一個泛型T,T的聲明為<T extends Number>
,這表示什么意思呢?這表明:類Test的泛型,只能是Number類型,或者Number類型的派生類型(子類型);所以,在下面的main方法中,我們將Test類對象的泛型定為Number
、Integer
、Double
、Byte
均沒有問題,因為它們是Number本身,或者Number的子類型;但是我們將泛型定義為String類型,就會編譯錯誤,因為String不是Number類型的派生類。所以,泛型中extends關鍵字的作用就是:限定泛型的上邊界;
2、Java泛型中的通配符
上面講解了泛型中,extends關鍵字最基本的用法,比較簡單,但是在實際的使用中,還有一種更加復雜的用法,就是搭配泛型中的通配符 ?
使用,所以我先來簡單的介紹一下泛型中的通配符—— ?;
在泛型中的通配符就是一個問號,標准叫法是無界通配符,它一般使用在參數或變量的聲明上:
// 在參數中使用無界通配符
public static void test(List<?> list) {
Object o = list.get(1);
}
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<Integer>();
// 在變量聲明中使用無界通配符
List<?> list2 = list1;
test(list1);
test(list2);
}
泛型中使用無界通配符,表示泛型可以是任意具體的類型,沒有限制(基本數據類型除外,基本數據類型不能用作泛型,可以使用基本數據類型的包裝類);所以無界通配符給人的感覺就和原生的類型沒什么區別,比如就上面這段代碼,使用List<?>,和直接使用List,好像是一樣的;但是實際上還是有一些區別的,比如看下面這段代碼:
// 在參數中使用無界通配符
public static void test1(List<?> list) {
// 均編譯錯誤,因為使用了無界通配符,編譯器無法確定具體是什么類型
// list.add(1111);
// list.add("aaa");
// list.add(new Object());
}
// 在參數中使用原生List
public static void test2(List list) {
// 編譯通過,不加泛型時,編譯器默認為Object類型
list.add(1111);
list.add("aaa");
list.add(new Object());
}
public static void main(String[] args) {
// 聲明兩個泛型明確的list集合
List<Integer> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
// 調用使用了<?>的方法
test(list1);
test(list2);
}
上面這段代碼演示了使用通配符和原生類型的區別。在方法test1
中,使用了泛型類型為通配符的List,此時,我們將無法使用List的add方法,為什么?我們先看一下add方法的聲明:
boolean add(E e);
我們可以看到,add方法的參數類型是一個泛型,可是在test1
中,我們在泛型中使用了通配符,這意味着list的泛型可以是任意類型,編譯器並不知道它具體是哪種類型,所以不允許我們調用list中的泛型方法。這時候大家可能有點疑問,為什么Object類型也不行呢,Object類型不是所有類型的父類嗎。那是因為Java對於原生類型和通配符有不一樣的定義,而在語法的設計上要符合這種定義;
在《Java編程思想》上描述了使用通配符泛型和原生類型在定義上的區別:
- List:表示可以存儲任意Object類型的集合;
- List<?>:表示一個存儲某種特定類型的List集合,但是不知道這種特定類型是什么;
從上面對兩種定義的描述,我們可以大致了解通配符與原生類型的區別;當然,具體其實還要更加復雜,但是我現在着重講的是泛型中的extends
和super
關鍵字,所有這里就不贅述了,上面的講解主要是為了引出下面的內容。下面開始講解這兩個關鍵字如何搭配通配符使用。
3、extends關鍵字搭配?使用
上面講解了extends的一個簡單用法,現在來講解一個更加復雜的用法,就是extends關鍵字搭配無界通配符?使用。首先還是一樣,來看一段代碼:
public static void test1(List<? extends Number> list) {
Number number = list.get(1);
// 下列均編譯錯誤:list中元素的類型可以是任意Number的子類,所有無法確定list存儲的具體是哪一種類型
// list.add(11);
// list.add(new Integer(1));
// list.add(new Double(1));
}
public static void test2(List list) {
Object object = list.get(1);
// 編譯通過:原生list可以存儲任意Object類型
list.add(11);
list.add(new Integer(1));
list.add(new Double(1));
}
public static void main(String[] args) {
// 注意下列List的泛型
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
List<Double> list3 = new ArrayList<>();
List<Number> list4 = new ArrayList<>();
// 調用使用了泛型的方法
test1(list1); // 編譯錯誤,因為list1的泛型為String,不是Number的子類
test1(list2); // 編譯通過,因為list2的泛型為Integer,是Number的子類
test1(list3); // 編譯通過,因為list3的泛型為Double,是Number的子類
test1(list4); // 編譯通過,因為list4的泛型為Number,是Number的本身
}
我們通過上面這段代碼進行講解。上面我們定義了一個方法,名字叫test1
,它接收一個參數List<? extends Number> list
,這表示參數是一個List類型,而且這個List類型的泛型不確定,但是只能是Number類型,或者Number類型的子類,所以我們在main方法中創建了四個List對象,泛型分別是Number
、Integer
、Double
、String
,只有String類型的list作為參數調用test1時,才編譯錯誤,因為String不是Number類型的子類;所以,此處extends的作用是:限定了參數或變量中,泛型的上界;
這種寫法有什么好處呢?好處就是:我們確定了泛型的上界,縮小了類型的范圍,例如test1中,我們取出List集合中的元素,返回值是一個Number
,而不是像test2方法中,返回值是Object類型。這是因為我們使用extends
,限制了泛型的類型是Number或其子類,於是編譯器就可以知道,這個list中的所有元素,一定屬於Number,所有可以用Number接收,也可以調用Number類的方法;但是在test2方法中,沒有限定類型上界,所有只能用Object接收;
那這么寫有什么問題呢?也很明顯,就是我們在講通配符時說到的問題:無法調用參數為泛型的方法。我們使用了通配符,同時繼承了Number類,根據我們之前說過的定義,List<? extends Number> list
表示一個存儲特定類型的List集合,且這個類型是Number或者Number的子類,這就是Java給這種參數的定義。所以編譯器只能知道,這個list中,元素的大致類型,但是它具體是哪種類型,編譯器不知道,所以編譯器不允許我們調用任何需要用到這個具體類型信息的方法;比如我們傳入一個元素為Byte類型的List,然后再調用add方法,為集合加入一個int值,這顯然是不合理的,雖然它們都是Number的子類。所以,使用這種參數類型,有時候也可以幫助我們限定某些不應該進行的操作。
4、super關鍵字搭配?使用
super關鍵字和extends關鍵字的含義相反,super關鍵字的作用是:限定了泛型的下界;還是先看一段代碼:
public static void test1(List<? super Integer> list) {
// 只能通過Object接收
Object object = list.get(1);
// 編譯正確,允許以下操作
list.add(111);
list.add(new Integer(1));
// 編譯錯誤,1.5不是Integer
list.add(1.5);
}
public static void main(String[] args) {
// 創建三個用於測試的List集合,泛型不同
List<Integer> list1 = new ArrayList<>();
List<Number> list2 = new ArrayList<>();
List<Double> list3 = new ArrayList<>();
// 調用使用了泛型的方法
test1(list1); // 編譯正確,因為list1的泛型為Integer,等價於參數中泛型的下界
test1(list2); // 編譯正確,因為list2的泛型為Number,是Integer的基類
test1(list3); // 編譯錯誤,因為list1的泛型為Double,不是Integer的基類
}
上面的代碼中定義了一個方法test1
來測試泛型中的super關鍵字,這個方法的參數類型是List<? super Integer> list
,這表示這個方法的參數是一個List集合,而集合的泛型只能是Integer,或者Integer的基類。我們在main方法中定義了三個集合驗證這個結論,這三個集合的泛型各不相同,分別使用這三個集合作為參數,調用test1方法。結果,泛型為Integer
,以及Number
的集合,調用方法成功,而泛型為Double
的list編譯錯誤。
我們看test1中的代碼可以發現,與泛型中使用extends
關鍵字不同,使用super關鍵字可以調用add這個參數為泛型的方法,這是為什么呢?因為我們在泛型中使用了super這個,限定了泛型的下界為Integer,這表示在list這個集合中,所有的元素一定是Integer類型,或者Integer類型的基類型,比如說Number;這表明,我們在集合中添加一個Integer類型的元素,一定是合法的,因為Integer類型的對象,肯定也是一個Integer的基類型的對象(多態);當然,如果Integer還有子類,那也可以在add中傳入Integer的子類對象(雖然Integer沒有子類);
但是我們從這個list中取出元素,只能用Object接收,這是為什么呢?因為我們定義了list的泛型下界是Integer,表明list的具體泛型可以是Integer的任何基類,而一個類的基類不止一個,比如Integer繼承Number,而Number又繼承Object。在這種情況下,編譯器並不知道泛型具體是哪一種類型,所以只能用最高類Object進行接收。
四、總結
上面的內容大致的講解了一下泛型中extends和super關鍵字的用法,讓人可以有一個簡單的認識,但是更多的是我個人的理解。通過看書,我覺得泛型是一個很復雜的東西,僅僅只是看還是不夠的,還是需要多多實踐,在實踐中才能加深理解。
參考
《Java編程思想》