Java這個高階特性,很多人還沒用過!
泛型是 Java 的高階特性之一,如果想寫出優雅而高擴充套件性的程式碼,或是想讀得懂一些優秀的原始碼,泛型是繞不開的檻。本文介紹了什麼是泛型、型別擦除的概念及其實現,最後總結了泛型使用的最佳實踐。
前言
想寫一下關於 Java 一些高階特性的文章,雖然這些特性在平常實現普通業務時不必使用,但如果想寫出優雅而高擴充套件性的程式碼,或是想讀得懂一些優秀的原始碼,這些特性又是不可避免的。
如果對這些特性不瞭解,不熟悉特性的應用場景,使用時又因為語法等原因困難重重,很難讓人克服惰性去使用它們,所以身邊總有一些同事,工作了很多年,卻從沒有用過 Java 的某些高階特性,寫出的程式碼總是差那麼一點兒感覺。
為了避免幾年後自己的程式碼還是非常 low,我準備從現在開始深入理解一下這些特性。本文先寫一下應用場景最多的泛型。
泛型是什麼
首先來說泛型是什麼。泛型的英文是 generic,中文意思是通用的、一類的,結合其應用場景,我理解泛型是一種 通用型別。但我們一般指泛型都是指其實現方式,也就是 將型別引數化
對於 Java 這種強型別語言來說,如果沒有泛型的話,處理相同邏輯不同型別的需求會非常麻煩。
如果想寫一個對 int 型資料的快速排序,我們編碼為(不是主角,網上隨便找的=_=):
public static void quickSort(int[] data, int start, int end) {
int key = data[start];
int i = start;
int j = end;
while (i key && j > i) {
j--;
}
data[i] = data[j];
while (data[i] start) {
quickSort(data, start, i - 1);
}
if (i + 1
可是如果需求變了,現在需要實現 int 和 long 兩種資料型別的快排,那麼我們需要利用 Java 類方法過載功能,複製以上程式碼,將引數型別改為 double 貼上一遍。可是,如果還要實現 float、double 甚至字串、各種類的快速排序呢,難道每新增一種型別就要複製貼上一遍程式碼嗎,這樣未必太不優雅。
當然我們也可以宣告傳入引數為 Object,並在比較兩個元素大小時,判斷元素型別,並使用對應的方法比較。這樣,程式碼就會噁心在型別判斷上了。不優雅的範圍小了一點,並不能解決問題。
這時,我們考慮使用通用型別(泛型),將快排方法的引數設定為一個通用型別,無論什麼樣的引數,只要實現了 Comparable 介面,都可以傳入並排序。
public static > void quickSort(T[] data, int start, int end) {
T key = data[start];
int i = start;
int j = end;
while (i 0 && j > i) {
j--;
}
data[i] = data[j];
while (data[i].compareTo(key) start) {
quickSort(data, start, i - 1);
}
if (i + 1
那麼,可以總結一下泛型的應用場景了,當遇到以下場景時,我們可以考慮使用泛型:
當引數型別不明確,可能會擴充套件為多種時。
想宣告引數型別為 Object,並在使用時用 instanceof 判斷時。
需要注意,泛型只能替代Object的子型別,如果需要替代基本型別,可以使用包裝類,至於為什麼,會在下文中說明。
使用
然後我們來看一下,泛型怎麼用。
宣告
泛型的宣告使用 的形式,需要在一個地方同時宣告多個佔位符時,使用 , 隔開。佔位符的格式並無限制,不過一般約定使用單個大寫字母,如 T 代表型別(type),E 代表元素*(element)等。雖然沒有嚴格規定,不過為了程式碼的易讀性,最好使用前檢查一下約定用法。
泛型指代一種引數型別,可以宣告在類、方法和介面上。
我們最常把泛型宣告在類上:
class Generics { // 在類名後宣告引入泛型型別
private T field; // 引入後可以將欄位宣告為泛型型別
public T getField() { // 類方法內也可以使用泛型型別
return field;
}
}
把泛型宣告在方法上時:
public [static] void testMethod(T arg) { // 訪問限定符[靜態方法在 static] 後使用 宣告泛型方法後,在引數列表後就可以使用泛型型別了
// doSomething
}
最後是在介面中宣告泛型,如上面的快排中,我們使用了 Comparable 的泛型介面,與此類似的還有 Searializable Iterable等,其實在介面中宣告與在類中宣告並沒有什麼太大區別。
呼叫
然後是泛型的呼叫,泛型的呼叫和普通方法或類的呼叫沒有什麼大的區別,如下:
public static void main(String[] args) {
String[] strArr = new String[2];
// 泛型方法的呼叫跟普通方法相同
Generics.quickSort(strArr, 0, 30 );
// 泛型類在呼叫時需要宣告一種精確型別
Generics sample = new Generics();
Long field = sample.getField();
}
// 泛型介面需要在泛型類裡實現
class GenericsImpl implements Comparable {
@Override
public int compareTo(T o) {
return 0;
}
}
型別擦除
講泛型不可不提型別擦除,只有明白了型別擦除,才算明白了泛型,也就可以避開使用泛型時的坑。
由來
嚴格來說,Java的泛型並不是真正的泛型。Java 的泛型是 JDK1.5 之後新增的特性,為了相容之前版本的程式碼,其實現引入了型別擦除的概念。
型別擦除指的是:Java 的泛型程式碼在編譯時,由編譯器進行型別檢查,之後會將其泛型型別擦除掉,只儲存原生型別,如 Generics 被擦除後是 Generics,我們常用的 List 被擦除後只剩下 List。
接下來的 Java 程式碼在執行時,使用的還是原生型別,並沒有一種新的型別叫 泛型。這樣,也就相容了泛型之前的程式碼。
如以下程式碼:
public static void main(String[] args) {
List stringList = new ArrayList();
List longList = new ArrayList();
if (stringList.getClass() == longList.getClass()) {
System.out.println(stringList.getClass().toString());
System.out.println(longList.getClass().toString());
System.out.println("type erased");
}
}
結果 longList 和 stringList 輸出的型別都為 class java.util.ArrayList,兩者型別相同,說明其泛型型別被擦除掉了。
實際上,實現了泛型的程式碼的位元組碼內會有一個 signature 欄位,其中指向了常量表中泛型的真正型別,所以泛型的真正型別,還可以透過反射獲取得到。
實現
那麼型別擦除之後,Java 是如何保證泛型程式碼執行期間沒有問題的呢?
我們將一段泛型程式碼用 javac 命令編譯成 class 檔案後,再使用 javap 命令檢視其位元組碼資訊:
我們會發現,型別裡的 T 被替換成了 Object 型別,而在 main 方法裡 getField 欄位時,進行了型別轉換(checkcast),如此,我們可以看出來 Java 的泛型實現了,一段泛型程式碼的編譯執行過程如下:
- 編譯期間編譯器檢查傳入的泛型型別與宣告的泛型型別是否匹配,不匹配則報出編譯器錯誤;
- 編譯器執行型別擦除,位元組碼內只保留其原始型別;
- 執行期間,再將 Object 轉換為所需要的泛型型別。
也就是說:Java 的泛型實際上是由編譯器實現的,將泛型型別轉換為 Object 型別,在執行期間再進行狀態轉換。
實踐問題
由上,我們來看使用泛型時需要注意的問題:
具體型別須為Object子型別
上文中提到實現泛型時宣告的具體型別必須為 Object 的子型別,這是因為編譯器進行型別擦除後會使用 Object 替換泛型型別,並在執行期間進行型別轉換,而基礎型別和 Object 之間是無法替換和轉換的。
如:Generics generics = new Generics(); 在編譯期間就會報錯的。
邊界限定萬用字元的使用
泛型雖然為通用型別,但也是可以設定其通用性的,於是就有了邊界限定萬用字元,而邊界萬用字元要配合型別擦除才好理解。
extends Generics> 是上邊界限定萬用字元,避開 上邊界 這個比較模糊的詞不談,我們來看其宣告 xx extends Generics, XX 是繼承了 Generics 的類(也有可能是實現,下面只說繼承),我們按照以下程式碼宣告:
List extends Generics> genericsList = new ArrayList();
Generics generics = genericsList.get(0);
genericsList.add(new Generics()); // 編譯無法透過
我們會發現最後一行編譯報錯,至於為什麼,可以如此理解:XX 是繼承了 Generics 的類,List 中取出來的類一定是可以轉換為 Generics,所以 get 方法沒問題;而具體是什麼類,我們並不知道,將父類強制轉換成子類可能會造成執行期錯誤,所以編譯器不允許這種情況;
而同理 super Generics> 是下邊界限定萬用字元, XX 是 Generics 的父類,所以:
List super Generics> genericsList = new ArrayList();
genericsList.add(new Generics()); // 編譯無法透過
Generics generics = genericsList.get(0);
使用前需要根據這兩種情況,考慮需要 get 還是 set, 進而決定用哪種邊界限定萬用字元。
最佳實踐
當然,泛型並不是一個萬能容器。什麼型別都往泛型裡扔,還不如直接使用 Object 型別。
什麼時候確定用泛型,如何使用泛型,這些問題的解決不僅僅只依靠程式設計經驗,我們使用開頭快排的例子整理一下泛型的實踐方式:
- 將程式碼邏輯拆分為兩部分:通用邏輯和型別相關邏輯;通用邏輯是一些跟引數型別無關的邏輯,如快排的元素位置整理等;型別相關邏輯,顧名思義,是需要確定型別後才能編寫的邏輯,如元素大小的比較,String 型別的比較和 int 型別的比較就不一樣。
- 如果沒有型別相關的邏輯,如 List 作為容器不需要考慮什麼型別,那麼直接完善通用程式碼即可。
如果有引數型別相關的邏輯,那麼就需要考慮這些邏輯是否已有共同的介面實現,如果已有共同的介面實現,可以使用邊界限定萬用字元。如快排的元素就實現了 Compare 介面,Object 已經實現了 toString() 方法,所有的列印語句都可以呼叫它。 - 如果還沒有共同的介面,那麼需要考慮是否可以抽象出一個通用的介面實現,如列印人類的衣服顏色和動物的毛皮顏色,就可以抽象出一個 getColor() 介面,抽象之後再使用邊界限定萬用字元。
如果無法抽象出通用介面,如輸出人類身高或動物體重這種,還是不要使用泛型了,因為不限定型別的話,具體型別的方法呼叫也就無從談起,編譯也無法透過。
我將以上步驟整理了一個流程圖,按照這個圖,我們可以快速得出能不能用泛型,怎麼用泛型。
小結
好好理了一下泛型,感覺收穫頗多,Java 迷霧被撥開了一些。這些特性確實挺難纏,每當自己覺得已經理解得差不多的時候,過些日子又覺得當初理解得還不夠。重要的還是要實踐,在使用時會很容易發現疑惑的地方。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2318/viewspace-2797647/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- find 命令這 7 種高階用法,你肯定沒用過?
- 這都Java15了,Java7特性還沒整明白?Java
- Java高階特性—泛型Java泛型
- Java高階特性之集合Java
- Java高階特性增強-鎖Java
- 《Java 高階篇》八:新特性Java
- Java高階特性泛型看這一篇就夠了Java泛型
- 從沒見過這麼牛的“Java進階面經”Java
- 突破這個四個階段年薪沒有50W,還好意思說是搞Java的?Java
- mysql高階特性MySql
- Python 高階特性Python
- Mongodb高階特性MongoDB
- Redis高階特性Redis
- RocketMQ高階特性MQ
- (小白學JAVA之)Java高階特性知識點梳理Java
- 【Java8新特性】還沒搞懂函式式介面?趕快過來看看吧!Java函式
- Laravel的這10個用法,你都沒用過吧!!Laravel
- SpringIOC的高階特性Spring
- Java高階特性1_流庫_初體驗Java
- 初學Java,這三個階段你經歷過嗎?Java
- RabbitMQ(二):RabbitMQ高階特性MQ
- MySQL 高階特性篇教程MySql
- JavaScript高階特性 — 作用域JavaScript
- Redis 高階特性 Redis Stream使用Redis
- PoweJob高階特性-MapReduce完整示例
- Java 中你絕對沒用過的一個關鍵字?Java
- 面試官:說一說CyclicBarrier的妙用!我:這個沒用過面試
- Java高階用法,寫個代理侵入你 ?Java
- Java高階特性-註解:註解實現Excel匯出功能JavaExcel
- 你一定需要知道的高階JAVA列舉特性!Java
- 超詳細MySQL入門教程,這11個MySQL資料庫的高階特性你知道嗎?MySql資料庫
- 這麼優雅的Java ORM沒見過吧!JavaORM
- MySQL高階特性——繫結變數MySql變數
- Tomcat高階特性及效能調優Tomcat
- Python 高階特性(4)- 生成器Python
- IntelliJ IDEA 老司機,還沒用過 Stream Trace 功能?IntelliJIdea
- 高階面試必備:一個Java物件佔用多大記憶體面試Java物件記憶體
- 【Java 8 新特性】Java Stream 通過skip()方法跳過前N個子元素Java