1. 程式人生 > 其它 >java中的泛型設計

java中的泛型設計

1.為什麼要使用泛型程式設計
ArrayList<String> files = new ArrayList<>() 等價於 var files = new ArrayList<String>()

2.定義簡單泛型類
public class Pair<T>{
private T first;
private T second;
public Pair(){first = null; second = null;}
public Pair(T first, T second){this.first = first; this.second = second;}
public T getFirst(){return first;}
public void setFirst(T newValue){first = newValue;}
泛型類後面可以有多個型別變數 如 public class pair<T, U>{}

3.泛型方法
可以在普通類中定義泛型方法 型別變數放在修飾符後面(public static) 在返回型別的前面(T) 如
class ArrayAlg{
public static <T> T getmiddle(T... a){
return a[a.length / 2];
}
}
當你呼叫一個泛型方法時 可以把具體型別放在尖括號中 放在方法名前面 如
String middle = ArrayAlg.<String>getmiddle("John", "Q", "Public");
也可以不寫具體型別 編譯器會自動推斷 如
String middle = ArrayAlg.getmiddle("John", "Q", "Public");

4.型別變數的限定
class ArrayAlg{
public static <T> T min(T[] a){
if(a == null || a.length == 0) return null;
T smallest = a[0];
for(int i = 1; i < a.length; i++)
if(smallest.compareTo(a[i]) > 0) smallest = a[i];
return smallest;
}
}
注意 這裡的min方法中 smallest的型別為T 而T必須是實現了Comparable介面的類 可以通過對T設定一個限定來實現 如
public static <T extends Comparable> T min(T[] a){}
型別變數的限定格式:<T extends BoundingType>
一個型別變數或萬用字元可以有多個限定 限定型別用"&"分隔 而逗號用來分隔型別變數 如 T extends Comparable & Serializable
為了提高效率 應將標籤介面(即沒有方法的介面)放在限定列表的末尾

5.泛型程式碼與虛擬機器(僅需瞭解原理即可)
虛擬機器沒有泛型型別物件 所有物件都屬於普通類
1.型別擦除:無論何時定義一個泛型型別 都會自動提供一個相應的原始型別 這個原始型別的名字就是去掉型別引數<T>的泛型型別名
型別變數 T 會被擦除 並替換為其限定型別(對於無限定的變數則替換為Object)
原始型別用第一個限定來替換型別變數T 若沒有給定限定 就替換為Object
如:public class Interval<T extends Comparable & Serializable> implements Serializable{
private T lower;
}
在虛擬機器中型別擦除後為:public class Interval implements Serializable{
private Comparable lower;
}
2.轉換泛型表示式:編寫一個泛型方法呼叫時 如果擦除了返回型別 編譯器會插入強制型別轉換 如下:
對於這個語句序列:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
getFirst擦除型別後的返回型別為Object 編譯器自動插入了轉換到Employee的強制型別轉換
3.轉換泛型方法:型別擦除也會出現在泛型方法中 但是可能引發一些多型的問題 虛擬機器是如何解決的呢?
如:class DateInterval extends Pair<LocalDate>{
public void setSecond(LocalDate Second){
if(Second.compareTo(getFirst()) >= 0)
super.setSecond(second);
}
}
這個類進行擦除後變為class DateInterval extends Pair{
public void setSecond(LocalDate Second){ ...}
}
然而還有一個從Pair繼承來的setSecond方法 即public void setSecond(Object Second)
考慮下面的語句序列:
var interval = new DateInterval( ...);
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);
那麼 呼叫時會呼叫哪個呢?
實際上 編譯器會在類中生成一個橋方法 public void setSecond(Object Second){setSecond((LocalDate) second);}
通過橋方法 會呼叫setSecond((LocalDate) second 這樣就實現了多型
總之 對於java泛型的轉換 需要記住以下幾個事實:
虛擬機器中沒有泛型 只有普通的類和方法
所有的型別引數都會體會為它們的限定型別
會合成橋方法來保持多型
為保持型別安全性 必要時會插入強制型別轉換
4.呼叫遺留程式碼:可用註解 @SuppressWarnings("unchecked") 消除警告 這個註解會關閉對接下來語句的檢查

6.限制與侷限性:
1.不能用基本型別來例項化型別引數
型別引數只能代表引用型型別,不能是原始型別(像 int、double、char 等)
如 沒有Pair<double> 只能用Pair<Double> 即基本型別使用時需要使用他們的包裝類
2.執行時型別查詢只適用於原始型別
所有的型別查詢都返回原始型別 如 instanceof檢查和getClass方法對於Pair<T>的檢查都返回Pair
3.不能建立引數化型別的陣列
不能例項化引數化型別的陣列 如 var table = new Pair<String>[10]是錯誤的
雖然不允許建立這些陣列 但宣告型別為Pair<String>[]的變數仍是合法的 不過不能用new Pair<String>[10]初始化這個變數
可以宣告萬用字元型別的陣列 然後強轉 如 var table = (Pair<String>[]) new Pair<?>[10]; 這樣雖然能通過 但是是不安全的 建議不用
如果需要收集引數化型別物件 簡單的使用ArrayList:ArrayList<Pair<String>>更安全有效
4.Varargs警告
public static <T> void addAll(Collection<T> coll, T... ts){
for(T t : ts) coll.add(t);
}
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);
為了呼叫這個方法 java虛擬機器必須建立一個Pair<String>陣列 但是虛擬機器只會給你提供一個警告 而不是錯誤
可用兩種方法來抑制這個警告 一種是為addAll方法增加註解@SuppressWarnings("unchecked") 另一種是用@SafeVarargs直接註解addAll方法
5.不能例項化型別變數
不能在類似new T(...)的表示式中使用型別變數 如 public Pair(){first = new T();}就是非法的
在java8之後 最好的解決辦法是讓呼叫者提供一個構造器表示式 如
Pair<String> p = Pair.makePair(String::new);
makePair方法接收一個Supplier<T> 這是一個函式式介面 表示一個無引數而且返回型別為T的函式
public static <T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get());
傳統方法是通過反射呼叫Constructor.newInstance方法來構造泛型物件
public static <T> Pair<T> makePair(Class<T> cl){
try{
return new Pair<>(cl.getConstructor().newInstance());
}
catch(Exception e){return null;}
}
這個方法可以如下呼叫
Pair<String> p = Pair.makePair(String.class)
注意 Class類本身是泛型的 因此 makePair方法能夠推斷出Pair的型別
6.不能構造泛型陣列
直接構造並強制轉換會出現異常 最好讓使用者提供一個數組構造器表示式 如
String[] names = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a){
T[] result = constr.apply(2);
......
}
比較老式的辦法是利用反射 並呼叫Array.newInstance:
public static <T extends Comparable> T[] minmax(T... a){
var result = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
......
}
7.泛型類的靜態上下文中型別變數無效
不能再靜態欄位或靜態方法中引用型別變數!
8.不能丟擲或捕獲泛型類的例項
既不能丟擲也不能捕獲泛型類的物件 如不能擴充套件Throwable類(一個泛型類) catch子句中也不能使用型別變數 如
public class Problem<T> extends Exception{...}是不合法的
9.可以取消對檢查型異常的檢查
可使用以下方法將所有異常都轉化為編譯器所認為的非檢查型異常
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T{
throw(T) t;
}
假設這個方法包含在介面Task中 如果有一個檢查型異常e 可使用如下程式碼
try{
......
}
catch(Throwable t){
Task.<RuntimeException>throwAs(t);
}
10.注意擦除後的衝突
注意不要和擦除後的Object中的方法同名 擦除後會引起衝突
不允許實現對同一介面的不同引數化 可能會引起橋方法的衝突

