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 < j) {
          while (data[j] > key && j > i) {
              j--;
          }
          data[i] = data[j];

          while (data[i] < key && i < j) {
              i++;
          }
          data[j] = data[i];
      }
      data[i] = key;

      if (i - 1 > start) {
          quickSort(data, start, i - 1);
      }
      if (i + 1 < end) {
          quickSort(data, i + 1, end);
      }
}

可是如果需求變了,現在需要實現 int 和 long 兩種數據類型的快排,那麼我們需要利用 Java 類方法重載功能,複製以上代碼,將參數類型改爲 double 粘貼一遍。可是,如果還要實現 float、double 甚至字符串、各種類的快速排序呢,難道每添加一種類型就要複製粘貼一遍代碼嗎,這樣未必太不優雅。

當然我們也可以聲明傳入參數爲 Object,並在比較兩個元素大小時,判斷元素類型,並使用對應的方法比較。這樣,代碼就會噁心在類型判斷上了。不優雅的範圍小了一點,並不能解決問題。

這時,我們考慮使用通用類型(泛型),將快排方法的參數設置爲一個通用類型,無論什麼樣的參數,只要實現了 Comparable 接口,都可以傳入並排序。

public static  <T extends Comparable<T>> void quickSort(T[] data, int start, int end) {
     T key = data[start];
     int i = start;
     int j = end;
     while (i < j) {
        while (data[j].compareTo(key) > 0 && j > i) {
            j--;
        }
        data[i] = data[j];

        while (data[i].compareTo(key) < 0 && i < j) {
            i++;
        }
        data[j] = data[i];
     }
     data[i] = key;

     if (i - 1 > start) {
         quickSort(data, start, i - 1);
     }
     if (i + 1 < end) {
         quickSort(data, i + 1, end);
     }
}

那麼,可以總結一下泛型的應用場景了,當遇到以下場景時,我們可以考慮使用泛型:

當參數類型不明確,可能會擴展爲多種時。想聲明參數類型爲 Object,並在使用時用 instanceof 判斷時。需要注意,泛型只能替代 Object 的子類型,如果需要替代基本類型,可以使用包裝類,至於爲什麼,會在下文中說明。

泛型的應用

然後來看一下,泛型如何應用。

聲明

泛型的聲明使用 <佔位符 [, 另一個佔位符] > 的形式,需要在一個地方同時聲明多個佔位符時,使用 , 隔開。佔位符的格式並無限制,不過一般約定使用單個大寫字母,如 T 代表類型(type),E 代表元素 *(element)等。雖然沒有嚴格規定,不過爲了代碼的易讀性,最好使用前檢查一下約定用法。

泛型指代一種參數類型,可以聲明在類、方法和接口上。

最常把泛型聲明在類上:

class Generics<T> { // 在類名後聲明引入泛型類型
     private T field;  // 引入後可以將字段聲明爲泛型類型

     public T getField() { // 類方法內也可以使用泛型類型
         return field;
     }
}

把泛型聲明在方法上時:

 public [static] <T> void testMethod(T arg) { // 訪問限定符[靜態方法在 static] 後使用 <佔位符> 聲明泛型方法後,在參數列表後就可以使用泛型類型了
    // doSomething
}

最後是在接口中聲明泛型,如上面的快排中,我們使用了 Comparable 的泛型接口,與此類似的還有 SearializableIterable 等,其實在接口中聲明與在類中聲明並沒有什麼太大區別。

調用

然後是泛型的調用,泛型的調用和普通方法或類的調用沒有什麼大的區別,如下:

public static void main(String[] args) {
    String[] strArr = new String[2];
        // 泛型方法的調用跟普通方法相同
  Generics.quickSort(strArr, 0, 30 );

  // 泛型類在調用時需要聲明一種精確類型
    Generics<Long> sample = new Generics<>();
    Long field = sample.getField();
}

// 泛型接口需要在泛型類裏實現
class GenericsImpl<T> implements Comparable<T> {
    @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<String> stringList = new ArrayList<>();
     List<Long> 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<int> generics = new Generics<int>(); 在編譯期間就會報錯的。

邊界限定通配符的使用

泛型雖然爲通用類型,但也是可以設置其通用性的,於是就有了邊界限定通配符,而邊界通配符要配合類型擦除纔好理解。

<? extends Generics> 是上邊界限定通配符,避開 上邊界 這個比較模糊的詞不談,我們來看其聲明 xx extends Generics, XX 是繼承了 Generics 的類(也有可能是實現,下面只說繼承),我們按照以下代碼聲明:

List<? extends Generics> genericsList = new ArrayList<>();
Generics generics = genericsList.get(0);
genericsList.add(new Generics<String>()); // 編譯無法通過

我們會發現最後一行編譯報錯,至於爲什麼,可以如此理解: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 類型。

什麼時候確定用泛型,如何使用泛型,這些問題的解決不僅僅只依靠編程經驗,我們使用開頭快排的例子整理一下泛型的實踐方式:

小結

好好理了一下泛型,感覺收穫頗多,Java 迷霧被撥開了一些。這些特性確實挺難纏,每當自己覺得已經理解得差不多的時候,過些日子又覺得當初理解得還不夠。重要的還是要實踐,在使用時會很容易發現疑惑的地方。

source: zhenbianshu.github.io
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Rt1v-s8Rl---S8L0k4E34Q