《回爐重造》——泛型

2022-05-23 21:11:44

泛型

前言

以前學習到「泛型」的時候,只是淺淺的知道可以限制型別,並沒有更深入理解,可以說基礎的也沒理解到位,只是浮於表面,所以,現在回爐重造,重學泛型!打好基礎!

什麼是泛型?

泛型(Generic),Generic 的意思有「一般化的,通用的」。

是 JDK 5 中引入的新特性,它提供編譯時的型別安全檢測,允許我們在編譯時檢測到非法的資料型別,本質是 引數化型別。

這裡還涉及到一個詞「引數化型別」。什麼意思呢?

意思就是:把型別引數化(只能感慨中國文化博大精深),即我們可以把型別作為引數,換句話說,就是所操作的資料型別被指定為一個引數。

說到引數,我們也熟悉,你看,方法上的形參、呼叫方法時的實參,這些都是引數,對吧。

同理,型別,即 Java 中的各種基本的參照型別,當然包含你自己定義的型別,說白了就是各種類(Class),類可以作為引數,就是上面講的把型別作為引數(好吧,好像講了一堆廢話)。這又涉及到一個詞,即「型別引數」。

我們可以看看 ArrayList 的原始碼,如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    ...
}

ArrayList<E> 中的 <E>,這裡的 E 可以說是一個「型別形參」。

而我們寫 ArrayList<String> list = new ArrayList<>() 的時候,給 ArrayList 這個集合指定了一個具體的型別 String ,形參 E 傳入的實參就是 String,也就是說 String 是一個「型別實參」。

簡而言之:

ArrayList<E> 中的 E 稱為 型別形參ArrayList<String> 中的 String 稱為 型別實參。這兩個合起來,就是上面提到的「型別引數」。

為什麼會有泛型的出現?

泛型和集合有千絲萬縷的關係,我們現在用集合,也是使用「泛型集合」。

List<Integer> list = new ArrayList<>(); // 泛型集合

當然,我們一開始學習的時候,並沒有用到泛型,即非泛型集合。

List list = new ArrayList();	// 非泛型集合

在以前沒有泛型的情況下,我們看看會出現什麼問題。預設 ArrayList 集合中儲存的元素型別是 Object ,這樣很好,Java 中任何型別的終極父類別就是 Object,什麼型別的資料都能儲存到這個集合中。

比如我可以這樣操作(經典案例):

List list = new ArrayList();	// 非泛型集合
list.add("Hello World!");		// 儲存 String 型別
list.add(23);					// 儲存 int 型別,這裡會自動裝箱為 Integer 型別
list.add(true);					// 儲存 Boolean 型別

for (Object o : list) {			// 用 Object 接收,合情合理
    System.out.println(o);
}

我們儲存資料之後,後續肯定需要使用它,就需要從集合中取出來,而取出來進一步操作是需要明確具體的資料型別,那麼就需要進行強制型別轉換。

for (Object o : list) {
    String s = (String) o;		// 強制型別轉換
    // 後續操作...
}

此時程式碼並不會報錯,編譯也不會有問題,直到我們執行時,就會出現異常——ClassCastException

這也是必然的,畢竟我們的集合中還有其他型別的資料,其他型別的資料,再怎樣強制型別轉換也不可能轉成 String 型別,也就會出現異常了。

看到這裡,估計有小夥伴要問了,我一個一個強制轉換不行嗎?我知道儲存的是什麼資料,到時直接獲取相對應的資料進行強轉就行了啊。是,沒錯,你可以一個一個強轉,數量少的情況下是可以,但是你數量很多的情況下呢?你怎麼辦?

所以,泛型出現了,它可以限制下我們在編譯期的型別,保證型別是安全的,即執行時不會發生異常的。

List<String> list = new ArrayList<>();	// 泛型集合
list.add("Hello World!");				// 儲存 String 型別
list.add("23");
list.add("Coding Coding");

