Java泛型程式設計
泛型程式設計的意義:編寫的程式碼可以被很多不同型別的物件所重用。
好處:出現編譯錯誤比類在執行時出現類的強制型別轉換異常要好得多。
程式設計師的任務:預測出所用類的未來可能有的所有用途。
1、定義簡單泛型類
public class Pair<T>{
...
}
ps:泛型類相當於普通類的工廠
2、定義泛型方法
class ArrayAlg{
public static <T> T getMiddle(T ...a){
...
}
}
ps:型別變數放在修飾符後面,返回值前面
呼叫方法,ArrayAlg.<String>getMiddle(...);
3、型別限定
限定型別變數必須是,實現了Comparable介面的類
public static <T extends Comparable> T min(T[] a) ...
public static <T extends Comparable & Serializable> T min(T[] a) ...
4、型別擦除
泛型實現的早期版本中,是向後相容的。
向後相容:在1.0版本中,可以使用之後,甚至未來版本的功能。
型別擦除:擦除型別變數,並替換為限定型別(無限定的變數用Object)
若有兩個限定型別,則型別擦除為,第一個限定型別,對於第二個限定型別,編譯器必要時則插入強制型別轉換,為提高效率,應將標籤介面放在限定型別列表最後。
與C++模板的區別,C++中每個模板的例項化產生不同的型別,這一現象稱為“模板程式碼膨脹”。
(1)翻譯型別表示式
Pair<Employee> buddies...
Employee buddy=buddies.getFirst();
編譯器把這個方法的呼叫,分別解釋為:
- 對原始方法Pair.getFirst的呼叫。
- 將返回的Object型別強制轉換為Employee型別。
(2)翻譯泛型方法
class DateInterval extends Pair<LocalDate>{
public void setSecond(LocalDate second){
....
}
}
DateInterval interval = new DateInterval();
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);
問題來了,Pair<LocalDate>類裡的setSecond(LocalDate second)方法,經過型別擦除後,變為setSecond(Object second),這個時候,interval怎麼通過父類的這個型別擦除後的方法,去呼叫自己重寫的方法呢?
編譯器是這樣解決的:
- 在DateInterval中生成一個橋方法,
- public void setSecond(Object second){
- setSecond((Date)second);
- }
- 實質上,合成的橋方法,呼叫了新定義的方法。
- 產生的問題:編譯器可能產生兩個僅返回型別不同的方法,但虛擬機器能正確處理。
小結:
虛擬機器中沒有泛型,只有普通的類和方法。
所有的型別引數都用它們的限定型別替換。
橋方法被合成來保持多型。
為保持型別安全性,必要時插入強制型別轉換。
5、泛型程式設計的約束和侷限
(1)不能用基本型別例項化型別引數
(2)執行時型別查詢只適用於原始型別
如,a instanceof Pair<String> //錯誤
Pair<String> stringPair;
Pair<Employee> employeePair;
stringPair.getClass() == employeePair.getClass(); //總是相等,它們都將返回Pair.class
(3)不能建立引數化型別的陣列
如,Pair<String>[] table = new Pair<String>[10]; //錯誤,型別擦除後,這個陣列存什麼都可以了。
但可以,Pair<String>[] table = (Pair<String>[])new Pair<?>[10]; //但不安全,因為可以存Pair<Emplyee>
如果需要獲取,一個引數化型別物件的集合,可以這樣使用,ArrayList<Pair<String>>。
(4)@SafeVarargs直接標註addAll方法
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
//Java 7 之後,直接用了SafeVarargs標註了addAll和array,消除了建立泛型陣列的有關限制
addAll(table,pair1,pair2);
(5)不能例項化型別變數
new T(),new T[],T.class,T.class.newInstance等表示式均非法。
解決辦法一:
Pair<String> p = Pair.makePair(String::new);
public static<T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get(),constr.get());
}
ps:Supplier<T>是一個函式式介面,表示一個無引數而且返回型別為T的函式。
解決辦法二:
public static<T> Pair<T> makePair(Class<T> cl){
return new Pair<>(cl.newInstance(),cl.newInstance());
}
Pair<String> p = Pair.makePair(String.class);
ps:Class類本身是泛型,String.class是一個Class<String>的唯一例項。
(6)構造泛型陣列方法
方法一:
String[] ss = ArrayAlg.minmax(String[]::new, "Tom","Dick","Harry");
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a){
T[] mm = constr.apply(2);
}
方法二,利用反射機制:
public static <T extends Comparable> T[] minmax(T... a){
T[] mm = (T[])Array.newInstance(a.getClass().getComponentType() , 2);
}
(7)泛型類的靜態上下文中型別變數無效
public class Singleton<T>{
private static T singleInstance; // 錯誤
}
ps:型別擦除後,只剩下Singleton類和 singleInstance域。
(8)不能丟擲或捕獲泛型類的例項
1、繼承throwable的類不能是泛型類。
2、catch子句裡,不能catch型別變數。
但是,在異常規範中使用型別變數是允許的。
如:public static <T extends Throwable> void do work(T t) throws T{
...
}
(9)可以消除對受查異常的檢查
Java異常處理的一個基本原則是,必須為所有受查異常提供一個處理器。
但是,類似於下面這種寫法:
@SuppressWarnings("unchecked")
public static <T extends throwable> void throwAs(Throwable e) throw T{
throw (T)e;
}
new Thread(){
public void run(){
try{
....
}catch(Throwable t){
Block.<RuntimeException>throwAs(t);
}
}
}
ps:一般來說,我們必須捕獲執行緒run方法中的所有受查異常,但run方法宣告為不丟擲任何受查異常,我們只是丟擲異常,並"哄騙"編譯器,讓它認為這不是一個受查異常。
(10)注意擦除後的衝突
泛型規範原則:要想支援擦除的轉換,就需要強行限制一個類或型別變數不能同時成為兩個介面型別的子類,而這兩個介面是同一介面的不同引數化。
class Employee implements Comparable<Employee>;
class Manager extends Employee implements Comparable<Manager>;
ps:以上兩個定義是錯誤的,原因是有可能和產生的橋方法,產生衝突。
6、泛型型別的繼承規則
(1)永遠可以將引數化型別轉換為一個原始型別。
如:Pari<Employee> 可以轉換為Pair,但可能會產生錯誤。
(2)泛型類可以擴充套件或實現其他的泛型類。
(3)ArrayList<Manager>可以轉換為List<Manager>,但不能轉換為List<Employee>。
7、萬用字元型別
萬用字元型別中,允許型別引數變化。
子型別限定的萬用字元:
Pair<Manager>是Pair<? extends Employee>的子型別。
? extends Employee限定:可以用於getter方法,不能用於setter方法。
ps:getter返回物件回來賦給Employee型別,完全可以
setter型別,不可以的理由是,不知道需要哪個子類
超型別限定的萬用字元:
void setFirst(? super Manager);
? super Manager getFirst();
setter方法,只能傳入Manager或其子類,不能傳入父類。
getter方法,只能獲取到Object物件。
一般的準則:setter方法一般用超型別限定的萬用字元,getter方法一般用子型別限定的萬用字元。
另一種應用:
public static <T extends Comparable<T>> T min(T[] a);
T extends Comparable<T>使用得更徹底,工作地更好。
但有ChronoLocalDate是LocalDate的父類,ChronoLocalDate擴充套件了Comparable<ChronoLocalDate>介面。因此使得,LocalDate類也擴充套件了Comparable<ChronoLocalDate>介面。
所以,可以寫成,public static <T extends Comparable<? super T>> T min(T[] a) 來解決問題。
ps:上述宣告,只為排除呼叫引數上的不必要的限制,若是一名庫程式設計師,一定要習慣於萬用字元,否則在程式碼中就需要多處,新增強制型別轉換直至程式碼可以編譯。
無限定萬用字元:
Pair<?>的返回?的getter方法,只能返回Object,相應的setter(?)方法,不能被呼叫。
Pair<?>和Pair的本質:可以用任意的Object物件呼叫原始Pair類的setObject方法。
用法:避免使用萬用字元型別。
setFirst(null);
public static boolean hasNulls(Pair<?> p){
return p.getFirst()==null || p.getSecond()==null;
}