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

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

答:

使用泛型機制編寫的代碼要比那些雜亂的使用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. 為保持類型安全性,必要時插入強制類型轉換

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

您可能也會喜歡…