在使用Java泛型時需要考慮一些限制,大多數限制都是由類型擦除引起的。
不能用基本類型實例化類型參數
不能用類型參數代替基本類型。因此,沒有Pair<double>
,只有Pair<Double>
。當然,其原因是類型擦除。擦除之後,Pair
類含有Object類型的域,而Object不能存儲double值。
這的確令人煩惱。但是,這樣做與Java語言中基本類型的獨立狀態想一致。這並不是一個致命的缺陷,只有8種基本類型,當包裝器類型不能接受替換時,可以使用獨立的類和方法處理它們。
運行時類型查詢只適用於原始類型
虛擬機中的對象總有一個特定的非泛型類型。因此,所有的類型查詢只產生原始類型。如:
if (a instanceof Pair<String>) // 錯誤的
實際上僅僅測試a
是否是任意類型的一個Pair
。下面的測試同樣如此:
if (a instanceof Pair<T>) // 錯誤的
或強制類型轉換:
Pair<String> p = (Pair<String>)a; // 警告
爲提醒這一風險,試圖查詢一個對象是否屬於某個泛型類型時,倘若使用instanceof
會得到一個編譯器錯誤,如果使用強制類型轉換會得到一個警告。
同樣的道理,getClass
方法總是返回原始類型:
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass()) ...;// true
其比較的結果是true
,這是因爲兩次調用getClass
都將返回Pair.class
。
不能創建參數化類型的數組
不能實例化參數化類型的數組,如:
Pair<String>[] table = new Pair<String>[10]; // Error
這有什麼問題呢?擦除之後,table的類型是Pair[]
。可以把它轉換爲Object[]
:
Object[] objArray = table;
數組會記住它的元素類型,如果試圖存儲其他類型的元素,就會拋出一個ArrayStoreException異常:
objarray[0] = "Hello"; // Error, component type is Pair
不過對於泛型類型,擦除會使這種機制無效:
objarray[0] = new Pair<Employee>();
能夠通過數組存儲檢查,不過仍會導致一個類型錯誤。處於這個原因,不允許創建參數化類型的數組。
需要說明的是,只是不允許創建這些數組,而聲明類型爲Pair<String>[]
的變量仍是合法的。不過不能用new Pair<String>[10]
初始化這個變量。
Varargs警告
上面看到Java不支持泛型類型的數組。如果是想參數個數可變的方法傳遞一個泛型類型呢?
考慮下面這個簡單的方法,它的參數個數是可變的:
public static <T> void addAll(Collection<T> coll, T... ts) {
for (t : ts) coll.add(t);
}
應該記得,實際上參數ts是一個數組,包含提供的所有實參。
現在考慮一下調用:
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);
爲了調用這個方法,Java虛擬機必須建立一個Pair<String>
數組,這就違反了前面的規則。不過,對於這種情況,規則有所放鬆,你只會得到一個警告,而不是錯誤。
可以採用兩種方法來抑制這個警告。一種方法是爲包含addAll
調用的方法增加註釋@SuppressWarnings("unckecked")
。或者在Java SE 7中,還可以用@SafeVarargs
直接標註addAll
方法:
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)
現在就可以提供泛型類型來調用這個方法了。對於只需要讀取參數數組元素的所有方法,都可以使用這個註解,這僅限於最常見的用例。
不能實例化類型變量
不能使用像new T(...)
,new T[...]
或T.class
這樣的表達式中的類型變量。如,下面的Pair<T>
構造器就是非法的:
public Pair() {
first = new T();
second = new T();
} // Error
類型擦除將T
改變成Object
,而且,本意肯定不希望調用new Object()
。在Java SE 8之後,最好的解決方法是讓調用者提供一個構造器表達式。如:
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(), constr.get());
}
比較傳統的解決方法是通過反射調用Class.newInstance
方法來構造泛型對象。
遺憾的是,細節有點複雜。不能調用
first = T.class.newInstance(); // Error
表達式T.class
是不合法的,因爲它會擦除爲Object.class
。必須像下面這樣設計API以便得到一個Class
對象:
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<>(cl.newInstance(), cl.newInstance());
} catch (Exception ex) {
return null;
}
}
這個方法可以按照下列方式調用:
Pair<String> p = Pair.makePair(String.class);
注意,Class
類本身是泛型。例如,String.class
是一個Class<String>
的實例。因此,makePair
方法能夠推斷出pair
的類型。
捐贈
若你感覺讀到這篇文章對你有啓發,能引起你的思考。請不要吝嗇你的錢包,你的任何打賞或者捐贈都是對我莫大的鼓勵。