面試官:十問泛型,你能扛住嗎?

山禾說發表於2020-06-22

問題一:為什麼需要泛型?

答:

使用泛型機制編寫的程式碼要比那些雜亂的使用Object變數,然後再進行強制型別轉換的程式碼具有更好的安全性和可讀性,也就是說使用泛型機制編寫的程式碼可以被很多不同型別的物件所重用。

問題二:從ArrayList的角度說一下為什麼要用泛型?

答:

在Java增加泛型機制之前就已經有一個ArrayList類,這個ArrayList類的泛型概念是使用繼承來實現的。

public class ArrayList {
    private Object[] elementData;
    public Object get(int i) {....}
    public void add(Object o) {....}
}

這個類存在兩個問題:

  1. 當獲取一個值的時候必須進行強制型別轉換
  2. 沒有錯誤檢查,可以向陣列中新增任何類的物件
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<TU{...}

類定義中的型別變數是指定方法的返回型別以及域和區域性變數的型別

//域
private T one;
//返回型別
public T getOne() return one; }
//區域性變數
public void setOne(T newValue) { one = newValue; }

使用具體的型別代替型別變數就可以例項化泛型型別:

Couple<Rapper>

泛型類可以看成是普通類的工廠,打個比方:我用泛型造了一個模型,具體填充什麼樣的材質,由使用者去做決定。

問題四: 說說泛型方法的定義和使用

答:

泛型方法可以定義在普通類中,也可以定義在泛型類中,型別變數是放在修飾符的後面返回型別的前面

我們來看一個泛型方法的例項:

class ArrayUtil {

    public static <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>

在型別變數的繼承中,可以根據需要擁有多個介面超型別,但是限定中至多有一個類。如果用一個類作為限定,它必定是限定列表中的第一個

型別變數的限定是為了限制泛型的行為,指定了只有實現了特定介面的類才可以作為型別變數去例項化一個類。

問題七:泛型與繼承你知道多少?

答:

首先,我們來看一個類和它的子類,比如 SingerRapper。但是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方法的具體型別,因此呼叫這個方法時不能接收型別為SingerObject的引數。只能傳遞Rapper型別的物件,或者某個子型別(Reader)物件。而且,如果呼叫getOne,不能保證返回物件的型別。

總結一下:

帶有超型別限定的萬用字元可以向泛型物件寫入,帶有子型別限定的萬用字元可以從泛型物件讀取。

問題九:泛型在虛擬機器中是什麼樣呢?

答:

  1. 虛擬機器沒有泛型型別物件,所有的物件都屬於普通類。 無論何時定義一個泛型型別,都自動提供了一個相應的原始型別。原始型別的名字就是刪去型別引數後的泛型型別名。擦除型別變數,並替換成限定型別(沒有限定的變數用Object)。這樣做的目的是為了讓非泛型的Java程式在後續支援泛型的 jvm 上還可以執行(向後相容)

  2. 當程式呼叫泛型方法時,如果擦除返回型別,編譯器插入強制型別轉換。

Couple<Singer> cps = ...;
Singer one = cp.getOne();

擦除cp.getOne的返回型別後將返回Object型別。編譯器自動插入Singer的強制型別轉換。也就是說,編譯器把這個方法呼叫編譯為兩條虛擬機器指令:

對原始方法cp.getOne的呼叫 將返回的Object型別強制轉換為Singer型別。

  1. 當存取一個公有泛型域時也要插入強制型別轉換。
//我們寫的程式碼
Singer one = cps.one;
//編譯器做的事情
Singer one = (Singer)cps.one;

問題十:關於泛型擦除,你知道多少?

答:

型別擦除會出現在泛型方法中,程式設計師通常認為下述的泛型方法

public static <T extends Comparable> 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泛型轉換的幾個點:

  1. 虛擬機器中沒有泛型,只有普通的類和方法
  2. 所有的型別引數都用它們的限定型別替換
  3. 橋方法被合成來保持多型
  4. 為保持型別安全性,必要時插入強制型別轉換

相關文章