本文是本人學習《JavaSE學習筆記 - 泛型進階》這篇文章的筆記,記錄自己學習理解的過程,主要是作爲備忘。建議大家直接看《JavaSE學習筆記 - 泛型進階》這篇文章原文後再來看本文。
原生類型
使用泛型的時候不指定類型實參,創建出來的類型就是原生類型
ArrayList rawList = new ArrayList();
原生類型與Object
參數化類型的區別
- 前者是逃避了類型檢查,而後者則明確告知編譯器它持有的是任意類型。
- 前者是所有參數化類型的父類型,而後者並不能作爲所有參數化類型的父類型。
List list = new ArrayList<String>();
list = new ArrayList<Integer>();
List<Object> list2 = new ArrayList<Object>();
list2 = new ArrayList<String>(); // 報錯,需明確指定爲Object類型
類型擦除
你會發現無法用new T()來創建一個對象
public class GenericHolder<T> {
// Cannot create a generic array of T
private T[] array = new T[10];
}
由以下代碼可得出結論使用多種類型實參參數化同一泛型創建出對應的參數化類型時,這些參數化類型實際上都是同一種類型,即是它們共享同一份字節碼
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);
System.out.println(c1.getName());
System.out.println(c2.getName());
}
}
// 輸出結果
true
java.util.ArrayList
java.util.ArrayList
Java泛型是僞泛型,類型實參的類型信息在編譯的過程中會被擦除掉,而這個過程就是類型擦除,在編譯後的字節碼文件中,所有泛型類型都已經替換爲對應的原生類型,並在相應的地方插入了強制轉換。所以在運行時,所有泛型的類型信息對於JVM是不可見的。更通俗直白地講,類型擦除就是將泛型代碼轉換爲非泛型代碼的過程。
如下一個普通泛型類
public class Holder<T> {
private T object;
public void set(T object) {
this.object = object;
}
public T get() {
return object;
}
public static void main(String[] args) {
Holder<String> holder = new Holder<String>();
holder.set("MakwaN");
String value = holder.get();
System.out.println("value = " + value);
}
}
命令行中運行該命令javap -c -v -p Holder>bytecode.txt查看字節碼,我是用Android Studio的ASM Bytecode Viewer插件查看的
- private Ljava/lang/Object; object:變量聲明類型擦除成Object
- public set(Ljava/lang/Object;)V:set方法參數擦除成Object
- public get()Ljava/lang/Object;:get方法返回值擦除成Object
main()方法字節碼
- INVOKEVIRTUAL com/himmy/java/test/Holder.set (Ljava/lang/Object;)V:調用set()方法,參數類型爲Object
- INVOKEVIRTUAL com/himmy/java/test/Holder.get ()Ljava/lang/Object;:調動get()犯法,返回值類型爲Object
- CHECKCAST java/lang/String:轉換成String類型
字節碼語法不懂沒關係,只要看這些關鍵的地方大概就會明白,所有的泛型都被擦除成Object,只是在使用的地方轉換成具體的類型
準確的說,應該說泛型的類型形參將擦除到它被限制的第一個邊界所代表的類型,因爲未設置邊界,所以默認爲Object
// class version 51.0 (51)
// access flags 0x21
// signature <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: com/himmy/java/test/Holder<T>
public class com/himmy/java/test/Holder {
// compiled from: Holder.java
// access flags 0x2
// signature TT;
// declaration: object extends T
private Ljava/lang/Object; object
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/himmy/java/test/Holder; L0 L1 0
// signature Lcom/himmy/java/test/Holder<TT;>;
// declaration: this extends com.himmy.java.test.Holder<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature (TT;)V
// declaration: void set(T)
public set(Ljava/lang/Object;)V
L0
LINENUMBER 7 L0
ALOAD 0
ALOAD 1
PUTFIELD com/himmy/java/test/Holder.object : Ljava/lang/Object;
L1
LINENUMBER 8 L1
RETURN
L2
LOCALVARIABLE this Lcom/himmy/java/test/Holder; L0 L2 0
// signature Lcom/himmy/java/test/Holder<TT;>;
// declaration: this extends com.himmy.java.test.Holder<T>
LOCALVARIABLE object Ljava/lang/Object; L0 L2 1
// signature TT;
// declaration: object extends T
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1
// signature ()TT;
// declaration: T get()
public get()Ljava/lang/Object;
L0
LINENUMBER 11 L0
ALOAD 0
GETFIELD com/himmy/java/test/Holder.object : Ljava/lang/Object;
ARETURN
L1
LOCALVARIABLE this Lcom/himmy/java/test/Holder; L0 L1 0
// signature Lcom/himmy/java/test/Holder<TT;>;
// declaration: this extends com.himmy.java.test.Holder<T>
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 15 L0
NEW com/himmy/java/test/Holder
DUP
INVOKESPECIAL com/himmy/java/test/Holder.<init> ()V
ASTORE 1
L1
LINENUMBER 16 L1
ALOAD 1
LDC "MakwaN"
INVOKEVIRTUAL com/himmy/java/test/Holder.set (Ljava/lang/Object;)V
L2
LINENUMBER 17 L2
ALOAD 1
INVOKEVIRTUAL com/himmy/java/test/Holder.get ()Ljava/lang/Object;
CHECKCAST java/lang/String
ASTORE 2
L3
LINENUMBER 18 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "value = "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 19 L4
RETURN
L5
LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
LOCALVARIABLE holder Lcom/himmy/java/test/Holder; L1 L5 1
// signature Lcom/himmy/java/test/Holder<Ljava/lang/String;>;
// declaration: holder extends com.himmy.java.test.Holder<java.lang.String>
LOCALVARIABLE value Ljava/lang/String; L3 L5 2
MAXSTACK = 3
MAXLOCALS = 3
}
如果非要在泛型中創建實例呢?可以通過Class<T>實現,而且比較麻煩,還要傳入Class類型,且僅適用含無參構造函數的類
public class Holder<T> {
private T object;
private Class<T> clz;
public Holder(Class<T> clz) {
this.clz = clz;
}
public T get() {
try {
object = clz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return object;
}
public static void main(String[] args) {
Holder<String> holder = new Holder<>(String.class);
String s = holder.get();
System.out.println(s.getClass().getSimpleName());
}
}
創建泛型數組
下面這種寫法是行不通的
T[] array = new T[10];
還是需要Class<T>來實現,配合Array.newInstance()方法
T[] array = (T[]) Array.newInstance(class, length);
instanceof判斷的問題
與常見泛型對象或者泛型數組一樣,還是要依靠Class
這樣是不行噠
obj instanceof T;
這樣子是ok噠
clz.isInstance(obj);
邊界
設置邊界的語法爲<T extends TypeA & TypeB & TypeC>,可以設置多個邊界,第一個位置可以爲類或者接口,之後的必須爲接口,類似於java繼承規則的單繼承多接口
對於單邊界,泛型實參的範圍爲邊界類型或者其子類型
對於多邊界,泛型實參分爲所有邊界類型及所有邊界類型的共同子集。比如泛型類所限制的邊界是T extends A & B & C
,那麼類型實參必須同時爲類型A
、B
、C
的子類型纔行。
泛型的邊界可以限制泛型形參爲某個類型的子集,並且在泛型類內可以使用邊界類的方法,如下代碼可以直接使用length()方法,因爲泛型形參的範圍是String或者String的子類
public class Holder<T extends String> {
private T t;
public int length() {
return t.length();
}
}
默認邊界,對於<T>未設置邊界的情況,其默認邊界爲Object,相當於<T extends Object>
關於多邊界情況下的類型擦除,有如下結論,可結合字節碼驗證結論
- 類型形參將擦除到它被限制的第一個邊界所代表的類型,即編譯後只用第一個邊界所代表的類型替換掉類型形參。
- 當代碼涉及到除第一個邊界之外其他邊界所代表的類型時,編譯器會爲其對應的邊界所代表的類型進行強制類型轉換,並檢查轉型是否成功。
還需要注意的是泛型邊界中只能用extends,super關鍵字是在通配符上下限中使用的,後面會介紹
泛型邊界是用在泛型形參上,而泛型統配符?的上下限extends、super是用在泛型實參上,這點需要注意下
當然,泛型邊界也可以應用再泛型方法上
static <T extends String & Runnable> void fun(T t) {
System.out.println(t.length());
}