7.泛型型別的繼承規則:
無論S與T有什麼關係(如S是T的子類或原始型別等等) Pair<S>與Pair<T>都沒有任何關係

8.萬用字元型別
1.萬用字元概念
在萬用字元型別中 允許型別引數發生變化 如 Pair<? extends Employee>表示任何泛型Pair型別 它的型別引數是Employee的子類
如 public static void printBuddies(Pair<Employee> p){} 不能將Pair<Manager>傳遞給這個方法 不過可以使用一個萬用字元型別
public static void printBuddies(Pair<? extends Employee> p) 型別Pair<Manager>是Pair<? extends Employee>的子型別
不能呼叫setFirst方法 因為無法知道具體是什麼型別 但是可以呼叫getFirst方法
2.萬用字元的超型別限定
? super Manager 這個萬用字元限制為Manager的所有超型別
與上一個相反 這種可以為方法提供引數 但不能使用返回值 即可以用setFirst 但不能用getFirst
直觀的講 帶有超型別限定的萬用字元允許你寫入一個泛型物件 而帶有子型別限定的萬用字元允許你讀取一個泛型物件
3.無限定萬用字元
Pair<?> 此時 getFirst的返回值只能賦給一個Object setFirst方法不能被呼叫 甚至不能用Object呼叫
Pair<?>與Pair本質的不同在於:可以用任意Object物件呼叫原始Pair類的setFirst方法
這種型別可能對一些簡單操作非常有用 如測試一個對組是否包含一個null引用 如下
public static boolean hadNulls(Pair<?> p){
return p.getFirst() == null || p.getSecond() == null;
}
4.萬用字元捕獲
public static void swap(Pair<?> p) 注意 不能再編寫程式碼中用?作為一種型別 那怎麼實現這個方法呢?
可以寫一個輔助方法swapHelper 再被上面的方法呼叫即可 這就是捕獲萬用字元
public static <T> void swapHelper(Pair<T> p){
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
這時可由swap呼叫swapHelper 如public static void swap(Pair<?> p){swapHelper(p)}
在這種情況下swapHelper中的引數T捕獲萬用字元
注意:編譯器必須保證萬用字元表示單個確定的型別 例如 ArrayList<Pair<T>>中的T永遠不能捕獲ArrayList<Pair<?>>中的萬用字元