摘要:Java泛型其本質是引數化型別,也就是說所操作的資料型別被指定為一個引數(type parameter)這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法。
Java泛型是J2 SE1.5中引入的一個新特性,其本質是引數化型別,也就是說所操作的資料型別被指定為一個引數(type parameter)這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法。
泛型方法
一般定義如下,即方法的前面加了個<T>
public class FTest { public <T> List<T> f(T t){...}; }
三種泛型引數推斷方式:
1、直接在f()前面加確定泛型
fTest.<Integer>f(xxx)
2、通過輸入引數確定, 下面這個推斷為Integer
int number = 0; fTest.f(number)
3、可通過 返回值 確定
List<Integer> list = fTest.f(xxx);
Q: 下面這段程式碼哪裡有問題? 是toString()那裡嗎?
public class A<T> { public static void test(T t){ System.out.println(t.toString()); } }
A:test是static方法, 因此無法感知A<T>例項裡的T
需要改成
public static <T> void test(T t)
toString()那裡沒問題,toString就是Object的方法。
泛型引數和型別消除
Q: 泛型引數T在執行時,會變成什麼?
A: 統一變成Object且不包含任何型別資訊。
Q: 泛型引數T可以可以使用instanceof做比較嗎?
class A<T> { void f(Object arg) if(arg instanceof T) { ... } }
A: 不能,編譯器會報錯。
Q: 泛型引數T可以進行new T()或者new T[]操作嗎?
A: 不能,編譯器會報錯。
Q: 能呼叫泛型引數物件裡的方法嗎?
T.f();
A: 只能呼叫Object的方法。
Q: 可以用T做強制轉化嗎?
T t = (T)object;
A: 能執行, 但不會真正發生轉型, 編譯時會觸發waring警告。
新建泛型物件時的問題
先假定有2個類, 基類Parent 和子類Child
class Parent{} class Child extends Parent{}
回答以下問題:
Q:下面這句話有問題嗎?
List<Parent> list = new ArrayList<Child>()
A:有問題,編譯就錯誤了。 List<Parent>和ArrayList<Child>並不存在父子類的關係
Q:
List<? extends Parent> list = new ArrayList<Child>();
這個list有什麼特點?
A:這個list可以呼叫A a = list.get(), 但是不能list.add(new Parent())
- 原因:
list.get()所做的操作是在返回時, 把內部的<? extend Parent> 強轉成Parent, 是合理的,任何Parent的子類都可以轉成Parent
list.add(new Parent())所做的操作是在輸入時, 把外部的A轉成內部的<? extend Parent>, 這是不合理的,因為我們不知道這個Parent物件可以轉成哪個Parent的子類。
Q:
List<? super Child> list = new ArrayList<Parent>();
這個list有什麼特點?
下面誰會報錯
list.add(new Child()) list.add(new Parent()) Parent a= list.get(); Child b = list.get()
A:截圖如下:
- Child c = list.get() 或者Parent p = list.get()所做的操作是在返回時, 把內部的<? super Child> 強轉成外部的Parent或者child, 是不合理的, 因為編譯器覺得child的父類 不一定 能轉成parent或者child,所以禁止了這種行為( 比如parent的父類是object, 但object不一定就能轉成parent或者child)。*list.add(new Child())所做的操作是在輸入時, 把外部的child或者parent轉成內部的<? super Child>, 這是合理的,因為child和parent一定能轉成child的父類。
Q:
List<?> list = new ArrayList<A>();
這個list有什麼特點?
A:get和add都不行,只能做remove等無返回值無輸入A的操作。
PS: 注意,不是說不能呼叫get或add方法, 而是呼叫get或add時,不能使用A這個物件去操作。
即無法做add(A) 或者 A a = get(0)
但是可以做add(object) 或者Object o = get(0)
因為?可以轉為Object, 但是無法轉為A。
Q:下面這個程式碼會報錯嗎?
List<Fruit> fruitList = new ArrayList<>(); fruitList.add(new Fruit()); List<Apple> appleList = new ArrayList<>(); appleList.add(new Apple()); fruitList.addAll(appleList); System.out.println(fruitList);
A:不會報錯。會正常列印結果。
PECS原則
注意PECS原則和上面的區別!
上面之前提到的? extend或者? supert, 都是在宣告物件的時候用的。
而PECS原則是用於泛型物件的方法輸入引數!
假設有一個類定義如下:
public static class MyList<T> { List<T> list = new ArrayList<>(); // 把輸入引數塞給自己,類似於生產操作 public void pushList(List<T> t) { list.addAll(t); } // 把自己的內容塞給輸入引數,類似於讓輸入引數做消費。 public void pollList(List<T> t) { t.addAll(list); } }
則T就是泛型引數。
Q:下面程式碼能正常執行嗎?
MyList<Number> myList = new MyList<>(); List<Integer> intList = new ArrayList<>(); myList.pushList(intList); List<Object> objectList = new ArrayList<>(); myList.pollList(objectList);
A:不能正常執行, pushList和pollList都會報錯
因為編譯器檢查後,認為 List<Integer>和List<Number>不是一個東西!
Q: 如果上文要支援pushList,應該怎麼修改pushList方法的定義?
A:改成這樣:
// 把輸入引數塞給自己,類似於生產操作 public void pushList(List<? extends T> t) { list.addAll(t); }
即編譯器認為,List<Integer> 和List<? extend Number>是一個東西,允許!
Q: 如果要支援pollList,怎麼修改定義?
A:
// 把自己的內容塞給輸入引數,類似於讓輸入引數做消費。 public void pollList(List<? super T> t) { t.addAll(list); }
因為是把自己的東西塞給輸入引數, 而想要能塞進去,必須保證自己這個T,是輸入引數的子類,反過來說,輸入引數必須是T的父類,所以用super
於是編譯器認為,List<Object> 和List<? super Number>是一個東西,允許!
PECS原則出自Effective Java, 注意只是一個程式設計建議而已!
- 如果有一個類A,泛型引數為T
- 如果他一般只用於接收輸入容器List後,塞入自己內部的T容器, 則類A就叫生產者, 因此輸入引數最好定義為<? extend T>最好, 以便能接收任何T子類的容器。
- 如果他一般只用於接收輸入容器後List, 把自己內部的T元素塞給它, 那麼這個類A就叫消費者, 輸入引數最好定義為<? super T>\ 最好, 以便自己的T元素能塞給任何T元素的父類容器。
本文分享自華為雲社群《15個問題掌握java泛型》,原文作者:breakDraw 。