Java泛型(三):型別擦除帶來的約束與侷限性
目錄:
1. 型別擦除帶來的約束與侷限性
Java泛型(二):泛型和虛擬機器(型別擦除)中已經詳細說明了Java虛擬機器(JVM,Java Virtual Machine)是如何應對泛型資料的——型別擦除機制。這種做法即相容了泛型出現之前的JDK版本,同時也解決了 JVM 沒有泛型型別的物件的問題。
但是,上帝給你開啟了一扇門,肯定會給你關上另外一扇窗,沒有哪種方法是十全十美的。型別擦除機制雖然很好的解決了 JVM 沒有泛型型別物件的問題,但同時也引出了一些新的問題:
- 不能用型別引數代替基本型別
- 執行時型別查詢(instanceof)對泛型型別並不適用
- getClass 方法總是返回原始型別
- 不能在靜態域或方法中引用型別變數
- 不能例項化引數化型別的陣列
- 不能例項化型別變數
// 以下是上述六點侷限性的舉例
Person<int> // error(第一點)
if (person instanceof Person<String>) // error(第二點)
Person<String> p = (Person<String>) person; // Warning-can only test that p is a Person
// 第三點
Person<String> stringPerson = ...
Person<Double> doublePerson = ...
stringPerson.getClass() == doublePerson.getClass() // result is true
// 第四點
private static T name // error
public static T getName() {...} // error
// 第五點
Person<String>[] person; // ok
new Person<String>[10]; // error
// 第六點
new T(); // error
new T[10] // error
T.class // error
前四點侷限性並不難理解,型別擦除之後所有泛型型別都會轉化為其所對應的原始型別,如引數 T 被轉化為 Object 型別。
-
對於第一點侷限性,由於 Object 類無法儲存八大基本型別的變數(其子類就更加不可以了),所以不能用型別引數代替基本型別。如果的確有需求,可以使用基本型別對應的包裝器型別(wrapper type)替換,如用 Integer 代替 int 型別。
-
對於第二點,由於型別擦除之後只剩下其原始型別,上述第二點對應的程式碼實際上僅僅測試了 person 物件是否是任意型別的一個 Person,並不能達到判斷 person 物件是否為一個 Person< String > 型別物件的目的。為提醒這一風險,試圖查詢一個物件是否屬於某個泛型型別時,倘若使用 instanceof 會得到一個編譯器錯誤,如果使用強制型別轉換會得到一個警告。
-
對於第三點,同樣是由於型別擦除,getClass 方法總是返回原始型別,故上述第三點程式碼中得到的結構為 true (兩次呼叫實際都返回了Person.class)。
-
對於第四點,由於靜態域和方法是“屬於”這個類的,對於一個泛型類,沒有指定具體的型別引數之前,它是不可能知道類內部的型別變數具體是什麼型別的。因此在靜態域或方法中引用型別變數顯然是不可行的。
2. 不能例項化引數化型別的陣列
正如前面所說,Java 中不能例項化引數化型別的陣列。
new Person<String>[10]; // error
可是,為什麼不能這麼做呢?在 Java 中,對於一個陣列,它會記住自己儲存的元素型別,如果試圖儲存其他型別的元素,就會丟擲一個 Array-StoreException 異常:
String[] str = new String[10];
str[0] = 1; // error
不過對於泛型型別,型別擦除會使得這種機制失效。
Person<String>[] stringPersonArray = new Person<String>[10];
stringPersonArray[0] = new Person<Double>();
// 能夠通過陣列儲存檢査,不過仍會導致一個型別錯誤。
型別擦除之後,只剩下原始型別 Person,所以在虛擬機器看來 Person< String > 和 Person< Double > 是“同一種型別”,所以上述程式碼中第二行的賦值操作能夠通過陣列儲存檢査,不過仍會導致一個型別錯誤。出於這個原因,Java 不允許建立引數化型別的陣列。
當然,可以用一種取巧的方法實現“例項化引數化型別的陣列”。可以宣告通配型別的陣列,然後進行型別轉換:
Person<String>[] stringPersonArray = (Person<String>[]) new Person<?>[10];
當然,如果這麼做的話,結果將是不安全的。如果在 stringPersonArray[0] 中儲存一個Person< Double >, 然後對 table[0].getInformation() 呼叫一個 String 的方法,會得到一個 ClassCastException 異常。(table[0].getInformation() 是在Peson< T >類中自己定義的一個方法,會返回一個 T 型別的物件)
如果需要收集引數化型別物件,只有一種安全而有效的方法:使用ArrayList:
ArrayList<Person<String>> p = new ArrayList<>();
3. 不能例項化型別變數
在 Java 中,不能使用像 new T(…),newT[…] 或 T.class 這樣的表示式中的型別變數。
3.1 不能使用 new T(…)
不能使用 new T(…) 例項化一個物件。型別擦除會將 T 改變成Object 型別,我們的本意肯定不希望呼叫 new Object()。例如,下面的建構函式就是非法的:
public Person() {
information = new T(); // error
}
在 JDK 1.8 之後,類似上述構造器問題的最好的解決辦法是讓呼叫者提供一個構造器表示式。例如:
Person<String> p = Person.makePerson(String::new);
public Person(T information) {
this.information = information;
}
public static <T> Person<T> makePerson(Supplier<T> constr) {
return new Person<>(constr.get());
}
makePerson 方法接收一個Supplier< T >,這是一個函式式介面, 表示一個無引數而且返回型別為T 的函式。
3.2 不能使用 new T[…]
就像不能 new T(…) 例項化一個物件一樣,也不能像這樣例項化一個陣列。
new T[10]; // error
那麼,當某個方法需要返回一個 T 型別的陣列時,該怎麼辦呢?對於這種情況,可以讓方法接收一個陣列引數,例 ArrayList 中的其中一個 toArray 方法是這麼實現的:
/**
* Returns an array containing all of the elements in this list in proper
* sequence (from first to last element); the runtime type of the returned
* array is that of the specified array. If the list fits in the
* specified array, it is returned therein. Otherwise, a new array is
* allocated with the runtime type of the specified array and the size of
* this list.
*
* <p>If the list fits in the specified array with room to spare
* (i.e., the array has more elements than the list), the element in
* the array immediately following the end of the collection is set to
* <tt>null</tt>. (This is useful in determining the length of the
* list <i>only</i> if the caller knows that the list does not contain
* any null elements.)
*
* @param a the array into which the elements of the list are to
* be stored, if it is big enough; otherwise, a new array of the
* same runtime type is allocated for this purpose.
* @return an array containing the elements of the list
* @throws ArrayStoreException if the runtime type of the specified array
* is not a supertype of the runtime type of every element in
* this list
* @throws NullPointerException if the specified array is null
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
此方法需要接收一個陣列引數。如果陣列足夠大,就使用這個陣列;否則,用 a 的執行時型別構造一個足夠大的新陣列。
ArrayList<String> list = new ArrayList<>();
String[] strings = list.toArray(new String[10]);
3.3 不能使用 T.class
表示式 T.class 是不合法的,因為它會擦除為 Object.class。
繼續借助 3.1 中的例子,除了讓呼叫者提供一個構造器表示式之外,是否可以通過反射實現同樣的功能呢?答案是可以的,但是遺憾的是,不能直接呼叫:
information = T.class.newInstance();
必須像下面這樣設計以便得到一個 Class 物件:
public static <T> Person<T> makePerson(Class<T> c) {
try {
return new MyPerson<>(c.newInstance());
} catch (Exception e) {
return null;
}
}
此方法可以這樣呼叫:
Person<String> p = Person.makePerson(String.class);
注意,Class 類本身就是泛型的。例如,String.class 是一個 Class< String > 的例項(事實上,它是唯一的例項)。
相關文章
- Go 1.18泛型的侷限性初探Go泛型
- Java泛型型別擦除問題Java泛型型別
- Java 泛型,你瞭解型別擦除嗎?Java泛型型別
- Go 泛型之泛型約束Go泛型
- 泛型的約束理解泛型
- java泛型的侷限探究Java泛型
- 泛型擦除的原理泛型
- C#泛型約束C#泛型
- Java™ 教程(型別擦除)Java型別
- 初探Java型別擦除Java型別
- 一句話,講清楚java泛型的本質(非型別擦除)Java泛型型別
- 資料型別與約束資料型別
- 面試官:說說什麼是泛型的型別擦除?面試泛型型別
- [譯]Kotlin泛型中何時該用型別形參約束?Kotlin泛型型別
- Java泛型T與?的區別Java泛型
- TreeSet的null值與元素型別的約束Null型別
- 泛型的型別擦除後,fastjson反序列化時如何還原?泛型型別ASTJSON
- Swift 型別擦除Swift型別
- Java™ 教程(泛型原始型別)Java泛型型別
- Java中建立泛型型別的例項Java泛型型別
- SQL教程——常見的約束型別SQL型別
- C#中泛型約束(where)是什麼?C#泛型
- 【java】【泛型】泛型geneticJava泛型
- [譯]Swift 中的型別擦除Swift型別
- Sqlserver中所有約束的型別,建立、修改與刪除SQLServer型別
- Java中的泛型程式設計:深入理解型別引數與型別邊界的使用Java泛型程式設計型別
- TypeScript 泛型型別TypeScript泛型型別
- 型別 VS 泛型型別泛型
- Java泛型理解與使用Java泛型
- 泛型類、泛型方法、型別萬用字元的使用泛型型別字元
- JavaScript中判斷物件是否屬於Array型別的4種方法及其背後的原理與侷限性JavaScript物件型別
- 泛型(三)泛型
- JAVA基礎之九-泛型(通用型別)Java泛型型別
- 泛型型別(.NET 指南)泛型型別
- Java泛型Java泛型
- Java中基於泛型的交叉型別 - {4Comprehension}Java泛型型別
- 【.NET】利用 IL 魔法實現隨心隨意的泛型約束泛型
- 從 Swift 中的序列到型別擦除Swift型別