《回爐重造》——泛型
泛型
前言
以前學習到「泛型」的時候,只是淺淺的知道可以限制類型,並沒有更深入理解,可以說基礎的也沒理解到位,只是浮於表面,所以,現在回爐重造,重學泛型!打好基礎!
什麼是泛型?
泛型(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
方法
最後的最後
由本人水平所限,難免有錯誤以及不足之處, 螢幕前的靚仔靚女們
如有發現,懇請指出!
最後,謝謝你看到這裡,謝謝你認真對待我的努力,希望這篇部落格對你有所幫助!
你輕輕地點了個贊,那將在我的心裡世界增添一顆明亮而耀眼的星!