一文帶你深入瞭解Java泛型
什麼是Java泛型
Java 泛型(generics)是 Jdk 5 中引入的一個新特性, 泛型提供瞭編譯時類型安全檢測機制, 該機制允許程序員在編譯時檢測到非法的類型。
比如 ArrayList<String> list= new ArrayList<String>()
這行代碼就指明瞭該 ArrayList 對象隻能 存儲String
類型,如果傳入其他類型的對象就會報錯。
讓我們時光回退到Jdk5的版本,那時ArrayList
內部其實就是一個Object[] 數組,配合存儲一個當前分配的長度,就可以充當“可變數組”:
public class ArrayList { private Object[] array; private int size; public void add(Object e) {...} public void remove(int index) {...} public Object get(int index) {...} }
我們來舉個簡單的例子,
ArrayList list = new ArrayList(); list.add("test"); list.add(666);
我們本意是用ArrayList來裝String類型的值,但是突然混進去瞭Integer類型的值,由於ArrayList底層是Object數組,可以存儲任意的對象,所以這個時候是沒啥問題的,但我們不能隻存不用啊,我們需要把值給拿出來使用,這個時候問題來瞭:
for(Object item: list) { System.out.println((String)item); }
結果:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
由於我們需要String類型的值,我們需要把ArrayList的Object值強制轉型,但是之前混進去瞭Integer ,雖然編譯階段通過瞭,但程序的運行結果會以崩潰結束,報ClassCastException異常
為瞭解決這個問題,在Jdk 5版本中就引入瞭泛型的概念,而引入泛型的很大一部分原因就是為瞭解決我們上述的問題,允許程序員在編譯時檢測到非法的類型。不是同類型的就不允許在一塊存放,這樣也避免瞭ClassCastException異常
的出現,而且因為都是同一類型,也就沒必要做強制類型轉換瞭。
我們可以把ArrayList 變量參數化:
public class ArrayList<T> { private T[] array;//我們 假設 ArrayList<T>內部會有個T[] array private int size; public void add(T e) {...} public void remove(int index) {...} public T get(int index) {...} }
其中T叫類型參數 ,T
可以是任何class類型,現在ArrayList我們可以如下使用:
// 存儲String的ArrayList ArrayList<String> list = new ArrayList<String>(); list.add(666);//編譯器會在編譯階段發現問題,從而提醒開發者
泛型其本質是參數化類型,也就是說數據類型 作為 參數,解決不確定具體對象類型的問題。
泛型的使用
泛型一般有三種使用方式,分別為:泛型類、泛型接口、泛型方法,我們簡單介紹一下泛型的使用
泛型類
//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型 //在實例化泛型類時,必須指定T的具體類型 public class Generic<T>{ private T key; public Generic(T key) { this.key = key; } public T getKey(){ return key; } }
如何實例化泛型類:
Generic<Integer> genericInteger = new Generic<Integer>(666); Generic<String> genericStr = new Generic<String>("hello");
泛型接口
//定義一個泛型接口 public interface Generator<T> { public T method(); } //實現泛型接口,不指定類型 class GeneratorImpl<T> implements Generator<T>{ @Override public T method() { return null; } } //實現泛型接口,指定類型 class GeneratorImpl<T> implements Generator<String>{ @Override public String method() { return "hello"; } }
泛型方法
public class GenericMethods { public <T> void f(T x){ System.out.println(x.getClass().getName()); } public static void main(String[] args) { GenericMethods gm = new GenericMethods(); gm.f("啦啦啦"); gm.f(666); } }
結果:
java.lang.String
java.lang.Integer
泛型的底層實現機制
ArrayList源碼解析
通過上文我們知道,為瞭讓ArrayList存取各種數據類型的值,我們需要把ArrayList模板化,將變量的數據類型 給抽象出來,作為類型參數
public class ArrayList<T> { private T[] array;// 我們以為ArrayList<T>內部會有個T[] array private int size; public void add(T e) {...} public void remove(int index) {...} public T get(int index) {...} }
但當我們查看Jdk8 的ArrayList源碼,底層數組還是Object數組:transient Object[] elementData;
那ArrayList為什麼還能進行類型約束和自動類型轉換呢?
什麼是泛型擦除
我們再看一個經典的例子:
public class genericTest { public static void main(String [] args) { String str=""; Integer param =null; ArrayList<String> l1 = new ArrayList<String>(); l1.add("aaa"); str = l1.get(0); ArrayList<Integer> l2 = new ArrayList<Integer>(); l2.add(666); param = l2.get(0); System.out.println(l1.getClass() == l2.getClass()); } }
結果竟然是true
,ArrayList.class 和 ArrayList.class 應該是不同的類型。通過getClass()方法獲取他們的類的信息,竟然是一樣的。我們來查看這個文件的class文件:
public class genericTest { public genericTest() { } public static void main(String[] var0) { String var1 = ""; Integer var2 = null; ArrayList var3 = new ArrayList();//泛型被擦擦瞭 var3.add("aaa"); var1 = (String)var3.get(0); ArrayList var4 = new ArrayList();//泛型被擦擦瞭 var4.add(666); var2 = (Integer)var4.get(0); System.out.println(var3.getClass() == var4.getClass()); } }
我們在對其反匯編一下:
$ javap -c genericTest ▒▒▒▒: ▒▒▒▒▒▒▒ļ▒genericTest▒▒▒▒com.zj.demotest.test5.genericTest Compiled from "genericTest.java" public class com.zj.demotest.test5.genericTest { public com.zj.demotest.test5.genericTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String 2: astore_1 3: aconst_null 4: astore_2 5: new #3 // class java/util/ArrayList 8: dup 9: invokespecial #4 // Method java/util/ArrayList."<init>":()V 12: astore_3 13: aload_3 14: ldc #5 // String aaa 16: invokevirtual #6 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 19: pop 20: aload_3 21: iconst_0 22: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object; 25: checkcast #8 // class java/lang/String 28: astore_1 29: new #3 // class java/util/ArrayList 32: dup 33: invokespecial #4 // Method java/util/ArrayList."<init>":()V 36: astore 4 38: aload 4 40: sipush 666 43: invokestatic #9 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 46: invokevirtual #6 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 49: pop 50: aload 4 52: iconst_0 53: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object; 56: checkcast #10 // class java/lang/Integer 59: astore_2 60: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 63: aload_3 64: invokevirtual #12 // Method java/lang/Object.getClass:()Ljava/lang/Class; 67: aload 4 69: invokevirtual #12 // Method java/lang/Object.getClass:()Ljava/lang/Class; 72: if_acmpne 79 75: iconst_1 76: goto 80 79: iconst_0 80: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V 83: return }
看第16、46處,add進去的是原始類型Object;
看第22、53處,get方法獲得也是Object類型,String、Integer類型被擦出,隻保留原始類型Object。
看25、55處,checkcast指令是類型轉換檢查 ,在結合class文件var1 = (String)var3.get(0);``var2 = (Integer)var4.get(0);
我們知曉編譯器自動幫我們強制類型轉換瞭,我們無需手動類型轉換
經過上面的種種現象,我們可以發現,在類加載的編譯階段,泛型類型String和Integer都被擦除掉瞭,隻剩下原始類型,這樣他們類的信息都是Object,這樣自然而然就相等瞭。這種機制就叫泛型擦除。
我們需要瞭解一下類加載生命周期:
詳情見:詳解Java類加載器與雙親委派機制
泛型是和編譯器的約定,在編譯期對代碼進行檢查的,由編譯器負責解析,JVM並無識別的能力,一個類繼承泛型後,當變量存入這個類的時候,編譯器會對其進行類型安全檢測,當從中取出數據時,編譯器會根據與泛型的約定,會自動進行類型轉換,無需我們手動強制類型轉換。
泛型類型參數化,並不意味這其對象類型是不確定的,相反它的對象類型 對於JVM來說,都是確定的,是Object或Object[]數組
泛型的邊界
來看一個經典的例子,我們想要實現一個ArrayList對象能夠儲存所有的泛型:
ArrayList<Object> list = new ArrayList<String>();
但可以的是編譯器提示報錯:
明明 String是Object類的子類,我們可以發現,泛型不存在繼承、多態關系,泛型左右兩邊要一樣別擔心,JDK提供瞭通配符?來應對這種場景,我們可以這樣:
ArrayList<?> list = new ArrayList<String>(); list = new ArrayList<Integer>();
通配符<?>表示可以接收任意類型,此處?是類型實參,而不是類型形參。我們可以把它看做是String、Integer等所有類型的"父類"。是一種真實的類型。通配符還有:
- 上邊界限定通配符,如<? extends E>;
- 下邊界通配符,如<? super E>;
?:無界通配符
?是開放限度最大的,可指向任意類型,但在對於其的存取上也是限制最大的:
- 入參和泛型相關的都不能使用, 除瞭null(禁止存入),比如ArrayList<?> list不可以添加任何類型,因為並不知道實際是哪種類型
- 返回值和泛型相關的都隻能用Object接收
extends 上邊界通配符
//泛型的上限隻能是該類型的類型及其子類,其中Number是Integer、Long、Float的父類 ArrayList<? extends Number> list = new ArrayList<Integer>(); ArrayList<? extends Number> list2 = new ArrayList<Long>(); ArrayList<? extends Number> list3 = new ArrayList<Float>(); list.add(1);//報錯,extends不允許存入 ArrayList<Long> longList = new ArrayList<>(); longList.add(1L); list = longList;//由於extends不允許存入,list隻能重新指向longList Number number = list.get(0); // extends 取出來的元素(Integer,Long,Float)都可以轉Number
extends指向性被砍瞭一半,隻能指向子類型和父類型,但方法使用上又適當放開瞭:
- 值得註意的是:這裡的extends並不表示類的繼承含義,隻是表示泛型的范圍關系
- extends不允許存入,由於使用extends ,比如
ArrayList<? extends Number> list
可以接收Integer、Long、Float,但是泛型本質是保證兩邊類型確定,這樣的話在程序運行期間,再存入數據,編譯器可無法知曉數據的類型,所以隻能禁止瞭。 - 但為什麼
ArrayList<? extends Number> list
可以重新指向longList
來變向地"存儲"值,那是因為ArrayList<Long> longList = new ArrayList<>();
這邊的泛型已經約束兩邊的類型瞭,編譯器知曉longList
儲存的數據都是Long類型
- 但extends允許取出,取出來的元素可以往邊界類型轉
- extends中可以指定多個范圍,實行泛型類型檢查約束時,會以最左邊的為準。
super 下邊界通配符
//泛型的下限隻能是該類型的類型及其父類,其中Number是Integer、Long、Float的父類 ArrayList<? super Integer> list = new ArrayList<Integer>(); ArrayList<? super Integer> list2 = new ArrayList<Number>(); ArrayList<? super Integer> list3 = new ArrayList<Long>();//報錯 ArrayList<? super Integer> list4 = new ArrayList<Float>();//報錯 list2.add(123);//super可以存入,隻能存Integer及其子類型元素 Object aa = list2.get(0);//super可以取出,類型隻能是Object
super允許存入編輯類型及其子類型元素,但取出元素隻能為Object類型
PECS原則
泛型通配符的出現,是為瞭獲得最大限度的靈活性。如果要用到通配符,需要結合業務考慮,《Effective Java》提出瞭:PECS(Producer Extends Consumer Super)
- 需要頻繁往外讀取內容(生產者Producer),適合用<? extends T>
- 需要頻繁寫值(消費者Consumer),適合用<? super T>:super允許存入子類型元素
?
表示不確定的 java 類型,一般用於隻接收任意類型,而不對其處理的情況
泛型是怎麼擦除的
Java 編譯器通過如下方式實現擦除:
- 用 Object 或者界定類型替代泛型,產生的字節碼中隻包含瞭原始的類,接口和方法;
- 在恰當的位置插入強制轉換代碼來確保類型安全;
- 在繼承瞭泛型類或接口的類中自動產生橋接方法來保留多態性。
擦除類定義中的無限制類型參數
當類定義中的類型參數沒有任何限制時,在類型擦除中直接被替換為Object,即形如和<?>的類型參數都被替換為Object
擦除類定義中的有限制類型擦除
當類定義中的類型參數存在限制(上下界)時,在類型擦除中替換為類型參數的上界或者下界,
形如
<T extends Number>
和<? extends Number>
的類型參數被替換為Number,<? super Number>
被替換為Object
擦除方法定義中的類型參數
擦除方法定義中的類型參數原則和擦除類定義中的類型參數是一樣的,額外補充 擦除方法定義中的有限制類型參數的例子
橋接方法和泛型的多態
public class A<T>{ public T get(T a){ //進行一些操作 return a; } } public class B extends A<String>{ @override public String get(String a){ //進行一些操作 return a; } }
由於類型擦出機制的存在,按理說編譯後的文件在翻譯為java應如下所示:
public class A{ public Object get(Object a){ //進行一些操作 return a; } } public class B extends A{ @override public String get(String a){ //進行一些操作 return a; } }
但是,我們可以發現 @override
意味著B對父類A中的get方法
進行瞭重寫,但是依上面的程序來看,隻是重載,依然可以執行父類的方法,這和期望是不附的,也不符合java繼承、多態的特性。
重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫, 返回值和形參都不能改變。即外殼不變,核心重寫!
重載(overloading) 是在一個類裡面,方法名字相同,而參數不同。返回類型可以相同也可以不同。
為瞭解決這個問題,java在編譯期間加入瞭橋接方法。編譯後再翻譯為java原文件其實是:
public class A{ public Object get(Object a){ //進行一些操作 return a; } } public class B extends A{ @override public String get(String a){ //進行一些操作 return a; } //橋接方法!!! public Object get(Object a){ return get((String)a) } }
橋接方法重寫瞭父類相同的方法,並且橋接方法中,最終調用瞭期望的重寫方法,並且橋接方法在調用目的方法時,參數被強制轉換為指定的泛型類型。橋接方法搭起瞭父類和子類的橋梁。
橋接方法是伴隨泛型方法而生的,在繼承關系中,如果某個子類覆蓋瞭泛型方法,則編譯器會在該子類自動生成橋接方法。所以我們實際使用泛型的過程中,無需擔心橋接方法。
泛型擦除帶來的限制與局限
泛型不適用基本數據類型
不能用類型參數代替基本類型(byte 、short 、int 、long、float 、 double、char、boolean)
比如, 沒有 Pair<double>, 隻 有 Pair<Double>
。 其原因是泛型擦除,擦除之後隻有原始類型Object, 而 Object 無法存儲 double等基本類型的值。
但Java同時有自動拆裝箱特性,可以將基本類型裝箱成包裝類型,這樣就使用泛型瞭,通過中轉,即可在功能上實現“用基本類型實例化類型化參數”。
數據類型 | 封裝類 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
無法創建具體類型的泛型數組
List<Integer>[] l1 = new ArrayList<Integer>[10];// Error List<String>[] l2 = new ArrayList<String>[10];// Error
上文我們知曉ArrayList,底層仍舊采用Object[]
,Integer,String
類型信息都被擦除
借助無限定通配符 ?
,可以創建泛型數組,但是涉及的操作都基本上與類型無關
List<?>[] l1 = new ArrayList<?>[10];
如果想對數組進行復制操作的話,可以通過Arrays.copyOfRange()
方法
public class TestArray { public static void main(String[] args) { Integer[] array = new Integer[]{2, 3, 1}; Integer[] arrNew = copy(array); } private static <E> E[] copy(E[] array) { return Arrays.copyOfRange(array, 0, array.length); } }
反射其實可以繞過泛型的限制
由於我們知曉java是通過泛型擦除來實現泛型的,JVM隻能識別原始類型Object,所以我們隻需騙過編譯器的校驗即可,反射是程序運行時發生的,我們可以借助反射來波騷操作
List<Integer> l1 = new ArrayList<>(); l1.add(111); //l1.add("騷氣的我"); // 泛型會報錯 try { Method method = l1.getClass().getDeclaredMethod("add",Object.class); method.invoke(l1,"騷氣的我 又出現瞭"); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } for ( Object o: l1){ System.out.println(o); }
結果:
111
騷氣的我 又出現瞭
以上就是一文帶你深入瞭解Java泛型的詳細內容,更多關於Java泛型的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Java泛型在集合使用與自定義及繼承上的體現和通配符的使用
- Java泛型常見面試題(面試必問)
- Java 泛型詳解(超詳細的java泛型方法解析)
- 一篇文章帶你入門java泛型
- Java的類型擦除式泛型詳解