for (String s : list) {					// 用 String 接收
    System.out.println(s);
    // 後續操作...
}

看到這裡的小夥伴,可能有這麼一個疑惑:那這樣為什麼不直接使用一個 String 陣列呢?這個問題問得好。

陣列確實能夠儲存同一個資料型別的資料,但是對於想無限制儲存元素時,陣列就有它的缺點,陣列長度是固定不可變。總而言之,陣列使用起來不方便,所以才有集合的出現,而集合又因為有這種問題,進而出現泛型集合。

這裡使用了泛型,那麼我們在 add() 的時候,編譯期間就會對新增的元素進行型別檢查,而且在獲取集合元素的時候,也不需要強制型別轉換了,直接用指定的型別接收就行了。

使用泛型有什麼好處?

  • 無需強制型別轉換(集合、反射)
  • 增加程式碼可讀性,我們可以通過泛型,知道現在操作的是什麼資料型別。一句話,給人看的。
  • 程式碼複用,可以根據不同情況傳入不同的資料型別,進行不同的操作。

泛型類

定義語法:

class 類名<萬用字元,萬用字元,萬用字元...> {
    private 萬用字元 變數名;
    ...
}

萬用字元:T、E、K、V,也就是上面說的型別形參。(這裡的萬用字元,也有人稱為泛型標識)

使用語法:

類名<具體的資料型別> 物件名 = new 類名<具體的資料型別>();
類名<具體的資料型別> 物件名 = new 類名<>();		// JDK 7 開始可以省略,人們稱為 菱形語法

舉個栗子:

/**
	定義泛型類
*/
public class Generic<T> {
    private T variable;
        
    public void setVariable(T variable) {
        return this.variable = variable;
    }
    
    public T getVariable() {
        return variable;
    }
}

測試

Generic<String> g = new Generic<>();	// 指定泛型為 String
g.setVariable("god23bin");				// 正常
g.setVariable(23);						// 提示錯誤,因為這裡是 int 型
String var = g.getVariable();

需要注意的點:

  • 你使用泛型類時,沒有指定資料型別,那麼將預設為 Object 型別

  • 泛型的型別引數,只能是參照資料型別,不支援基本資料型別。

  • 泛型在邏輯上,你操作的是不同的資料型別,但是實際上,還是同樣的型別(比如上面例子中的 Generic 類,泛型指定不同的資料型別,邏輯上是不同的,但是實際上還是 Generic 型別,這裡就涉及到「型別擦除」)

  • 如果有繼承:

// 子類如果需要是泛型類,那麼其型別引數需要包含父類別的型別引數
class ChildGeneric<T> extends Generic<T> {}		// OK
class ChildGeneric<T, E> extends Generic<T> {}	// OK

// 子類不是泛型類,那麼父類別的型別引數需要明確
class ChildGeneric extends Generic<String> {} 

泛型介面

定義語法:

interface 介面名 <萬用字元,萬用字元,萬用字元...> {
    萬用字元 方法名();
    ...
}

使用語法:

// 介面實現類是泛型類,那麼實現類的型別引數需要包含介面的型別引數
class Demo<T> implements Generic<T> {}    // OK
class Demo<T, E> implements Generic<T> {} // OK

// 介面實現類不是泛型類,那麼介面型別引數需要明確
class Demo implements Generic<String> {}

泛型方法

之前是在類和介面上定義了泛型,然而有時候,我們並不需要整個類都定義型別,只需要其中某一個方法定義泛型,只關心這一個方法,這時就可以使用把泛型定義在方法上,這樣呼叫泛型方法的時候,才指定具體的型別引數。

定義語法:

存取修飾符 <萬用字元,萬用字元,萬用字元...> 返回值型別 方法名(形參列表) {
    // 方法體
}

舉個栗子:

public <T, E> void getGeneric() {
    // 方法體
}

public <T, E> void getGeneric(Game<T> game) {
    // 方法體
}

