Java泛型(三):型別擦除帶來的約束與侷限性

琚ICIW發表於2020-12-20

1. 型別擦除帶來的約束與侷限性

Java泛型(二):泛型和虛擬機器(型別擦除)中已經詳細說明了Java虛擬機器(JVM,Java Virtual Machine)是如何應對泛型資料的——型別擦除機制。這種做法即相容了泛型出現之前的JDK版本,同時也解決了 JVM 沒有泛型型別的物件的問題。

但是,上帝給你開啟了一扇門,肯定會給你關上另外一扇窗,沒有哪種方法是十全十美的。型別擦除機制雖然很好的解決了 JVM 沒有泛型型別物件的問題,但同時也引出了一些新的問題:

  1. 不能用型別引數代替基本型別
  2. 執行時型別查詢(instanceof)對泛型型別並不適用
  3. getClass 方法總是返回原始型別
  4. 不能在靜態域或方法中引用型別變數
  5. 不能例項化引數化型別的陣列
  6. 不能例項化型別變數
// 以下是上述六點侷限性的舉例

	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 型別。

  1. 對於第一點侷限性,由於 Object 類無法儲存八大基本型別的變數(其子類就更加不可以了),所以不能用型別引數代替基本型別。如果的確有需求,可以使用基本型別對應的包裝器型別(wrapper type)替換,如用 Integer 代替 int 型別。

  2. 對於第二點,由於型別擦除之後只剩下其原始型別,上述第二點對應的程式碼實際上僅僅測試了 person 物件是否是任意型別的一個 Person,並不能達到判斷 person 物件是否為一個 Person< String > 型別物件的目的。為提醒這一風險,試圖查詢一個物件是否屬於某個泛型型別時,倘若使用 instanceof 會得到一個編譯器錯誤,如果使用強制型別轉換會得到一個警告。

  3. 對於第三點,同樣是由於型別擦除,getClass 方法總是返回原始型別,故上述第三點程式碼中得到的結構為 true (兩次呼叫實際都返回了Person.class)。

  4. 對於第四點,由於靜態域和方法是“屬於”這個類的,對於一個泛型類,沒有指定具體的型別引數之前,它是不可能知道類內部的型別變數具體是什麼型別的。因此在靜態域或方法中引用型別變數顯然是不可行的。

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 > 的例項(事實上,它是唯一的例項)。

相關文章