目錄
- 前言
- 探討
- 泛型解決了什么問題?
- 擴展
- 引入泛型
- 什么是泛型?
- 泛型類
- 泛型介面
- 泛型方法
- 型別擦除
- 擦除的問題
- 邊界
- 通配符
- 上界通配符
- 下界通配符
- 通配符和向上轉型
- 泛型約束
- 實踐總結
- 泛型命名
- 使用泛型的建議
- 參考資料:
前言
泛型是Java基礎知識的重點,雖然我們在初學Java的時候,都學過泛型,覺得自己掌握對于Java泛型的使用(全是錯覺),往后的日子,當我們深入去閱讀一些框架原始碼,你就發現了,自己會的只是簡單的使用,卻看不懂別人的泛型代碼是怎么寫的,還可以這樣,沒錯,別人寫出來的代碼那叫藝術,而我......
探討
Java語言為什么存在著泛型,而像一些動態語言Python,JavaScipt卻沒有泛型的概念?
原因是,像Java,C#這樣的靜態編譯型的語言,它們在傳遞引數的時候,引數的型別,必須是明確的,看一個例子,簡單撰寫一個存放int型別的堆疊—StackInt,代碼如下:
public class StackInt {
private int maxSize;
private int[] items;
private int top;
public StackInt(int maxSize){
this.maxSize = maxSize;
this.items = new int[maxSize];
this.top = -1;
}
public boolean isFull(){
return this.top == this.maxSize-1;
}
public boolean isNull(){
return this.top <= -1;
}
public boolean push(int value){
if(this.isFull()){
return false;
}
this.items[++this.top] = value;
return true;
}
public int pop(){
if(this.isNull()){
throw new RuntimeException("當前堆疊中無資料");
}
int value = https://www.cnblogs.com/kalton/p/this.items[top];
--top;
return value;
}
}
在這里使用建構式初始化一個StackInt物件時,可以傳入String字串嗎?很明顯是不行的,我們要求的是int型別,傳入字串String型別,這樣在語法檢查階段時會報錯的,像Java這樣的靜態編譯型的語言,引數的型別要求是明確的
泛型解決了什么問題?
引數不安全:引入泛型,能夠在編譯階段找出代碼的問題,而不是在運行階段
泛型要求在宣告時指定實際資料型別,Java 編譯器在編譯時會對泛型代碼做強型別檢查,并在代碼違反型別安全時發出告警,早發現,早治理,把隱患扼殺于搖籃,在編譯時發現并修復錯誤所付出的代價遠比在運行時小,
避免型別轉換:
未使用泛型:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); //需要在取出Value的時候進行強制轉換
使用泛型:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); //不需要強制轉換
重復編碼::通過使用泛型,可以實作通用編碼,可以處理不同型別的集合,并且型別安全且易于閱讀,像上面的StackInt類,我們不能針對每個型別去撰寫對應型別的堆疊,那樣太麻煩了,而泛型的出現就很好的解決了這點
擴展
在上面的StackInt類有一些不好的地方,那就是太具體了,不夠抽象,不夠抽象,那么它的復用性也是不高的,例如,在另外的場景下,我需要的是往堆疊里存String型別的字串,或者是其他型別,那么StackInt類就做不到了,那么有什么方法能夠做到呢?再寫一個StackString類,不可能,那樣不得累死,那就只有引入基類Object了,我們改進一下代碼:
public class StackObject {
private int maxSize;
private Object[] items;
private int top;
public StackObject(int maxSize){
this.maxSize = maxSize;
this.items = new Object[maxSize];
this.top = -1;
}
public boolean isFull(){
return this.top == this.maxSize-1;
}
public boolean isNull(){
return this.top <= -1;
}
public boolean push(Object value){
if(this.isFull()){
return false;
}
this.items[++this.top] = value;
return true;
}
public Object pop(){
if(this.isNull()){
throw new RuntimeException("當前堆疊中無資料");
}
Object value = https://www.cnblogs.com/kalton/p/this.items[top];
--top;
return value;
}
}
使用StackObject可以存盤任意型別的資料,那么這樣做,又有什么優點和缺點呢?
優點:StackObject類變得相對抽象了,我們可以往里面存盤任何型別的資料,這樣就避免了寫一些重復代碼
缺點:
1、用Object表示的物件是比較抽象的,它失去了型別的特點,那么我們在做一些運算的時候,可能會頻繁的拆箱裝箱的程序
看上面的例圖,我們理解的認為存放了兩個數值,12345和54321,將兩個進行相加,這是很常見的操作,但是報錯了,編譯器給我們的提示是,+操作運算不能用于兩個Object型別,那么只能對其進行型別轉換,這也是我們上面說到的泛型能解決的問題,我們需要這樣做,int sum = (int)val1 + (int)val2;,同時在涉及拆箱裝箱時,是有一定性能的損耗的,關于拆箱裝箱在這里不作描述,可以參考我寫過的隨筆—— 深入理解Java之裝箱與拆箱
2、對于我們push進去的值,我們在取出的時候,容易忘記型別轉換,或者不記得它的型別,型別轉換錯誤,這在后面的一些業務可能埋下禍根,例如下面這個場景:直到運行時錯誤才暴露出來,這是不安全的,也是違反軟體開發原則的,應該盡早的在編譯階段就發現問題,解決問題
3、使用Object太過于模糊了,沒有具體型別的意義
最好不要用到Object,因為Object是一切型別的基類,也就是說他把一些型別的特點給抹除了,比如上面存的數字,對于數字來說,加法運算就是它的一個特點,但是用了Object,它就失去了這一特點,失去型別特有的行為
引入泛型
什么是泛型?
泛型:是被引數化的類或介面,是對型別的約定
泛型類
class name<T1, T2, ..., Tn> { /* ... */ }
一般將泛型中的類名稱為原型,而將 <> 指定的引數稱為型別引數,<> 相當于型別的約定,T就是型別,相當于一個占位符,由我們在呼叫時指定
使用泛型改進一下上面StackObject類,但是,陣列和泛型不能很好地結合,你不能實體化具有引數化型別的陣列,例如下面的代碼是不合格的:
public StackT(int maxSize){
this.maxSize = maxSize;
this.items = new T[maxSize];
this.top = -1;
}
Java 中不允許直接創建泛型陣列,這是因為相比于C++,C#的語法,Java泛型其實是偽泛型,這點在后面會說到,但是,可以通過創建一個型別擦除的陣列,然后轉型的方式來創建泛型陣列,
private int maxSize;
private T[] items;
private int top;
public StackT(int maxSize){
this.maxSize = maxSize;
this.items = (T[]) new Object[maxSize];
this.top = -1;
}
實際上,真的需要存盤泛型,還是使用容器更合適,回到原來的代碼上,需要知道的是,泛型型別不能是基本型別的,需要是包裝類
上面說到了Java 中不允許直接創建泛型陣列,事實上,Java中的泛型我們是很難通new的方式去實體化物件,不僅僅是實體化物件,甚至是獲取T的真實型別也是很難的,當然通過反射的機制還是可以獲取到的,Java獲取真實型別的方式有 3 種,分別是:
1、類名.class
2、物件.getClass
3、class.forName("全限定類名")
但是,在這里,1和2的方式都是做不到的,雖然我們在外邊明確的傳入了Integer型別,new StackT<Integer>(3);但是在StackT
類,使用T.class還是獲取不到真實型別的,第 2 種方式的話,并沒有傳入物件,前面也說到是沒有辦法new方式實體化的,而通過反射機制是可以做到的,這里不作演示,需要了解的話可以參考 —— Java如何獲得泛型類的真實型別、 Java通過反射獲取泛型的型別
但是在C#中的泛型以及C++的模板,這是很容易做到的,所以說Java的泛型是偽泛型,Java并不是做不到像C#一樣,而是為了遷就老的JDK語法所作出的妥協,至于上面為什么做不到這樣,這就要說到泛型的型別擦除了,
再說型別擦除之前,先說一下泛型介面,和泛型方法吧
泛型介面
介面也可以宣告泛型,泛型介面語法形式:
public interface Content<T> {
T text();
}
泛型介面有兩種實作方式:
- 實作介面的子類明確宣告泛型型別
public class ContentImpl implements Content<Integer> {
private int text;
public ContentImpl(int text) {
this.text = text;
}
public static void main(String[] args) {
ContentImpl one = new ContentImpl(10);
System.out.print(one.text());
}
}
// Output:
// 10
- 實作介面的子類不明確宣告泛型型別
public class ContentImpl<T> implements Content<T> {
private T text;
public ContentImpl(T text) {
this.text = text;
}
@Override
public T text() { return text; }
public static void main(String[] args) {
ContentImpl<String> two = new ContentImpl<>("ABC");
System.out.print(two.text());
}
}
// Output:
// ABC
泛型方法
泛型方法是引入其自己的型別引數的方法,泛型方法可以是普通方法、靜態方法以及構造方法,
泛型方法語法形式如下:
public <T> T func(T obj) {}
是否擁有泛型方法,與其所在的類是否是泛型沒有關系,
泛型方法的語法包括一個型別引數串列,在尖括號內,它出現在方法的回傳型別之前,對于靜態泛型方法,型別引數部分必須出現在方法的回傳型別之前,型別引數能被用來宣告回傳值型別,并且能作為泛型方法得到的實際型別引數的占位符,
使用泛型方法的時候,通常不必指明型別引數,因為編譯器會為我們找出具體的型別,這稱為型別引數推斷(type argument inference),型別推斷只對賦值操作有效,其他時候并不起作用,如果將一個泛型方法呼叫的結果作為引數,傳遞給另一個方法,這時編譯器并不會執行推斷,編譯器會認為:呼叫泛型方法后,其回傳值被賦給一個 Object 型別的變數,
public class GenericsMethod {
public static <T> void printClass(T obj) {
System.out.println(obj.getClass().toString());
}
public static void main(String[] args) {
printClass("abc");
printClass(10);
}
}
// Output:
// class java.lang.String
// class java.lang.Integer
泛型方法中也可以使用可變引數串列
public class GenericVarargsMethod {
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<T>();
Collections.addAll(result, args);
return result;
}
public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
}
}
// Output:
// [A]
// [A, B, C]
型別擦除
事實上,Java的運行大致可以分為兩個階段,編譯階段,運行階段
那么對于Java泛型來說,當編譯階段過后,泛型 T 是已經被擦除了,所以在運行階段,它已經丟失了 T 的具體資訊,而我們去實體化一個物件的時候,比如T c = new T();,它的發生時機是在運行階段,而在運行階段,你要new T(),就需要知道 T 的具體型別,實際上這時候 T是被替換成Integer了,而JVM是不知道T的型別的,所以是沒有辦法實體化的,
那么,型別擦除做了什么呢?它做了以下作業:
- 把泛型中的所有型別引數替換為 Object,如果指定型別邊界,則使用型別邊界來替換,因此,生成的位元組碼僅包含普通的類,介面和方法,
- 擦除出現的型別宣告,即去掉
<>的內容,比如T get()方法宣告就變成了Object get();List<String>就變成了List,如有必要,插入型別轉換以保持型別安全, - 生成橋接方法以保留擴展泛型型別中的多型性,型別擦除確保不為引數化型別創建新類;因此,泛型不會產生運行時開銷,
讓我們來看一個示例:
import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
}
/* Output:
true
*/
ArrayList<String> 和 ArrayList<Integer> 應該是不同的型別,不同的型別會有不同的行為,例如,如果嘗試向 ArrayList<String> 中放入一個 Integer,所得到的行為(失敗)和 向 ArrayList<Integer> 中放入一個 Integer 所得到的行為(成功)完全不同,但是結果輸出的是true,這意味著使用泛型時,任何具體的型別資訊都被擦除了,ArrayList<Object> 和 ArrayList<Integer> 在運行時,JVM 將它們視為同一型別class java.util.ArrayList
再用一個例子來對于該謎題的補充:
import java.util.*;
class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION, MOMENTUM> {}
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<>();
Map<Frob, Fnorkle> map = new HashMap<>();
Quark<Fnorkle> quark = new Quark<>();
Particle<Long, Double> p = new Particle<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
}
}
/* Output:
[E]
[K,V]
[Q]
[POSITION,MOMENTUM]
*/
根據 JDK 檔案,Class.getTypeParameters() “回傳一個 TypeVariable 物件陣列,表示泛型宣告中宣告的型別引數...” 這暗示你可以發現這些引數型別,但是正如上例中輸出所示,你只能看到用作引數占位符的識別符號,這并非有用的資訊,
殘酷的現實是:在泛型代碼內部,無法獲取任何有關泛型引數型別的資訊,
以上兩個例子皆出《Java 編程思想》第五版 —— On Java 8中的例子,本文借助該例子,試圖講清楚Java泛型是使用型別擦除這里機制實作的,能力不足,有錯誤的地方,還請指正,關于On Java 8一書,已在github上開源,并有熱心的伙伴將之翻譯成中文,現在給出閱讀地址,On Java 8
擦除的問題
擦除的代價是顯著的,泛型不能用于顯式地參考運行時型別的操作中,例如轉型、instanceof 操作和 new 運算式,因為所有關于引數的型別資訊都丟失了,當你在撰寫泛型代碼時,必須時刻提醒自己,你只是看起來擁有有關引數的型別資訊而已,
考慮如下的代碼段:
class Foo<T> {
T var;
}
看上去當你創建一個 Foo 實體時:
Foo<Cat> f = new Foo<>();
class Foo 中的代碼應該知道現在作業于 Cat 之上,泛型語法也在強烈暗示整個類中所有 T 出現的地方都被替換,就像在 C++ 中一樣,但是事實并非如此,當你在撰寫這個類的代碼時,必須提醒自己:“不,這只是一個 Object“,
繼承問題
泛型時基于型別擦除實作的,所以,泛型型別無法向上轉型,
向上轉型是指用子類實體去初始化父類,這是面向物件中多型的重要表現,
Integer 繼承了 Object;ArrayList 繼承了 List;但是 List<Interger> 卻并非繼承了 List<Object>,
這是因為,泛型類并沒有自己獨有的 Class 類物件,比如:并不存在 List<Object>.class 或是 List<Interger>.class,Java 編譯器會將二者都視為 List.class,
如何解決上面所產生的問題:
其實并不一定要通過new的方式去實體化,我們可以通過顯式的傳入源類,一個Class<T> clazz的物件來補償擦除,例如instanceof 操作,在程式中嘗試使用 instanceof 將會失敗,型別標簽可以使用動態 isInstance() ,這樣改進代碼:
public class Improve<T> {
//錯誤方法
public boolean f(Object arg) {
// error: illegal generic type for instanceof
if (arg instanceof T) {
return true;
}
return false;
}
//改進方法
Class<T> clazz;
public Improve(Class<T> clazz) {
this.clazz = clazz;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
}
實體化:
試圖在 new T() 是行不通的,部分原因是由于擦除,部分原因是編譯器無法驗證 T 是否具有默認(無參)建構式,
Java 中的解決方案是傳入一個工廠物件,并使用該物件創建新實體,方便的工廠物件只是 Class 物件,因此,如果使用型別標記,則可以使用 newInstance() 創建該型別的新物件:
class Improve<T> {
Class<T> kind;
Improve(Class<T> kind) {
this.kind = kind;
}
public T get(){
try {
return kind.newInstance();
} catch (InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
class Employee {
@Override
public String toString() {
return "Employee";
}
}
public class InstantiateGenericType {
public static void main(String[] args) {
Improve<Employee> fe = new Improve<>(Employee.class);
System.out.println(fe.get());
}
}
/* Output:
Employee
*/
通過這樣改進代碼,可以實作創建物件的實體,但是要注意的是,newInstance();方法呼叫無參建構式的,如果傳入的型別,沒有無參構造的話,是會拋出InstantiationException例外的,
泛型陣列:
泛型陣列這部分,我們在上面說到可以通過創建一個型別擦除的陣列,然后轉型的方式來創建泛型陣列,這次我們可以通過顯式的傳入源類的方式來撰寫StackT類,解決創建泛型陣列的問題,代碼如下:
public class StackT<T> {
private int maxSize;
private T[] items;
private int top;
public StackT(int maxSize, Class<T> clazz){
this.maxSize = maxSize;
this.items = this.createArray(clazz);
this.top = -1;
}
public boolean isFull(){
return this.top == this.maxSize-1;
}
public boolean isNull(){
return this.top <= -1;
}
public boolean push(T value){
if(this.isFull()){
return false;
}
this.items[++this.top] = value;
return true;
}
public T pop(){
if(this.isNull()){
throw new RuntimeException("當前堆疊中無資料");
}
T value = https://www.cnblogs.com/kalton/p/this.items[top];
--top;
return value;
}
private T[] createArray(Class clazz){
T[] array =(T[])Array.newInstance(clazz, this.maxSize);
return array;
}
}
邊界
有時您可能希望限制可在引數化型別中用作型別引數的型別,型別邊界可以對泛型的型別引數設定限制條件,例如,對數字進行操作的方法可能只想接受 Number 或其子類的實體,
要宣告有界型別引數,請列出型別引數的名稱,然后是 extends 關鍵字,后跟其限制類或介面,
型別邊界的語法形式如下:
<T extends XXX>
示例:
public class GenericsExtendsDemo01 {
static <T extends Comparable<T>> T max(T x, T y, T z) {
T max = x; // 假設x是初始最大值
if (y.compareTo(max) > 0) {
max = y; //y 更大
}
if (z.compareTo(max) > 0) {
max = z; // 現在 z 更大
}
return max; // 回傳最大物件
}
public static void main(String[] args) {
System.out.println(max(3, 4, 5));
System.out.println(max(6.6, 8.8, 7.7));
System.out.println(max("pear", "apple", "orange"));
}
}
// Output:
// 5
// 8.8
// pear
示例說明:
上面的示例宣告了一個泛型方法,型別引數
T extends Comparable<T>表明傳入方法中的型別必須實作了 Comparable 介面,
型別邊界可以設定多個,語法形式如下:
<T extends B1 & B2 & B3>
注意:extends 關鍵字后面的第一個型別引數可以是類或介面,其他型別引數只能是介面,
通配符
通配符是Java泛型中的一個非常重要的知識點,很多時候,我們其實不是很理解通配符?和泛型型別T區別,容易混淆在一起,其實還是很好理解的,?和 T 都表示不確定的型別,區別在于我們可以對 T 進行操作,但是對 ?不行,比如如下這種 :
// 可以
T t = operate();
// 不可以
? car = operate();
但是這個并不是我們混淆的原因,雖然?和 T 都表示不確定的型別,T 通常用于泛型類和泛型方法的定義,?通常用于泛型方法的呼叫代碼和形參,不能用于定義類和泛型方法,用代碼解釋一下,回到文章最初說的堆疊類StackT,我們以這個為基礎來解釋,上面的觀點:
public class Why {
public static void main(String[] args) {
StackT<Integer> stackT = new StackT<>(3, Integer.class);
stackT.push(8);
StackT<String> stackT1 = new StackT<>(3, String.class);
stackT1.push("7");
test(stackT1);
}
public static void test(StackT stackT){
System.out.println(stackT.pop());
}
}
// Output: 8
以我們撰寫的StackT類,進行測驗,撰寫一個test方法,傳入引數型別StackT,上面的程式正常輸出字串"7" ,這沒有什么問題,問題在這里失去了泛型的限定,傳進去的實參StackT1,是被我們限定為StackT<String> ,但是我們通過編譯器可以看到stackT.pop()出來的物件,并沒有String型別的特有方法,也就是說,它其實是Object類
那么我們就需要修改test方法的形參,改為:
public static void test(StackT<String> stackT){
System.out.println(stackT.pop());
}
這樣子就回到了我們問題的本質來了,將形參修改為StackT<String>,這起到了泛型的限定作用,但是會出現這樣的問題,如果我們需要向該方法傳入StackT<Integer>型別的物件 stackT是,因為方法形參限定了StackT<String>,,這時候就報錯了
這個時候就是通配符?起作用了,將方法形參改為StackT<?>就可以了,這也就確定了我們剛剛的結論,?通配符通常是用于泛型傳參,而不是泛型類的定義,
public static void test(StackT<?> stackT){
System.out.println(stackT.pop());
}
但是這種用法我們通常也不會去用,因為它還是失去了型別的特點,即當無界泛型通配符作為形參時,作為呼叫方,并不限定傳遞的實際引數型別,但是,在方法內部,泛型類的引數和回傳值為泛型的方法,不能使用!
這里,StackT.push就不能用了,因為我并不知道?傳的是Integer還是String ,還是其他型別,所以是會報錯的,
但是我們有時候是有這樣的需求的,我們在接收泛型堆疊StackT作為形參的時候,我想表達一種約束的關系,但是又不像StackT<String>一樣,約束的比較死板,而Java是面向物件的語言,那么就會有繼承的機制,我想要的約束關系是我能接收的泛型堆疊的型別都是Number類的派生類,即不會像?無界通配符一樣失去類的特征,又不會像StackT<String>約束的很死,這就引出了上界通配符的概念,
上界通配符
可以使用上界通配符來縮小型別引數的型別范圍,
它的語法形式為:<? extends Number>
public class Why {
public static void main(String[] args) {
StackT<Integer> stackT = new StackT<>(3, Integer.class);
stackT.push(8);
StackT<String> stackT1 = new StackT<>(3, String.class);
stackT1.push("7");
StackT<Double> stackT2 = new StackT<>(3, Double.class);
//通過
test(stackT);
test(stackT2);
//error
test(stackT1);
}
public static void test(StackT<? extends Number> stackT){
System.out.println(stackT.pop());
}
}
這樣就實作了一型別別的限定,但是需求變更了,我現在希望的約束關系是我能接收的泛型堆疊的型別都是Number類的父類,或者父類的父類,那么有上界,自然就有下界
下界通配符
下界通配符將未知型別限制為該型別的特定型別或超型別別,
注意:上界通配符和下界通配符不能同時使用,
它的語法形式為:<? super Number>
public class Why {
public static void main(String[] args) {
StackT<Number> stackT1 = new StackT<>(3, Number.class);
stackT1.push(8);
StackT<Double> stackT2 = new StackT<>(3, Double.class);
StackT<Object> stackT3 = new StackT<>(3, Object.class);
//通過
test(stackT1);
test(stackT3);
//error
test(stackT2);
}
public static void test(StackT<? super Number> stackT){
System.out.println(stackT.pop());
}
}
這樣子的話,就確保了我們的test方法只接收Number型別以上的方法,泛型的各種高級語法可能在寫業務代碼的時候可以規避,但是如果你要去寫一些框架的時候,由于你不知道框架的使用者的使用場景,那么掌握泛型的高級語法就很有用了,
通配符和向上轉型
前面,我們提到:泛型不能向上轉型,但是,我們可以通過使用通配符來向上轉型,
public class GenericsWildcardDemo {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Error
List<? extends Integer> intList2 = new ArrayList<>();
List<? extends Number> numList2 = intList2; // OK
}
}
通配符邊界問題,關于一些更加深入的解惑可以參考整理的轉載的文章——Java泛型解惑之上下通配符
泛型約束
- 泛型型別的型別引數不能是值型別
Pair<int, char> p = new Pair<>(8, 'a'); // 編譯錯誤
- 不能創建型別引數的實體
public static <E> void append(List<E> list) {
E elem = new E(); // 編譯錯誤
list.add(elem);
}
- 不能宣告型別為型別引數的靜態成員
public class MobileDevice<T> {
private static T os; // error
// ...
}
- 型別引數不能使用型別轉換或
instanceof
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // 編譯錯誤
// ...
}
}
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 編譯錯誤
- 不能創建型別引數的陣列
List<Integer>[] arrayOfLists = new List<Integer>[2]; // 編譯錯誤
- 不能創建、catch 或 throw 引數化型別物件
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // 編譯錯誤
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // 編譯錯誤
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
- 僅僅是泛型類相同,而型別引數不同的方法不能多載
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { } // 編譯錯誤
}
實踐總結
泛型命名
泛型一些約定俗成的命名:
- E - Element
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
使用泛型的建議
- 消除型別檢查告警
- List 優先于陣列
- 優先考慮使用泛型來提高代碼通用性
- 優先考慮泛型方法來限定泛型的范圍
- 利用有限制通配符來提升 API 的靈活性
- 優先考慮型別安全的異構容器
參考資料:
深入理解 Java 泛型
On Java 8
Java泛型解惑之 extends T>和 super T>上下界限
7月的直播課——Java 高級語法—泛型
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/3020.html
標籤:Java