這裡需要注意的是,泛型方法和泛型類中使用了泛型的普通的方法是不一樣的。

// 這是泛型類中使用了泛型的普通的方法
public T getVariable() {
    return variable;
}

// 這是泛型方法,只有定義了 <T,...> 的方法才是泛型方法
public <T, E> void getGeneric() {
    // 方法體
}

而且,如果你在泛型類中定義了泛型方法,那麼泛型方法中的 <T,...> 型別形參和泛型類上的型別形參是不一樣的,是相互獨立的。還有,泛型方法可以定義成靜態的,還沒完,泛型方法還可以結合可變引數。

舉個栗子:

/**
	定義泛型類
*/
public class Generic<T> {
    private T variable;
        
    public void setVariable(T variable) {
        return this.variable = variable;
    }
    
    public T getVariable() {
        return variable;
    }
    
    // 泛型方法,這裡的T和類上的T不是同一個T
    public <T> T getGeneric(List<T> list) {
        return list.get(0);
    }
    
    // 靜態的泛型方法
    public static <T> T getGenericStatic(List<T> list) {
        return list.get(0);
    }
    
    // 結合可變引數的泛型方法
    public static <E> void print(E... e) {
        // 這裡的引數可以當作陣列進行遍歷
        for (E elem : e) {
            System.out.println(elem);
        }
    }
}

萬用字元之問號

之前出現的 T,E,K,V 這些,也都是萬用字元,不過,這些萬用字元是屬於型別形參的萬用字元。那麼型別實參的萬用字元呢?這就來啦!型別實參萬用字元:?。沒錯,你沒看錯,就是一個問號。

型別實參的萬用字元是使用 ? 來代表具體的型別實參的,代表任意型別。

舉個例子:

public class Generic<T> {
    ...
    
    public static void showGame(Games<String> games) { // 要求Games指定的型別為String
        String one = games.getOne();
        System.out.println(one);
    }
    
}

上面要求 Games 指定的型別為 String。那麼我們這樣操作:

Generic<String> g = new Generic<>();

Games<String> games = new Games();
g.showGame(games);		// OK

Games<Integer> games2 = new Games();
g.showGame(games2);		// Error,因為指定了為String

所以使用 ? 萬用字元

public class Generic<T> {
    ...
    
    public static void showGame(Games<?> games) { // 使用型別實參萬用字元 ?
        String one = games.getOne();
        System.out.println(one);
    }
    
}

萬用字元上下限

型別萬用字元的上下限,有的地方也稱為上下界,還有稱限定萬用字元的,意思都一樣。

上限語法:

類/介面<? extends 實參型別>

這裡的 extends 可以這樣理解,<? extends A> ,使用的時候,我們傳入的實參型別需要小於等於A類,即需要是A的子類或A本身,這樣就限制了萬用字元的上限了,你最高只能是A類。

下限語法:

類/介面<? super 實參型別>

這裡的 super 可以這樣理解,<? super A> ,使用的時候,我們傳入的實參型別需要大於等於A類,即需要是A的父類別或A本身,這樣就限制了萬用字元的下限了,你最低只能是A類。

舉個例子,這裡有 A、B、C 三個類,A 是 B 的父類別,B 是 C 的父類別。

public class Demo {
    public static upperLimit(List<? extends B> list) { // 型別實參萬用字元上限為B類
        // ...
    }
    
    public static lowerLimit(List<? super B> list) { // 型別實參萬用字元下限為B類
        // ...
    }
}

呼叫這個方法

List<A> l1 = new ArrayList<>();
List<B> l2 = new ArrayList<>();
List<C> l3 = new ArrayList<>();

Demo.upperLimit(l1);	// Error,這裡傳入 l1,而上面搞了萬用字元上限,超過了B類,比B類還上
Demo.upperLimit(l2);	// OK
Demo.upperLimit(l3);	// OK

