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