軟體構造隨筆4
為了防止個人小部落格出現不可訪問的狀況,故在部落格園亦留一份備份!
還請老師不要判為抄襲等,感謝!本人20級HIT學生,學號尾號230
第4篇
在本篇隨筆中,我們主要介紹:
- 泛型中的型別擦除
什麼是泛型的型別擦除?
我們在使用泛型時,總會在尖括號中輸入某個具體的型別,譬如List<String>
,殊不知在編譯階段會將所有的泛型表示(尖括號中的內容)都替換為具體的型別(其對應的原生態型別),我們所規定的<String>
彷彿是規定了個寂寞。這個過程就叫做型別擦除。
型別擦除的原則
進行型別擦除時,需遵循一定的規則[1]:
- 消除型別引數宣告,即刪除<>及其包圍的部分。
- 根據型別引數的上下界推斷並替換所有的型別引數為原生態型別:如果型別引數是無限制萬用字元或沒有上下界限定則替換為Object,如果存在上下界限定則根據子類替換原則取型別引數的最左邊限定型別(即父類)。
- 為了保證型別安全,必要時插入強制型別轉換程式碼。
- 自動產生“橋接方法”以保證擦除型別後的程式碼仍然具有泛型的“多型性”。
型別擦除的例子
猶記得在劉老師的第九章課件第50頁中,有這麼一句話
List<String> is not a subtype of List<Object>
為什麼這麼說呢?結合型別擦除,我們很容易意識到,由於沒有上下界限定,型別擦除後都會被替換成Object,因而兩者最終應該是同一類。接下來通過檢視List<String>
List<Integer>
對應物件的getClass()
來驗證這一點:
public class Hello { public static void main(String[] args) { List<String> stringList = new ArrayList<String>(); List<Integer> integerList = new ArrayList<Integer>(); stringList.add("先入一個字串"); integerList.add(114514); System.out.println(stringList.getClass() == integerList.getClass()); } }
控制檯輸出true
,說明型別確實被擦除掉了,剩下了原始型別。在這裡,根據上面所提到的規則,由於沒有上下界限定,都會被替換成Object
::: tip 原始型別
原始型別[1:1],就是擦除去了泛型資訊,最後在位元組碼中的型別變數的真正型別,無論何時定義一個泛型,相應的原始型別都會被自動提供,型別變數擦除,並使用其限定型別(無限定的變數用Object)替換。
:::
泛型方法中的型別擦除
無論是聲明瞭一個帶泛型的變數、抑或是帶泛型的類,其進行泛型擦除的原則已在上面提及。
但是,如果一個方法中帶泛型,具體會被處理成什麼樣子呢?這裡先下個結論:方法中的型別擦除仍遵循上面提及的擦除原則,但仍有一些額外的原則[1:2]:
- 在不指定泛型的情況下,泛型變數的型別為該方法中的幾種型別的同一父類的最小級,直到Object。
該方法中的幾種型別
本人理解為傳入的各個引數的各個型別的最小父類,詳見下面不遠處的例子。 - 在指定泛型的情況下,該方法的幾種型別必須是該泛型的例項的型別或者其子類
說的再簡潔些:沒指定泛型,型別就是最小父類;指定了泛型,型別就是所指定泛型的子類。
注:這裡的指定泛型,指的是在呼叫泛型方法時,指明泛型型別。譬如給出一個泛型方法:
public static <T> T add(T x,T y){
return y;
}
採用如xxx.<Integer>add(1,2)
的方式呼叫方法,就叫做指定泛型了。
下面給出一個例子[1:3],在指定泛型和不指定泛型的情況下,對於泛型方法是如何進行型別擦除的:
public class Test {
public static void main(String[] args) {
/**不指定泛型的時候*/
int i = Test.add(1, 2); //這兩個引數都是Integer,所以T為Integer型別
Number f = Test.add(1, 1.2); //這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Number
Object o = Test.add(1, "asd"); //這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Object
/**指定泛型的時候*/
int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能為Integer型別或者其子類
int b = Test.<Integer>add(1, 2.2); //編譯錯誤,指定了Integer,不能為Float
Number c = Test.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float
}
//這是一個簡單的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
::: tip 和實驗2中泛型方法宣告不一樣?
咦,這裡和我們在實驗2中所寫的好像有些不一樣呀?我們實驗2中相關泛型方法在宣告時,並沒有<T>
嘞?
其實,在實驗2中的ConcreteEdgesGraph
和ConcreteVerticesGraph
類的宣告中,是包含了<T>
的:
public class ConcreteEdgesGraph<L> implements Graph<L> {
...
}
如果在類例項化時指定了型別(譬如ConcreteVerticesGraph<String>
),那麼對於該類中所有返回T型別的方法,我們認為是已經被指定泛型的。
:::
到這裡,我又想起來了老師課件裡頭所提及的java.util.Collections
的例子:
public static <T> void copy(
List<? super T> dest,
List<? extends T> src);
List<Number> source = new LinkedList<>();
source.add(Float.valueOf(3));
source.add(Integer.valueOf(2));
source.add(Double.valueOf(1.1));
List<Object> dest = new LinkedList<>();
Collections.copy(dest,source);
有同學問:注意到copy方法的宣告中,dest和src對應的List中都有對泛型上下界的限定。在這個例子中,我們對於Collections.copy(dest,source);
的使用,是如何判斷這麼用合理的呢?
!注意,以下為個人見解,可能不準確或不正確,僅供參考!
之所以不好分析,是因為這個裡頭既有?
又有T
!把人給搞亂了!
::: tip 淺淺區分下?
與T
?
是萬用字元,泛指所有型別。一般用於定義一個引用變數,例如:
SuperClass<?> sup = new SuperClass<String>("lisi");
sup = new SuperClass<People>(new People());
sup = new SuperClass<Animal>(new Animal());
而我們方法上的<T>
代表括號裡面要用到泛型引數,引數的型別為T
。
:::
咱們根據上面所介紹的規則,慢慢理下思路:
-
copy
是個泛型方法,在這個例子中,並沒有指定泛型; - 首先根據型別擦除相關原則確定T:變數
dest
為List<Object>
型別的,變數source
為List<Number>
型別的。這裡根據不指定泛型
的規則,以及在整個函式的宣告中,我們並沒有對T
有任何的約束,故再根據本文開頭部分提到的型別擦除原則,確定下來T
應該是Object
- 接下來和型別擦除就沒有關係了!是能否進行合法引用的問題了!
是看方法中List<? super Object> dest
(注意T
已經換成Object
了)這個引用,能否指向List<Object>
的物件;是看方法中List<? extends Object> src
這個引用,能否指向List<Number>
的物件;
結果是都能,因而合理。