泛型的概念
泛型的魅力在于:程序具有更好的可讀性(避免出現(xiàn)過多的強制轉(zhuǎn)化)和安全性(可以很清晰的看出類型,避免存取不一致導致的錯誤)
同時泛型又是一個編譯器的概念,虛擬機沒有泛型類型的對象,只有普通類和方法
泛型的使用
泛型不僅可以定義在泛型類上,也可以在普通類里面定義泛型方法,類型變量\<T>要放到修飾符后面,返回參數(shù)前面
對于泛型方法,調(diào)用的時候可以根據(jù)參數(shù)省略掉類型變量,如:
ArrayAlg.getMiddle("a","b"); //全等于
ArrayAlg.<String>getMiddle("a","b");
編譯器可以根據(jù)參數(shù)確定類型變量為string,如果傳參的比較模糊,如:
ArrayAlg.getMiddle(1.27,5200);
當編譯器收到一個Double和一個Integer時候,就會尋找這些類的共同超類型,也就是Number和Comparable,所以無論用哪個形參接受都行,但最好統(tǒng)一一下
類型變量的限定
通過extends關鍵字,可以限定泛型屬于哪一個類的子類
而super關鍵字可以限定泛型屬于哪一個類的超類
<T extends Employee> or <T extends Comparable & Serializable>
<T extends Manager>
注:類型變量可以擁有多個接口的限定,但只能擁有一個類的限定,并且這個類的限定必須寫在第一個
類型擦除
無論何時定義一個泛型類型,都自動提供一個原始類型(raw type),原始類型的名字就是刪去類型參數(shù)后的泛型類型名,擦除(erased)類型變量,并替換為限定類型(無限定類型則為Object,如果有多個限定類型則用第一個替換)
注:如果存在多個限定類型,盡量將標簽(tagging)接口(即沒有方法的接口)放在后面,因為如果方法中調(diào)用限定中的接口的方法,而第一個限定類型又不是該接口,那么會多一步強行轉(zhuǎn)化。
然而類型擦除帶來一個棘手的問題:
如果一個非泛型類繼承自一個泛型類,并且重寫了其中一個帶有泛型的參數(shù)的方法,如下:
public class DateInterval extends Pair<LocalDate>{
...
@Override
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) > 0){
super.setSecond(second);
}
}
}
public class Pair<T>{
...
public void setSecond(T second){
this.second = second;
}
}
在類型消除后類Pair的setSecond方法的參數(shù)變?yōu)镺bject second,如此一來類DateInterval變有了兩個setSecond方法,一個類型為Object,另一個類型為LocalDate,這是不合理的。對于以下代碼執(zhí)行:
DateInterval interval = new Datelnterval(. . .);
Pair<Loca1Date> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);
這里的pair調(diào)用setSecond調(diào)用具有多態(tài)性,由于是Pair引用的Datelnterval實例,Pair的引用方法在擦除泛型后為setSecond(Object second),那么應該調(diào)用所以應該調(diào)用Datelnterval.setSecond(Object second)方法,但是Datelnterval中只有setSecond(T second)方法,所有編譯器替Datelnterval類生成如下一個橋方法(bridge method)
public class DateInterval{
public void setSecond(Object second) { setSecond((Date) second);
}
另一個問題:
如果DateInterval也實現(xiàn)了 getSecond()方法,那么橋方法會和原始方法擁有同一個方法簽名
public LocalDate getSecond(){...}
public Object getSecond(){return (LocalDate)getSecond()}
雖然以上方法在java編譯器中不合法,但是在虛擬機中可以存在返回值不同但方法簽名相同的方法
約束與局限性
局限性:
- 不能用于基本類型,如:Pair<int>
- 不能用于類型檢測,如:if (a instanceof Pair<String>) //error
- 不能創(chuàng)建數(shù)組實例,如:Pair<String>[] table = new Pair<String>[10]; // Error
- 不能實例化類型變量,如:new T();但可以通過函數(shù)式接口實現(xiàn)
- 泛型類的靜態(tài)上下文中類型變量無效,原因也很簡單,靜態(tài)方法和域是跟著類走的,只有一份,類型擦除后,類只能攜帶一份靜態(tài)資源。
- 泛型消除的一個原則:要想支持擦除的轉(zhuǎn)換,就要強行限制一個類或者類型變量不能同時成為兩個接口類型的子類,并且這兩個接口類型是同一接口的不同參數(shù)化。如以下泛型就是非法的:因為消除變量后,compareTo方法不知該強轉(zhuǎn)成哪個類型
class Employee implements Comparable<Emp1oyee> { . . . }
class Manager extends Employee implements Comparable<Manager>
{ . . . } // Error
</br>
通配符類型
泛型有時候并不能確定某一個類型,但是可以確定他們都有某些特性,或者繼承自某些接口,又或者是某些類的超類
通配符子類限定
Pair<? extends Employee>
通配符超類限定(supertype bound)
Pair<? super Manager>
總結(jié):
帶有超類限定的通配符可以向泛型對象寫入,帶有子類型限定的通配符可以從泛型對象讀取
解釋一下:
首先明確一點,java的多態(tài),是指父類引用可以引用子類實現(xiàn)
當使用子類通配符時, setFirst(? extends Employee)時,編譯器只知道傳遞Employee子類,但是不知道具體類型,但是如果使用getFirst()的時候,無論什么類型的實例都可以用Employee類型作為引用參數(shù)。
而當使用超類通配符時,setFirst(? super Manager)時,Pair類的first參數(shù)的引用只可能是Manager及其超類,那么傳遞Manager或是其子類的實例都可以,但是getFirst()并不能知道得到哪個類型,只能用Object去接收
超類的另一種應用:
public static <T extends Comparable> T min(T[] a){
...
}
如上所示,該方法就是調(diào)用compareTo方法來確定最小值,由于Comparable也是一個泛型,我們可以通過如下方式提升效率,減少強轉(zhuǎn)
public static <T extends Comparable<T>> T min(T[] a){
...
}
上面這個對于string是夠用了,但是如果傳入LocalDate數(shù)組,由于LocalDate實現(xiàn)了ChronoLocalDate接口,而ChronoLocalDate又繼承自Comparable<ChronoLocalDate>,所以LocalDate實現(xiàn)的是Comparable<ChronoLocalDate>而不是Comparable<LocalDate>,所以返回值變成了ChronoLocalDate,還需要用戶進行強行轉(zhuǎn)化才行,如下代碼可以進行正確的限制。
public static <T extends Comparable<? super T>> T min(T[] a){
...
}
由于ChronoLocalDate 是LocalDate 的超類Comparable<ChronoLocalDate>是Comparable<? super LocalDate>的子類,所以返回值可以為LocalDate
無限定通配符
Pair<?>
無限定通配符和原始類的區(qū)別在于,無限定通配符不可以set泛型域(除了null),主要用于一些簡單方法的使用,如:
public static boolean hasNulls(Pair<?> p)
{
return p.getFirst() == null || p.getSecond() == null;
}
</br>