大綱
一、為什么需要泛型?泛型的優(yōu)點
二、泛型定義
三、限定"類型變量"
四、泛型中的約束和局限性
五、泛型類型的繼承規(guī)則和通配符類型
六、虛擬機是如何實現(xiàn)泛型的?-類型擦除
七、類型擦除與多態(tài)的沖突和解決方法
一、 為什么需要泛型?泛型的優(yōu)點
適用于多種類型執(zhí)行相同的代碼
比如int相加 float相加 可以抽取出一個泛型方法
public static <T> T add(T x, T y) {
}
- 比如SharedPreference set get操作 對于不同類型 可以抽取一個公用的方法
public static <T> T getPrefValue(@NonNull SharedPreferences pref, @NonNull String key, @NonNull T t) {
Objects.requireNonNull(pref);
Objects.requireNonNull(key);
Objects.requireNonNull(t);
if (t instanceof String) {
String str = pref.getString(key, (String) t);
t = (T) str;
} else if (t instanceof Integer) {
Integer in = pref.getInt(key, (Integer) t);
t = (T) in;
} else if (t instanceof Long) {
Long lon = pref.getLong(key, (Long) t);
t = (T) lon;
} else if (t instanceof Float) {
Float fl = pref.getFloat(key, (Float) t);
t = (T) fl;
} else if (t instanceof Boolean) {
Boolean bl = pref.getBoolean(key, (Boolean) t);
t = (T) bl;
} else if (t instanceof Set) {
t = (T) pref.getStringSet(key, (Set<String>) t);
} else {
throw new IllegalArgumentException("getPrefValue fail ! Value Type not supported");
}
return t;
}
- 指定限制的類型,插入錯誤的數(shù)據(jù)類型,能夠在編譯期間就發(fā)現(xiàn)錯誤。不至于在運行時才發(fā)現(xiàn)異常。
List list = new ArrayList<>();
list.add("hello");
list.add("world");
list.add(800);
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
運行時拋出異常:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
二、泛型定義
-
泛型類和泛型接口
在類名或接口后加一個<T>
public class NormalGeneric<T> {
private T mData;
}
public interface IGeneric<T> {
T getData();
}
//也可聲明多個泛型
public class MultiGeneric<K, V> {
private K mKey;
private V mValue;
public void setKeyValue(K key, V value) {
mKey = key;
mValue = value;
}
}
- 泛型方法
- 在返回值前用尖括號聲明泛型 同樣可定義多個
public <T> T getZ(T t) {
//泛型方法
return null;
}
publid <K,V> void setKeyValue(K key,V value){
//todo
}
注意泛型方法
public T get(T t){}//注意 這不是一個泛型方法 這里的T 沒有在方法中聲明,是屬于類聲明的泛型。
public <T> T get(){}//這才是一個泛型方法
- 泛型類以及其內部泛型方法同時聲明泛型T,但泛型方法的T可以是個全新的類型,可與泛型類中的聲明T不是一個類型。
public class GenericType<T> {
private T mData;
//備注 T可以是全新的類型,與類聲明的T不沖突
public <T> void test(T t) {
}
}
三、限定"類型變量"
對傳入的方法做限定。(看圖示理解)
支持多個限定,T 可以繼承類也可以實現(xiàn)接口,但只能有一個類,并要放在最前面,后面的接口用&分割
public <K, V extends ArrayList & Comparable & Iterable> void set(K k, V value) {
}
ArrayList是類,Comparable和Iterable是接口
ArrayList是類,Comparable和Iterable是接口
四、泛型中的約束和局限性
編譯器強制規(guī)定:
- 不能實例化類型變量 new T() 不可以
- 靜態(tài)域或者方法里不能引用類型變量
privete state T getInstance();//不可以
為什么?泛型的類型在對象創(chuàng)建時,才知道具體的類型。而static在類加載就被執(zhí)行了 在構造方法之前,所以這時還不知道具體類型,但如果靜態(tài)方法本身就是一個泛型方法就可以。
public static <T> T getInstance(){
}//可行
- 泛型不能用instance of
- 由于泛型擦除導致判斷類名一致等(詳情看底部類型擦除的詳情介紹)
- 可以定義泛型數(shù)組,但不能給泛型數(shù)組初始化
- 泛型類不能繼承Exception或Throwable,不能捕獲泛型類對象
- 泛型類型變量不能是基本數(shù)據(jù)類型 比如GenericType<double>是不行的
五、泛型類型的繼承規(guī)則和通配符類型
定義三個類:子類Apple繼承父類Fruit Fruit類繼承Food 三個類是這樣一個關系
public class GenericType<T> {
private T mData;
//省略 get set方法
}
盡管Apple繼承父類Fruit ,但注意 GenericType<Apple> 和 GenericType<Fruit> 之間沒有繼承關系。所以如果想讓其有繼承關系,引入--通配符
通配符--使用方法時定義
//apple-fruit-food
GenericType<Fruit> fruitGenericType = new GenericType<>();
GenericType<Apple> appleGenericType = new GenericType<>();
GenericType<Food> foodGenericType=new GenericType<>();
print(appleGenericType);//不可以,因為沒有繼承關系
print2(appleGenericType);//可以 因為 ? extends Fruit 限定Fruit的子類都可以
print2(foodGenericType);//不可以 food是fruit的父類
print3(foodGenericType);//可以 ? super Fruit 限定Fruit的父類都可以
public static void print(GenericType<Fruit> type) {
}
public static void print2(GenericType<? extends Fruit> type) {
}
public static void print3(GenericType<? super Fruit> type) {
}
extend --規(guī)定了傳入?yún)?shù)的訪問上限 主要用于安全的“訪問”數(shù)據(jù)。不能set
GenericType<? extends Fruit> type=new GeneicType<>();
type.setData();// 不可以
Fruit fruit=type.getData();//可以
GenericType<? super Fruit> type2=new GenericType<>();
type.setData(new Fruit());//可
type.setData(new Apply());//可
type.setData(new Food())//不可以 需要限定Fruit的子類!
type.getData();//可以 但返回類型只能是Object
super --規(guī)定了傳入?yún)?shù)的下限,主要用于安全的寫入數(shù)據(jù),寫入數(shù)據(jù)限定在x的子類
六、虛擬機是如何實現(xiàn)泛型的?-類型擦除
- 在編譯時,類型擦除,會用一個原生類型代替泛型T
比如<T> 會將定義的T替換成Object
但如果<T extends ArrayList> 將T替換為ArrayList
即有限定類型用限定類型(第一個邊界)替換,無限定類型用Object替換
生成字節(jié)碼時,里面是不包含泛型具體對象的,比如List<String> List<Integer>都要被轉化為List
public class GenericType<T>{
private T mData;
}
//查看.class字節(jié)碼文件為
public class GenericType<Object>{
private Object mData;
}
- 在調用泛型方法時,可以指定泛型,也可以不指定泛型
- 在不指定泛型的情況下,泛型變量的類型為該方法中的幾種類型的同一父類的最小級,直到Object
- 在指定泛型的情況下,該方法的幾種類型必須是該泛型的實例的類型或者其子類
public class Test {
public static void main(String[] args) {
/**不指定泛型的時候*/
int i = Test.add(1, 2); //這兩個參數(shù)都是Integer,所以T為Integer類型
Number f = Test.add(1, 1.2); //這兩個參數(shù)一個是Integer,一個是Float,所以取同一父類的最小級,為Number
Object o = Test.add(1, "asd"); //這兩個參數(shù)一個是Integer,一個是String,所以取同一父類的最小級,為Object
/**指定泛型的時候*/
int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能為Integer類型或者其子類
int b = Test.<Integer>add(1, 2.2); //編譯錯誤,指定了Integer,不能為Float
Number c = Test.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float
}
//這是一個簡單的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
- 證明泛型被擦除:
驗證1:
ImplGeneric<String> stringImplGeneric = new ImplGeneric<>();
ImplGeneric<Integer> integerImplGeneric = new ImplGeneric<>();
final Class stringImplGenericClass = stringImplGeneric.getClass();
final Class integerImplGenericClass = integerImplGeneric.getClass();
boolean equal = stringImplGenericClass == integerImplGenericClass;
System.out.println(equal);
輸出true
ImplGeneric<String> 和ImplGeneric<Integer>打印其class文件 發(fā)現(xiàn)相同,如果打印其className,最終都是ImplGeneric類
驗證2:
重載--需要保證方法名相同,參數(shù)不同,但以上圖來看,說明List<String> 和 List<Integer> 相同,因為類型擦除,擦除后都是List,所以編譯器編譯不通過的。所以不符合重載,編譯不通過。
驗證3:
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //這樣調用 add 方法只能存儲整形,因為泛型類型的實例為 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
在程序中定義了一個ArrayList泛型類型實例化為Integer對象,如果直接調用add()方法,那么只能存儲整數(shù)數(shù)據(jù),不過當我們利用反射調用add()方法的時候,卻可以存儲字符串,這說明了Integer泛型實例在編譯之后被擦除掉了,只保留了原始類型。
七、類型擦除與多態(tài)的沖突和解決方法
父類
public class GenericType<T> {
private T mData;
public T getData() {
return mData;
}
public void setData(T data) {
mData = data;
}
}
子類
public class IntegerType extends GenericType<Integer> {
private Integer mData;
@Override
public Integer getData() {
return 100;
}
@Override
public void setData(Integer data) {
mData = data;
}
}
看起來是正常的,子類限定泛型類型為Integer,重寫了get set方法
但要知道父類類型擦除后原生類型Object代替了T那么將變成
public class GenericType<Object> {
private Object mData;
public Object getData() {
return mData;
}
public void setData(Object data) {
mData = data;
}
}
父類的setData(Object data) 子類為setData(Integer data),這樣看并不符合重寫規(guī)則,因為重寫是要求父類和子類方法參數(shù)一致的?類型擦除和多態(tài)特性有了沖突:
本意是將IntegerType變?yōu)檫@樣
public class IntegerType {
private Integer mData;
@Override
public Integer getData() {
return 100;
}
@Override
public void setData(Integer data) {
mData = data;
}
}
正常編譯器做不到,只能變?yōu)镺bject。但為了實現(xiàn)這個需求,JVM做了特殊優(yōu)化,通過使用橋方法。
反編譯IntegerType.class的字節(jié)碼文件(javap -c IntegerType.class 命令 ),結果如下:
public class IntegerType extends GenericType<java.lang.Integer> {
public com.study.java.generic.IntegerType();
Code:
0: aload_0
1: invokespecial #1 // Method com/study/java/generic/GenericType."<init>":()V
4: return
public java.lang.Integer getData();
Code:
0: bipush 100
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: areturn
public void setData(java.lang.Integer);
Code:
0: aload_0
1: aload_1
2: putfield #3 // Field mData:Ljava/lang/Integer;
5: return
public void setData(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #4 // class java/lang/Integer
5: invokevirtual #5 // Method setData:(Ljava/lang/Integer;)V
8: return
public java.lang.Object getData();
Code:
0: aload_0
1: invokevirtual #6 // Method getData:()Ljava/lang/Integer;
4: areturn
}
可以發(fā)現(xiàn)除了我們已知的setData(java.lang.Integer)和java.lang.Integer getData()還有兩個JVM生成的兩個橋方法setData(java.lang.Object)和java.lang.Object getData()。在setData(java.lang.Object)里第25行,實際調用的是setData(java.lang.Integer) 在java.lang.Object getData()里的第32行,實際調用的Integer getData()。這就是橋方法。橋方法的內部實現(xiàn),就只是去調用我們自己重寫的那兩個方法。
虛擬機巧妙的使用了橋方法,來解決了類型擦除和多態(tài)的沖突。
這時候又會有一個疑問,如圖編譯器提示,在一個類中,已經(jīng)重寫了getData[返回值為Integer],此時再加一個getData[返回值為Object],在常規(guī)編程中是不允許的,不能通過編譯器檢查的。因為在編譯時我們判斷一個方法是否相同主要是判斷方法名和參數(shù),但!虛擬機內部判斷方法是否相同是判斷方法名 參數(shù),外加返回值。所以編譯器為了實現(xiàn)泛型的多態(tài)允許自己做這個看起來“不合法”的事情,然后交給虛擬器去區(qū)別。
八、其他
-
既然說類型變量會在編譯的時候擦除掉,那為什么我們往 ArrayList<String>添加int值會錯誤呢?
Java編譯器是通過先檢查代碼中泛型的類型,然后在進行類型擦除,再進行編譯。
既然都被替換為原始類型,那么為什么我們在獲取的時候,不需要進行強制類型轉換呢?
//看一下ArrayList.get()方法
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
在獲取時會根據(jù)泛型類型做一個強制類型轉化
- 注意,在虛擬機里,泛型信息雖然擦除,但會保留在Signature。
- 為什么要擦除?jdk1.5之后加入的泛型,為了兼容之前的版本,才要做擦除。
- .......有遇到的新知識點繼續(xù)補充