《回爐重造》——泛型

泛型

前言

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

泛型

什麼是泛型?

泛型(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 方法

最後的最後

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

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

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