目錄
- 什么是Java泛型
- 泛型的使用
- 泛型類
- 泛型介面
- 泛型方法
- 泛型的底層實作機制
- ArrayList原始碼決議
- 什么是泛型擦除
- 泛型的邊界
- ?:無界通配符
- extends 上邊界通配符
- super 下邊界通配符
- PECS原則
- 泛型是怎么擦除的
- 擦除類定義中的無限制型別引數
- 擦除類定義中的有限制型別擦除
- 擦除方法定義中的型別引數
- 橋接方法和泛型的多型
- 泛型擦除帶來的限制與局限
- 泛型不適用基本資料型別
- 無法創建具體型別的泛型陣列
- 反射其實可以繞過泛型的限制
- 尾語
作者:小牛呼嚕嚕 | https://xiaoniuhululu.com
計算機內功、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
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,這樣自然而然就相等了,這種機制就叫泛型擦除,
我們需要了解一下類加載生命周期:

詳情見:https://mp.weixin.qq.com/s/v91bqRiKDWWgeNl1DIdaDQ
泛型是和編譯器的約定,在編譯期對代碼進行檢查的,由編譯器負責決議,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,即形如

擦除類定義中的有限制型別擦除
當類定義中的型別引數存在限制(上下界)時,在型別擦除中替換為型別引數的上界或者下界,
形如
<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
騷氣的我 又出現了

尾語
如果你了解其他語言(例如 C++ )的引數化機制,你會發現,Java 泛型并不能滿足所有的預期,由于泛型出來前,java已經有了很多專案了,為了兼容老版本,采用了泛型擦除來“實作泛型”,這會遇到很多意料之外的麻煩,但這并不是說 Java 泛型毫無用處,它大多數情況能夠讓代碼更加優雅,后面有機會我們會繼續深入聊聊泛型擦除帶來的麻煩及其歷史淵源,
參考資料:
《On Java8》
《Effective Java》
https://www.liaoxuefeng.com/wiki/1252599548343744/1265102638843296
https://www.cnblogs.com/mahuan2/p/6073493.html
本篇文章到這里就結束啦,很感謝你能看到最后,如果覺得文章對你有幫助,別忘記關注我!更多精彩的文章

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/502095.html
標籤:Java