Demo.lowerLimit(l1);	// OK
Demo.lowerLimit(l2);	// OK
Demo.lowerLimit(l3);	// Error,同理,比B類還下,自然錯誤,需要比B類上,超過B類才行

需要注意的是,你搞了萬用字元的上限,在集合中,那麼是隻能用來讀取資料,而不能用來儲存資料,這該怎麼理解呢?

public class Demo {
    public static upperLimit(List<? extends B> list) { 	// 型別實參萬用字元上限為B類
        list.add(new B()); // Error
        list.add(new C()); // Error
        // 因為我們使用上限萬用字元,不知道傳入進來的 List 是什麼型別的,可能是List<B>,可能是List<C>
        // 所以是不能進行儲存資料的
    }
    
    public static lowerLimit(List<? super B> list) { 	// 型別實參萬用字元下限為B類
        // ...
    }
}

那麼下限呢?放心,下限沒有這個問題,可以儲存資料。

public class Demo {
    public static upperLimit(List<? extends B> list) { 	// 型別實參萬用字元上限為B類
        // ...
    }
    
    public static lowerLimit(List<? super B> list) { 	// 型別實參萬用字元下限為B類
        list.add(new B());
        list.add(new C());
        // 因為下限萬用字元,只限定了下限,但是上限是沒有限制的,也就是說可以看成上限就是 Object
        // 上限是 Object,那麼任何類都預設繼承 Object,那麼自然可以新增 C 型別的資料
        // 也就是儲存資料的型別是沒有限制的。
        for (Object o : list) {
            System.out.println(o);
        }
    }
}

型別擦除

泛型的限制,只在編譯期存在,一旦在執行了,那麼便消失了,即型別被擦除了。

有兩種情況:

  • 無限制型別擦除
  • 有限制型別擦除

無限制:

有限制:

泛型方法上的型別擦除也是同理。還有一個知識點就是,在泛型介面的型別擦除中,會出現一個「橋接方法」,主要是保持介面和類的實現關係。

以上,就是泛型的基本內容了。

面試題

開始回顧八股文!!!

Java 泛型是什麼?常用的萬用字元有哪些?

泛型(Generics)是 JDK5 中引入的一個新特性,它提供了編譯時型別安全檢測的機制。這個機制可以在編譯時就檢測到非法的資料型別。本質是一個引數化型別,就是所操作的資料型別可以被指定為一個特定的引數型別。

常用的萬用字元有 T(Type)、K(Key)、V(Value)、E(Element)、?(未知型別)

Java 的泛型是如何工作的 ? 什麼是型別擦除?(泛型擦除是什麼?)

Java 的泛型是偽泛型,因為在 Java 執行期間,這些泛型資訊都會被擦掉,就是所謂的型別擦除(泛型擦除)。

什麼是泛型中的限定萬用字元和非限定萬用字元?

限定萬用字元,顧名思義,就是對型別進行限定,Java 中有兩種限定萬用字元。

一種是 < ? extends T >,它通過確保型別必須是T的子類來限定上界,即型別必須為T型別或者T子類

另一種是< ? super T >,它通過確保型別必須是T的父類別來限定下屆,即型別必須為T型別或者T的父類別

< ? > 表示了非限定萬用字元,因為 < ? > 可以用任意型別來替代。

你的專案中哪裡用到了泛型?

  • 可用於定義通用返回結果類 CommonResult 通過引數 T 可根據具體的返回型別動態指定結果的資料型別
  • 用於構建集合工具類。參考 Collections 中的 sort, binarySearch 方法

最後的最後

由本人水平所限,難免有錯誤以及不足之處, 螢幕前的靚仔靚女們 如有發現,懇請指出!

最後,謝謝你看到這裡,謝謝你認真對待我的努力,希望這篇部落格對你有所幫助!

你輕輕地點了個贊,那將在我的心裡世界增添一顆明亮而耀眼的星!