面試官:十問泛型,你能扛住嗎?
- 2020 年 6 月 22 日
- 筆記
- Java Grammar, 面試
問題一:為什麼需要泛型?
答:
使用泛型機制編寫的程式碼要比那些雜亂的使用Object
變數,然後再進行強制類型轉換的程式碼具有更好的安全性和可讀性,也就是說使用泛型機制編寫的程式碼可以被很多不同類型的對象所重用。
問題二:從ArrayList的角度說一下為什麼要用泛型?
答:
在Java增加泛型機制之前就已經有一個ArrayList
類,這個ArrayList
類的泛型概念是使用繼承來實現的。
public class ArrayList {
private Object[] elementData;
public Object get(int i) {....}
public void add(Object o) {....}
}
這個類存在兩個問題:
-
當獲取一個值的時候必須進行強制類型轉換 -
沒有錯誤檢查,可以向數組中添加任何類的對象
ArrayList files = new ArrayList();
files.add(new File(""));
String filename = (String)files.get(0);
對於這個調用,編譯和運行都不會出錯,但是當我們在其他地方使用get方法獲取剛剛存入的這個File
對象強轉為String
類型的時候就會產生一個錯誤。
泛型對於這種問題的解決方案是提供一個類型參數。
ArrayList<String> files = new ArrayList<>();
這樣可以使程式碼具有更好的可讀性,我們一看就知道這個數據列表中包含的是String
對象。
編譯器也可以很好地利用這個資訊,當我們調用get
的時候,不需要再使用強制類型轉換,編譯器就知道返回值類型為String
,而不是Object
:
String filename = files.get(0);
編譯器還知道ArrayList<String>
中add
方法中有一個類型為String
的參數。這將比使用Object
類型的參數安全一些,現在編譯器可以檢查,避免插入錯誤類型的對象:
files.add(new File(""));
這樣的程式碼是無法通過編譯的,出現編譯錯誤比類在運行時出現類的強制類型轉換異常要好得多。
問題三:說說泛型類吧
一個泛型類就是具有一個或多個類型變數的類,對於這個類來說,我們只關注泛型,而不會為數據存儲的細節煩惱。
public class Couple<T> {
private T one;
private T two;
}
Singer
類引入了一個類型變數T,用尖括弧括起來,並放在類名的後面。泛型類可以有多個類型變數:
public class Couple<T, U> {...}
類定義中的類型變數是指定方法的返回類型以及域和局部變數的類型
//域
private T one;
//返回類型
public T getOne() { return one; }
//局部變數
public void setOne(T newValue) { one = newValue; }
使用具體的類型代替類型變數就可以實例化泛型類型:
Couple<Rapper>
泛型類可以看成是普通類的工廠,打個比方:我用泛型造了一個模型,具體填充什麼樣的材質,由使用者去做決定。
問題四: 說說泛型方法的定義和使用
答:
泛型方法可以定義在普通類中,也可以定義在泛型類中,類型變數是放在修飾符的後面,返回類型的前面。
我們來看一個泛型方法的實例:
class ArrayUtil {
public static <T> T getMiddle(T...a){
return a[a.length / 2];
}
}
當調用一個泛型方法時,在方法名前的尖括弧中放入具體的類型:
String middle = ArrayUtil.<String>getMiddle("a","b","c");
在這種情況下,方法調用中可以省略<String>
類型參數,編譯器會使用類型推斷來推斷出所調用的方法,也就是說可以這麼寫:
String middle = ArrayAlg.getMiddle("a","b","c");
問題五:E V T K ?
這些是什麼
答:
-
E
——Element 表示元素 特性是一種枚舉 -
T
——Type 類,是指Java類型 -
K
—— Key 鍵 -
V
——Value 值 -
?
——在使用中表示不確定類型
問題六:了解過類型變數的限定嗎?
答:
一個類型變數或通配符可以有多個限定,例如:
<T extends Serializable & Cloneable>
單個類型變數的多個限定類型使用&
分隔,而,
用來分隔多個類型變數。
<T extends Serializable,Cloneable>
在類型變數的繼承中,可以根據需要擁有多個介面超類型,但是限定中至多有一個類。如果用一個類作為限定,它必定是限定列表中的第一個。
類型變數的限定是為了限制泛型的行為,指定了只有實現了特定介面的類才可以作為類型變數去實例化一個類。
問題七:泛型與繼承你知道多少?
答:
首先,我們來看一個類和它的子類,比如 Singer
和 Rapper
。但是Couple<Rapper>
卻並不是Couple<Singer>
的一個子類。
無論S和T有什麼聯繫,Couple<S>
與Couple<T>
沒有什麼聯繫。
這裡需要注意泛型和Java數組之間的區別,可以將一個Rapper[]
數組賦給一個類型為Singer[]
的變數:
Rapper[] rappers = ...;
Singer[] singer = rappers;
然而,數組帶有特別的保護,如果試圖將一個超類存儲到一個子類數組中,虛擬機會拋出ArrayStoreException
異常。
問題八:聊聊通配符吧
答:
通配符類型中,允許類型參數變化。比如,通配符類型:
Couple<? extends Singer>
表示任何泛型類型,它的類型參數是Singer
的子類,如Couple<Rapper>
,但不會是Couple<Dancer>
。
假如現在我們需要編寫一個方法去列印一些東西:
public static void printCps(Couple<Rapper> cps) {
Rapper one = cp.getOne();
Rapper two = cp.getTwo();
System.out.println(one.getName() + " & " + two.getName() + " are cps.");
}
正如前面所講到的,不能將Couple<Rapper>
傳遞給這個方法,這一點很受限制。解決的方案很簡單,使用通配符類型:
public static void printCps(Couple< ? extends Singer> cps)
Couple<Rapper>
是Couple< ? extends Singer>
的子類型。
我們接下來來考慮另外一個問題,使用通配符會通過Couple< ? extends Singer>
的引用破壞Couple<Rapper>
嗎?
Couple<Rapper> rapper = new Couple<>(rapper1, rapper2);
Couple<? extends Singer> singer = rapper;
player.setOne(reader);
這樣可能會引起破壞,但是當我們調用setOne
的時候,如果調用的不是Singer
的子類Rapper
類的對象,而是其他Singer
子類的對象,就會出錯。
我們來看一下Couple<? extends Singer>
的方法:
? extends Singer getOne();
void setOne(? extends Singer);
這樣就會看的很明顯,因為如果我們去調用setOne()
方法,編譯器之可以知道是某個Singer
的子類型,而不能確定具體是什麼類型,它拒絕傳遞任何特定的類型,因為 ? 不能用來匹配。
但是使用getOne
就不存在這個問題,因為我們無需care它獲取到的類型是什麼,但一定是Singer
的子類。
通配符限定與類型變數限定非常相似,但是通配符類型還有一個附加的能力,即可以指定一個超類型限定:
? super Rapper
這個通配符限制為Rapper
的所有父類,為什麼要這麼做呢?帶有超類型限定的通配符的行為與子類型限定的通配符行為完全相反,可以為方法提供參數,但是卻不能獲取具體的值,即訪問器是不安全的,而更改器方法是安全的:
編譯器無法知道setOne
方法的具體類型,因此調用這個方法時不能接收類型為Singer
或Object
的參數。只能傳遞Rapper
類型的對象,或者某個子類型(Reader
)對象。而且,如果調用getOne
,不能保證返回對象的類型。
總結一下:
帶有超類型限定的通配符可以向泛型對象寫入,帶有子類型限定的通配符可以從泛型對象讀取。
問題九:泛型在虛擬機中是什麼樣呢?
答:
-
虛擬機沒有泛型類型對象,所有的對象都屬於普通類。
無論何時定義一個泛型類型,都自動提供了一個相應的原始類型。原始類型的名字就是刪去類型參數後的泛型類型名。擦除類型變數,並替換成限定類型(沒有限定的變數用Object
)。這樣做的目的是為了讓非泛型的Java
程式在後續支援泛型的jvm
上還可以運行(向後兼容) -
當程式調用泛型方法時,如果擦除返回類型,編譯器插入強制類型轉換。
Couple<Singer> cps = ...;
Singer one = cp.getOne();
擦除cp.getOne
的返回類型後將返回Object
類型。編譯器自動插入Singer
的強制類型轉換。也就是說,編譯器把這個方法調用編譯為兩條虛擬機指令:
對原始方法
cp.getOne
的調用
將返回的Object類型強制轉換為Singer
類型。
-
當存取一個公有泛型域時也要插入強制類型轉換。
//我們寫的程式碼
Singer one = cps.one;
//編譯器做的事情
Singer one = (Singer)cps.one;
問題十:關於泛型擦除,你知道多少?
答:
類型擦除會出現在泛型方法中,程式設計師通常認為下述的泛型方法
public static <T extends Comparable> T min(T[] a)
是一個完整的方法族,而擦除類型之後,只剩下一個方法:
public static Comparable min(Comparable[] a)
這個時候類型參數T已經被擦除了,只留下了限定類型Comparable
。
但是方法的擦除會帶來一些問題:
class Coupling extends Couple<People> {
public void setTwo(People people) {
super.setTwo(people);
}
}
擦除後:
class Coupling extends Couple {
public void setTwo(People People) {...}
}
這時,問題出現了,存在另一個從Couple
類繼承的setTwo
方法,即:
public void setTwo(Object two)
這顯然是一個不同的方法,因為它有一個不同類型的參數(Object
),而不是People
。
Coupling coupling = new Coupling(...);
Couple<People> cp = interval;
cp.setTwo(people);
這裡,希望對setTwo
的調用具有多態性,並調用最合適的那個方法。由於cp
引用Coupling
對象,所以應該調用Coupling.setTwo
。問題在於類型擦除與多態發生了衝突。要解決這個問題,就需要編譯器在Coupling
類中生成一個橋方法:
public void setTwo(Object second) {
setTwo((People)second);
}
變數cp
已經聲明為類型Couple<LocalDate>
,並且這個類型只有一個簡單的方法叫setTwo
,即setTwo(Object)
。虛擬機用cp
引用的對象調用這個方法。這個對象是Coupling
類型的,所以會調用Coupling.setTwo(Object)
方法。這個方法是合成的橋方法。它會調用Coupling.setTwo(Date)
,這也正是我們所期望的結果。
所以,我們要記住關於Java泛型轉換的幾個點:
-
虛擬機中沒有泛型,只有普通的類和方法 -
所有的類型參數都用它們的限定類型替換 -
橋方法被合成來保持多態 -
為保持類型安全性,必要時插入強制類型轉換
