大綱
一、為什么需要泛型趣竣?泛型的優(yōu)點(diǎn)
二摇庙、泛型定義
三、限定"類型變量"
四遥缕、泛型中的約束和局限性
五卫袒、泛型類型的繼承規(guī)則和通配符類型
六、虛擬機(jī)是如何實(shí)現(xiàn)泛型的单匣?-類型擦除
七夕凝、類型擦除與多態(tài)的沖突和解決方法
一、 為什么需要泛型户秤?泛型的優(yōu)點(diǎn)
適用于多種類型執(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)錯誤虎忌。不至于在運(yù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));
}
運(yùn)行時拋出異常: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(){}//這才是一個泛型方法
- 泛型類以及其內(nèi)部泛型方法同時聲明泛型T,但泛型方法的T可以是個全新的類型,可與泛型類中的聲明T不是一個類型挑围。
public class GenericType<T> {
private T mData;
//備注 T可以是全新的類型礁竞,與類聲明的T不沖突
public <T> void test(T t) {
}
}
三、限定"類型變量"
對傳入的方法做限定杉辙。(看圖示理解)
支持多個限定模捂,T 可以繼承類也可以實(shí)現(xiàn)接口,但只能有一個類蜘矢,并要放在最前面狂男,后面的接口用&分割
public <K, V extends ArrayList & Comparable & Iterable> void set(K k, V value) {
}
ArrayList是類,Comparable和Iterable是接口
ArrayList是類品腹,Comparable和Iterable是接口
四岖食、泛型中的約束和局限性
編譯器強(qiáng)制規(guī)定:
- 不能實(shí)例化類型變量 new T() 不可以
- 靜態(tài)域或者方法里不能引用類型變量
privete state T getInstance();//不可以
為什么?泛型的類型在對象創(chuàng)建時舞吭,才知道具體的類型泡垃。而static在類加載就被執(zhí)行了 在構(gòu)造方法之前,所以這時還不知道具體類型羡鸥,但如果靜態(tài)方法本身就是一個泛型方法就可以蔑穴。
public static <T> T getInstance(){
}//可行
- 泛型不能用instance of
- 由于泛型擦除導(dǎo)致判斷類名一致等(詳情看底部類型擦除的詳情介紹)
- 可以定義泛型數(shù)組,但不能給泛型數(shù)組初始化
- 泛型類不能繼承Exception或Throwable惧浴,不能捕獲泛型類對象
- 泛型類型變量不能是基本數(shù)據(jù)類型 比如GenericType<double>是不行的
五存和、泛型類型的繼承規(guī)則和通配符類型
定義三個類:子類Apple繼承父類Fruit Fruit類繼承Food 三個類是這樣一個關(guān)系
public class GenericType<T> {
private T mData;
//省略 get set方法
}
盡管Apple繼承父類Fruit ,但注意 GenericType<Apple> 和 GenericType<Fruit> 之間沒有繼承關(guān)系衷旅。所以如果想讓其有繼承關(guān)系哑姚,引入--通配符
通配符--使用方法時定義
//apple-fruit-food
GenericType<Fruit> fruitGenericType = new GenericType<>();
GenericType<Apple> appleGenericType = new GenericType<>();
GenericType<Food> foodGenericType=new GenericType<>();
print(appleGenericType);//不可以,因?yàn)闆]有繼承關(guān)系
print2(appleGenericType);//可以 因?yàn)?? 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的子類
六、虛擬機(jī)是如何實(shí)現(xiàn)泛型的九串?-類型擦除
- 在編譯時绞佩,類型擦除,會用一個原生類型代替泛型T
比如<T> 會將定義的T替換成Object
但如果<T extends ArrayList> 將T替換為ArrayList
即有限定類型用限定類型(第一個邊界)替換猪钮,無限定類型用Object替換
生成字節(jié)碼時品山,里面是不包含泛型具體對象的,比如List<String> List<Integer>都要被轉(zhuǎn)化為List
public class GenericType<T>{
private T mData;
}
//查看.class字節(jié)碼文件為
public class GenericType<Object>{
private Object mData;
}
- 在調(diào)用泛型方法時烤低,可以指定泛型肘交,也可以不指定泛型
- 在不指定泛型的情況下,泛型變量的類型為該方法中的幾種類型的同一父類的最小級扑馁,直到Object
- 在指定泛型的情況下涯呻,該方法的幾種類型必須是該泛型的實(shí)例的類型或者其子類
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;
}
}
- 證明泛型被擦除:
驗(yàn)證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類
驗(yàn)證2:
重載--需要保證方法名相同褐荷,參數(shù)不同,但以上圖來看嘹悼,說明List<String> 和 List<Integer> 相同叛甫,因?yàn)轭愋筒脸脸蠖际荓ist,所以編譯器編譯不通過的杨伙。所以不符合重載其监,編譯不通過。
驗(yàn)證3:
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //這樣調(diào)用 add 方法只能存儲整形限匣,因?yàn)榉盒皖愋偷膶?shí)例為 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泛型類型實(shí)例化為Integer對象抖苦,如果直接調(diào)用add()方法,那么只能存儲整數(shù)數(shù)據(jù)米死,不過當(dāng)我們利用反射調(diào)用add()方法的時候锌历,卻可以存儲字符串,這說明了Integer泛型實(shí)例在編譯之后被擦除掉了峦筒,只保留了原始類型究西。
七、類型擦除與多態(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ī)則峦失,因?yàn)橹貙懯且蟾割惡妥宇惙椒▍?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尉辑。但為了實(shí)現(xiàn)這個需求帆精,JVM做了特殊優(yōu)化,通過使用橋方法。
反編譯IntegerType.class的字節(jié)碼文件(javap -c IntegerType.class 命令 )卓练,結(jié)果如下:
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行,實(shí)際調(diào)用的是setData(java.lang.Integer) 在java.lang.Object getData()里的第32行昆庇,實(shí)際調(diào)用的Integer getData()。這就是橋方法闸溃。橋方法的內(nèi)部實(shí)現(xiàn)整吆,就只是去調(diào)用我們自己重寫的那兩個方法。
虛擬機(jī)巧妙的使用了橋方法辉川,來解決了類型擦除和多態(tài)的沖突表蝙。
這時候又會有一個疑問炉菲,如圖編譯器提示蕴侧,在一個類中,已經(jīng)重寫了getData[返回值為Integer]泳赋,此時再加一個getData[返回值為Object]屿愚,在常規(guī)編程中是不允許的汇跨,不能通過編譯器檢查的。因?yàn)樵诰幾g時我們判斷一個方法是否相同主要是判斷方法名和參數(shù)妆距,但穷遂!虛擬機(jī)內(nèi)部判斷方法是否相同是判斷方法名 參數(shù),外加返回值娱据。所以編譯器為了實(shí)現(xiàn)泛型的多態(tài)允許自己做這個看起來“不合法”的事情蚪黑,然后交給虛擬器去區(qū)別。
八中剩、其他
-
既然說類型變量會在編譯的時候擦除掉忌穿,那為什么我們往 ArrayList<String>添加int值會錯誤呢?
Java編譯器是通過先檢查代碼中泛型的類型,然后在進(jìn)行類型擦除结啼,再進(jìn)行編譯掠剑。
既然都被替換為原始類型,那么為什么我們在獲取的時候郊愧,不需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換呢澡腾?
//看一下ArrayList.get()方法
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
在獲取時會根據(jù)泛型類型做一個強(qiáng)制類型轉(zhuǎn)化
- 注意,在虛擬機(jī)里糕珊,泛型信息雖然擦除动分,但會保留在Signature。
- 為什么要擦除红选?jdk1.5之后加入的泛型澜公,為了兼容之前的版本,才要做擦除。
- .......有遇到的新知識點(diǎn)繼續(xù)補(bǔ)充