java泛型原理及其使用
一、什麼是泛型
Java從1.5之後支援泛型,泛型的本質是型別引數,也就是說所操作的資料型別被指定為一個引數。這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法。
若不支援泛型,則表現為支援Object,不是特定的泛型。泛型是對 Java 語言的型別系統的一種擴充套件,以支援建立可以按型別進行引數化的類。可以把型別引數看作是使用引數化型別時指定的型別的一個佔位符,就像方法的形式引數是執行時傳遞的值的佔位符一樣。許多重要的類,比如集合框架,都已經成為泛型化的了。
二、泛型有什麼優點
泛型的好處是在編譯的時候檢查型別安全,並且所有的強制轉換都是自動和隱式的,以提高程式碼的重用率。
1、型別安全
泛型的主要目標是提高 Java 程式的型別安全。通過知道使用泛型定義的變數的型別限制,編譯器可以在一個高得多的程度上驗證型別假設。沒有泛型,這些假設就無法落實到程式碼中,僅僅能停留在設計方案或者註釋中。
2、消除強制型別轉換
泛型的一個附帶好處是,消除原始碼中的許多強制型別轉換。這使得程式碼更加可讀,並且減少了強制轉換程式碼和出錯機會。
3、潛在的效能收益
泛型為較大的優化帶來可能。在泛型的初始實現中,編譯器將強制型別轉換(沒有泛型的話,程式設計師會指定這些強制型別轉換)插入生成的位元組碼中。
三、泛型如何表示
我們在泛型中是用的T,E,K,V有什麼區別呢,實際上使用大寫字母A,B,C,D......X,Y,Z定義的,就都是泛型,把T換成A也一樣,這裡T只是名字上的意義而已,如:
- ?表示不確定的java型別,型別是未知的。
- T (type)表示具體的一個java型別,如果要定義超過兩個,三個或三個以上的泛型引數可以使用T1, T2, ..., Tn
- K V (key value)分別代表java鍵值中的Key Value
- E (element)代表Element
- extends、super泛型的引數型別可以使用extends、super語句,例如<T extends superclass>。習慣上稱為“有界型別”。
四、泛型的原理
泛型是一種語法糖,泛型這種語法糖的基本原理是型別擦除,即編譯器會在編譯期間「擦除」泛型語法並相應的做出一些型別轉換動作。例如:
public class Caculate<T> { private T num; }
我們定義了一個泛型類,定義了一個屬性成員,該成員的型別是一個泛型型別,這個 T 具體是什麼型別,我們也不知道,它只是用於限定型別的。反編譯一下這個 Caculate 類:
public class Caculate{ public Caculate(){} private Object num; }
發現編譯器擦除 Caculate 類後面的兩個尖括號,並且將 num 的型別定義為 Object 型別。
那麼是不是所有的泛型型別都以 Object 進行擦除呢?大部分情況下,泛型型別都會以 Object 進行替換,而有一種情況則不是。那就是使用到了extends和super語法的有界型別,如:
public class Caculate<T extends String> { private T num; }
這種情況的泛型型別,num 會被替換為 String 而不再是 Object。這是一個型別限定的語法,它限定 T 是 String 或者 String 的子類,也就是你構建 Caculate 例項的時候只能限定 T 為 String 或者 String 的子類,所以無論你限定 T 為什麼型別,String 都是父類,不會出現型別不匹配的問題,於是可以使用 String 進行型別擦除。
實際上編譯器會正常的將使用泛型的地方編譯並進行型別擦除,然後返回例項。但是除此之外的是,如果構建泛型例項時使用了泛型語法,那麼編譯器將標記該例項並關注該例項後續所有方法的呼叫,每次呼叫前都進行安全檢查,非指定型別的方法都不能呼叫成功。
實際上編譯器不僅關注一個泛型方法的呼叫,它還會為某些返回值為限定的泛型型別的方法進行強制型別轉換,由於型別擦除,返回值為泛型型別的方法都會擦除成 Object 型別,當這些方法被呼叫後,編譯器會額外插入一行 checkcast 指令用於強制型別轉換。這一個過程就叫做『泛型翻譯』。
五、泛型使用
泛型型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法,其中類和介面使用方式大致一致。
1、泛型類和介面
訪問修飾符 class/interface 類名或介面名<限定型別變數名>
如泛型類和介面:
//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的引數常用於表示泛型 //在例項化泛型類時,必須指定T的具體型別 public class Generic<T>{ //key這個成員變數的型別為T,T的型別由外部指定 private T key; public Generic(T key) { //泛型構造方法形參key的型別也為T,T的型別由外部指定 this.key = key; } public T getKey(){ //泛型方法getKey的返回值型別為T,T的型別由外部指定 return key; } } //定義一個泛型介面 public interface Generator<T> { public T next(); }
注意當實現泛型介面的類,未傳入泛型實參時:
/** * 未傳入泛型實參時,與泛型類的定義相同,在宣告類的時候,需將泛型的宣告也一起加到類中 * 即:class FruitGenerator<T> implements Generator<T>{ * 如果不宣告泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class" */ class FruitGenerator<T> implements Generator<T>{ @Override public T next() { return null; } }
當實現泛型介面的類,傳入泛型實參時:
/** * 傳入泛型實參時: * 定義一個生產器實現這個介面,雖然我們只建立了一個泛型介面Generator<T> * 但是我們可以為T傳入無數個實參,形成無數種類型的Generator介面。 * 在實現類實現泛型介面時,如已將泛型型別傳入實參型別,則所有使用泛型的地方都要替換成傳入的實參型別 * 即:Generator<T>,public T next();中的的T都要替換成傳入的String型別。 */ public class FruitGenerator implements Generator<String> { private String[] fruits = new String[]{"Apple", "Banana", "Pear"}; @Override public String next() { Random rand = new Random(); return fruits[rand.nextInt(3)]; } }
2、泛型方法
泛型類/介面,是在例項化類的時候指明泛型的具體型別;泛型方法,是在呼叫方法的時候指明泛型的具體型別。
方法並不一定依賴其外部的類或者介面,它可以獨立存在,也可以依賴外圍類存在。
public E get(int index) { rangeCheck(index); return elementData(index); }
ArrayList 的這個 get 方法就是一個泛型方法,它依賴外圍 ArrayList 宣告的 E 這個泛型型別,也就是它沒有自己宣告一個泛型型別而用的外圍類的。當然,另一種方式就是自己申明一個泛型型別並使用:
/** * 泛型方法的基本介紹 * @param tClass 傳入的泛型實參 * @return T 返回值為T型別 * 說明: * 1)public 與 返回值中間<T>非常重要,可以理解為宣告此方法為泛型方法。 * 2)只有聲明瞭<T>的方法才是泛型方法,泛型類中的使用了泛型的成員方法並不是泛型方法。 * 3)<T>表明該方法將使用泛型型別T,此時才可以在方法中使用泛型型別T。 * 4)與泛型類的定義一樣,此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的引數常用於表示泛型。 */ public <T> T genericMethod(Class<T> tClass)throws InstantiationException , IllegalAccessException{ T instance = tClass.newInstance(); return instance; }
如果既不依賴外圍又未自己宣告的話會:
// 這個方法顯然是有問題的,在編譯器會給我們提示這樣的錯誤資訊"cannot reslove symbol E" // 因為在類的宣告中並未宣告泛型E,所以在使用E做形參和返回值型別時,編譯器會無法識別。 public E setKey(E key){ this.key = keu }
3、萬用字元
萬用字元是用於解決泛型之間引用傳遞問題的特殊語法。如:
public static void main(String[] args){ Integer[] integerArr = new Integer[2]; Number[] numberArr = new Number[2]; numberArr = integerArr; ArrayList<Integer> integers = new ArrayList<>(); ArrayList<Number> numbers = new ArrayList<>(); numbers = integers;//編譯不通過 }
Java 中,陣列是協變的,即 Integer extends Number,那麼子類陣列例項是可以賦值給父類陣列例項的。那是由於 Java 中的陣列型別本質上會由虛擬機器執行時動態生成一個型別,這個型別除了記錄陣列的必要屬性,如長度,元素型別等,會有一個指標指向記憶體某個位置,這個位置就是該陣列元素的起始位置。
所以子類陣列例項賦值父類陣列例項,只不過意味著父類陣列例項的引用指向堆中子類陣列而已,並不會有所衝突,因此是 Java 允許這種操作的。而泛型是不允許這麼做的,為什麼呢?我們假設泛型允許這種協變,看看會有什麼問題。
ArrayList<Integer> integers = new ArrayList<>(); ArrayList<Number> numbers = new ArrayList<>(); numbers = integers;//假設的前提下,編譯器是能通過的 numbers.add(23.5);
假設 Java 允許泛型協變,那麼上述程式碼在編譯器看來是沒問題的,但執行時就會出現問題。這個 add 方法實際上就將一個浮點數放入了整型容器中了,雖然由於型別擦除並不會對程式執行造成問題,但顯然違背了泛型的設計初衷,容易造成邏輯混亂,所以 Java 乾脆禁止泛型協變。
所以雖然 ArrayList<Integer> 和 ArrayList<Number>編譯器型別擦除之後都是 ArrayList 的例項,但是起碼在編譯器看來,這兩者是兩種不同的型別。那麼,假如有某種需求,我們的方法既要支援子類泛型作為形參傳入,也要支援父類泛型作為形參傳入,又該怎麼辦呢?
我們使用萬用字元處理這樣的需求,例如:
public void test1(ArrayList<? extends Number> list){}
// 或者
public void test2(自定義類<?> obj){}
如上ArrayList<? extends Number> 表示泛型型別具體是什麼不知道,但是具體型別必須是 Number 及其子類型別。例如:ArrayList<Number>,ArrayList<Integer>,ArrayList<Double> 等。
但是,萬用字元往往用於方法的形參中,而不允許用於定義和呼叫語法中。因為? 代表不確定型別,萬用字元匹配出來的泛型型別只能讀取,不能寫。所以你只能讀取裡面的資料,不能瞎往裡面新增元素。如:
ArrayList<Number> list = new ArrayList<>(); ArrayList<?> arrayList = list; // 後面的會編譯報錯 arrayList.add(32); arrayList.add("fadsf"); arrayList.add(new Object());
4、型別限定
在使用泛型的時候,我們還可以為傳入的泛型型別實參進行上下邊界的限制,如:型別實參只准傳入某種型別的父類或某種型別的子類。為泛型新增上邊界,即傳入的型別實參必須是指定型別的子型別。如:<? super T> 與 <? extends T>、<T extends String>
如一個類:
public class Generic<T>{ private T key; public Generic(T key) { this.key = key; } public T getKey(){ return key; } public void showKeyValue1(Generic<? extends Number> obj){ Log.d("泛型測試","key value is " + obj.getKey()); } }
那麼:
Generic<String> generic1 = new Generic<String>("11111"); Generic<Integer> generic2 = new Generic<Integer>(2222); Generic<Float> generic3 = new Generic<Float>(2.4f); Generic<Double> generic4 = new Generic<Double>(2.56); //這一行程式碼編譯器會提示錯誤,因為String型別並不是Number型別的子類 //showKeyValue1(generic1); showKeyValue1(generic2); showKeyValue1(generic3); showKeyValue1(generic4);
如果改為:
public class Generic<T extends Number>{ private T key; public Generic(T key) { this.key = key; } public T getKey(){ return key; } public void showKeyValue1(Generic<? extends Number> obj){ Log.d("泛型測試","key value is " + obj.getKey()); } }
//這一行程式碼也會報錯,因為String不是Number的子類 Generic<String> generic1 = new Generic<String>("11111");
在泛型方法中新增上下邊界限制的時候,必須在許可權宣告與返回值之間的<T>上新增上下邊界,即在泛型宣告的時候新增:
//public <T> T showKeyName(Generic<T extends Number> container),編譯器會報錯:"Unexpected bound" public <T extends Number> T showKeyName(Generic<T> container){ System.out.println("container key :" + container.getKey()); T test = container.getKey(); return test; }
六、泛型限制
1、靜態屬性不支援泛型,如:
private static T target; //編譯報錯
2、在類中的靜態方法無法訪問類上定義的泛型
如果靜態方法操作的引用資料型別不確定的時候,必須要將泛型定義在方法上。即:如果靜態方法要使用泛型的話,必須將靜態方法也定義成泛型方法。
public class StaticGenerator<T> { /** * 如果在類中定義使用泛型的靜態方法,需要新增額外的泛型宣告(將這個方法定義成泛型方法) * 即使靜態方法要使用泛型類中已經宣告過的泛型也不可以。 * 如:public static void show(T t){..},此時編譯器會提示錯誤資訊: "StaticGenerator cannot be refrenced from static context" */ public static <T> void show(T t){} }
3、泛型的型別引數只能是類型別(包括自定義類),不能是簡單型別,可使用其包裝型別代替(如:Integer)。
4、不能使用泛型類異常
5、不能例項化泛型物件:如:T t = new T();
6、不能例項化泛型陣列
經過檢視sun的說明文件,在java中是”不能建立一個確切的泛型型別的陣列”的。
也就是說下面的這個例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用萬用字元建立泛型陣列是可以的,如下面這個例子:
List<?>[] ls = new ArrayList<?>[10];
這樣也是可以的:
List<String>[] ls = new ArrayList[10];
7、一個泛型類被其所有呼叫共享
List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass());
會返回true,因為一個泛型類的所有例項在執行時具有相同的執行時類(class),而不管他們的實際型別引數。事實上,泛型之所以叫泛型,就是因為它對所有其可能的型別引數,有同樣的行為;同樣的類可以被當作許多不同的型別。作為一個結果,類的靜態變數和方法也在所有的例項間共享。這就是為什麼在靜態方法或靜態初始化程式碼中或者在靜態變數的宣告和初始化時使用型別引數(型別引數是屬於具體例項的)是不合法的原因。
8、不能對確切的泛型型別使用instanceof操作
泛型類被所有其例項(instances)共享的另一個暗示是檢查一個例項是不是一個特定型別的泛型類是沒有意義的。如:
Collection cs = new ArrayList<String>(); if (cs instanceof Collection<String>) { ...} // 非法