【Java】泛型學習筆記


我的博客即將入駐“雲棲社區”,誠邀技術同仁一同入駐。

參考書籍

《Java核心技術:卷1》

泛型, 先睹為快

先通過一個簡單的例子說明下Java中泛型的用法:

泛型的基本形式類似於模板, 通過一個類型參數T, 你可以"私人定制"一個類,具體定制的范圍包括實例變量的類型返回值的類型傳入參數的類型

Foo.java

public class Foo <T> {
  // 約定實例變量的類型
  private T data;
  // 約定返回值的類型
  public T getData () {
    return this.data;
  }
  // 約定傳入參數的類型
  public void setData (T data) {
    this.data = data;
  }
}

 

Test.java

public class Test {
  public static void main (String args[]) {
    Foo<String> s = new Foo<String> ();
  }
}

 

泛型的由來

泛型設計源於我們的編寫類時的一個剛需:想讓我們編寫的處理類能夠更加"通用", 而不是只能處理某些特定的對象或場景。或者說:我們希望我們的類能實現盡可能多的復用。舉個栗子:一般來說,你並不想要編寫多個分別處理不同數據類型,但內在邏輯代碼卻完全一樣的類。因為這些處理類可能除了數據類型變換了一下外,所有代碼都完全一致。“只要寫一個模板類就OK了嘛~ 等要使用的時候再傳入具體的類型,多省心”, 當你這么思考的時候:浮現在你腦海里的,就是泛型程序設計(Generic pogramming)的思想

在介紹Java的泛型機制之前, 先讓我們來看看, 還沒加入泛型機制的“泛型程序設計”是怎樣子的

泛型程序設計1.0: 不用Java泛型機制

下面我們編寫一個存儲不同的對象的列表類,列表有設置(set)和取值(get)兩種操作。
假設這個列表類為ObjArray,同時嘗試存儲的值為String類型,則:
1.在ObjArray類里我們維護一個數組arr, 為了將來能容納不同的對象, 將對象設為Object類型(所有對象的父類)
2.在實例化ObjArray后, 通過調用set方法將String存入Object類型的數組中; 而在調用get方法時, 要對取得的值做強制類型轉換—從Object類型轉為String類型


ObjArray.java:

public class ObjArray  {
  private Object [] arr;
  public ObjArray(int n) {
    this.arr = new Object[n];
  }
  public void set (int i, Object o) {
    this.arr[i] = o;
  }
  public Object get (int i) {
    return this.arr[i];
  }
}

 


Test.java:

/**
 * @description: 測試代碼
 */
public class Test {
  public static void main (String args[]) {
    ObjArray arr = new ObjArray(3);
    arr.set(0, "彭湖灣");
    // get操作時要做強制類型轉換
    String n =(String)arr.get(0);
    // 輸出 "彭湖灣"
    System.out.print(n);
  }
}

 

 

 

如果不使用泛型機制,但又想要實現泛型程序設計,就會編寫出類似這樣的代碼。

泛型程序設計2.0: 使用Java泛型機制

讓我們來看看使用泛型機制改進后的結果。
看起來大約是這樣:


GenericArray.java

public class GenericArray<T>  {
  public void set (int i, T o) {
    // ...
  }
  public T get (int i) {
    // ...
  }
}

 【具體代碼下面給出】

 

Test.java:

public class Test {
  public static void main (String args[]) {
    GenericArray<String> arr = new <String>GenericArray(3);
    arr.set(0, "彭湖灣");
    // 不用做強制類型轉換啦~~
    String s =arr.get(0);
    // 輸出: 彭湖灣
    System.out.print(s);
  }
}

 

我們發現,改進后的設計有以下幾點好處:

1. 規范、簡化了編碼: 我們不用在每次get操作時候都要做強制類型轉換了
2. 良好的可讀性:GenericArray<String> arr這一聲明能清晰地看出GenericArray中存儲的數據類型
3. 安全性:使用了泛型機制后,編譯器能在set操作中檢測傳入的參數是否為T類型, 同時檢測get操作中返回值是否為T類型,如果不通過則編譯報錯

泛型並非無所不能

了解到了泛型的這些特性后, 也許你會迫不及待地想要在ObjArray類里大干一場。
例如像下面這樣, 用類型參數T去直接實例化一個對象, 或者是實例化一個泛型數組

可惜的是 ......

public class GenericArray<T>  {
  private T obj = new T (); // 編譯報錯
  private T [] arr = new T[3]; // 編譯報錯
  // ...
}

 

沒錯, 泛型並不是無所不能的, 相反, 它的作用機制受到種種條框的限制。

這里先列舉泛型機制的兩個限制:

1.不能實例化類型變量, 如T obj = new T ();

2. 不能實例化泛型數組,如T [] arr = new T[3];

【注意】這里不合法僅僅指實例化操作(new), 聲明是允許的, 例如T [] arr

 

我們現在來繼續看看上面泛型設計中, GenericArray類的那部分代碼:

public class GenericArray<T>  {
  private Object [] arr;
  public GenericArray(int n) {
    this.arr = new Object[n];
  }
  public void set (int i, T o) {
    this.arr[i] = o;
  }
  public T get (int i) {
    return (T)this.arr[i];
  }
}

 

 

沒錯, 在ObjArray類內部我們仍然還是用到了強制轉型。看到這里也許令人有那么一點點的小失望, 畢竟還是沒有完全跳出
初始的泛型設計的邊界。 但是, 泛型的優點仍然是顯而易見的, 只不過要知道的是:它並沒有無所不能的魔法, 並受到諸多限制。

 

泛型的編寫規則

1.泛型類和泛型方法的定義

泛型類
如前面所說,可以像下面一樣定義一個泛型類
類型變量T放在類名的后面

public class Foo <T> {
  // 約定實例變量的類型
  private T data;
  // 約定返回值的類型
  public T getData () {
    return this.data;
  }
  // 約定傳入參數的類型
  public void setData (T data) {
    this.data = data;
  }
}

 

泛型方法
也可以定義一個泛型方法:

泛型變量T放在修飾符(這里是public static)的后面, 返回類型的前面

public class Foo {
  public static <T> T getSelf (T a) {
    return a;
  }
}

 

泛型方法可以定義在泛型類當中,也可以定義在一個普通類當中

2.可以使用多個類型變量

public class Foo<T, U> {
  private T a;
  private U b;
}

 

【注意】在Java庫中,常使用E表示集合的元素類型, K和V分別表示關鍵字和值的類型, T(U,S)表示任意類型

3.JavaSE7以后,在實例化一個泛型類對象時,構造函數中可以省略泛型類型

ObjArray<Node> arr = new <Node>ObjArray();


可簡寫成:

ObjArray<Node> arr = new <>ObjArray();

 

類型變量的限定

當我們實例化泛型類的時候, 我們一般會傳入一個可靠的類型值給類型變量T。 但有的時候,被定義的泛型類作為接收方,也需要對傳入的類型變量T的值做一些限定和約束,例如要求它必須是某個超類的子類,或者必須實現了某個接口, 這個時候我們就要使用extends關鍵字了。如:

超類SuperClass:

public class SuperClass {
}


子類SubClass:

public class SubClass extends SuperClass {
}

 

對T使用超類類型限定:要求父類必須為SuperClass

public class Foo<T extends SuperClass> {
}


測試:

public class Test {
  public static void main (String args[]) {
    Foo<SubClass> f = new Foo<SubClass>(); // 通過
    Foo<String> n = new Foo<String>(); // 報錯
  }
}

 

extends使用的具體規則

1. 對於要求實現接口, 或者繼承自某個父類, 統一使用extends關鍵字 (沒有使用implements關鍵字,為了追求簡單)

2. 限定類型之間用 "&" 分隔

3. 如果限定類型既有超類也有接口,則:超類限定名必須放在前面,且至多只能有一個(接口可以有多個)


這個書寫規范和類的繼承和接口的實現所遵循的規則是一致的(<1>不允許類多繼承,但允許接口多繼承<2>書寫類的時候類的繼承是寫在接口實現前面的)

// 傳入的T必須是SuperClass的子類,且實現了Comparable接口
public class Foo<T extends SuperClass&Comparable> {
}

【注意】: 上面的SuperClass和Comparable不能顛倒順序

 

泛型類的繼承關系

泛型類型的引入引發了一些關於泛型對象繼承關系的有趣(?)問題。

在Java中, 如果兩個類是父類和子類的關系,那么子類的實例也都是父類的實例,這意味着:
一個子類的實例可以賦給一個超類的變量:

SubClass sub = new SubClass();
SuperClass sup = sub;

 

當引入了泛型以后, 有趣(?)的問題來了:

 

我們通過兩對父子類List/ArrayList, Employee/Manager來說明這個問題

(我們已經知道List是ArrayList的父類(抽象類),這里假設Employee是Manager的父類)
1. ArrayList<Employee> 和 ArrayList<Manager>之間有繼承關系嗎?(ArrayList<Manager>的實例能否賦給ArrayList<Employee>變量?)
2. List<Employee> 和 ArrayList<Employee>之間有繼承關系嗎?(ArrayList<Employee>的實例能否賦給 List<Employee>變量?)
3. ArrayList和ArrayList<Employee>之間有繼承關系嗎?(ArrayList<Employee>的實例能否賦給ArrayList變量?)

答案如下:

對1: 沒有繼承關系; 不能

ArrayList<Manager> ae = new ArrayList<Manager>();
ArrayList<Employee> am = ae; // 報錯


對2: 有繼承關系; 可以

ArrayList<Employee> al = new ArrayList<>();
List<Employee> l = al; // 通過


對3: 有繼承關系; 可以

ArrayList<Employee> ae = new ArrayList<>();
ArrayList a = ae;

 

下面用三幅圖描述上述關系:

 


描述下1,2的關系

對上面三點做個總結:
1. 類名相同,但類型變量T不同的兩個泛型類沒有什么聯系,當然也沒有繼承關系(ArrayList<Manager>和ArrayList<Employee>)
2. 類型變量T相同,同時本來就是父子關系的兩個類, 作為泛型類依然保持繼承關系 (ArrayList<Employee>和List<Employee>)
3. 某個類的原始類型,和其對應的泛型類可以看作有“繼承關系”(ArrayList和ArrayList<Employee>)

引用一幅不太清晰的圖

 

通配符?

泛型繼承關系帶來的問題 — 類型匹配的苦惱

問題出現在上面所述規范中的第二點:ArrayList<Manager> 和 ArrayList<Employee>之間沒有繼承關系。
這意味着,如果你像下面一樣編寫一個處理ArrayList<Employee>的方法

public class Foo {
  public static void handleArr (ArrayList<Employee> ae) {
    // ...
  }
}


你將無法用它來處理ArrayList<Manager>:

public static void main (String args[]) {
    ArrayList<Manager> am = new ArrayList<Manager>();
    Foo.handleArr(am); // 報錯,類型不匹配
}

 

 

 現在我們想要:“handleArr方法不僅僅能處理ArrayList<Employee>, 而且還能處理ArrayList<X> (這里X代表Employee和它子類的集合)”。於是這時候通配符?就出現了:ArrayList<? extends Employee>能夠匹配ArrayList<Manager>, 因為ArrayList<? extends Employee>是 ArrayList的父類

現在我們的例子變成了:

public class Foo {
  public static void handleArr (ArrayList<? extends Employee> ae) {
    // ...
  }
}
public static void main (String args[]) {
    ArrayList<Manager> am = new ArrayList<Manager>();
    Foo.handleArr(am); // 可以運行啦!
  }

 

通配符和super關鍵字

?統配不僅可以用於匹配子類型, 還能用於匹配父類型:

<? super Manager>

 

泛型的其他約束

上面我們介紹了泛型的一些約束,例如不能直接實例化實例化類型變量和泛型數組,這里和其他約束一起做個總結:

在定義泛型類時不能做的事:


1. 不能實例化類型變量

T obj = new T (); // 報錯, 提示: Type parameter 'T' cannot be instantiated directly

 

解決方案:
如果實在要創建一個泛型對象的話, 可以使用反射:

public class GenericObj<T> {
  private T obj;
  public GenericObj(Class<T> c){
      try {
        obj = c.newInstance(); // 利用反射創建實例
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
}

 

/**
 * @description: 測試代碼
 */
public class Test {
  public static void main (String args[]) {
    // 通過
    GenericObj<String> go = new GenericObj<> (String.class);
  }
}


因為Class類本身就是泛型, 而String.class是Class<T>的實例,


2. 不能實例化泛型數組,如T [] arr = new T[3];

private T [] arr = new T[3]; // 報錯, 提示: Type parameter 'T' cannot be instantiated directly

 

解決方法一:

上文所提到的,創建Object類型的數組,然后獲取時轉型為T類型:

public class GenericArray<T>  {
  private Object [] arr;
  public GenericArray(int n) {
    this.arr = new Object[n];
  }
  public void set (int i, T o) {
    this.arr[i] = o;
  }
  public T get (int i) {
    return (T)this.arr[i];
  }
}

 

解決方法二: 利用反射
這里使用反射機制中的Array.newInstance方法創建泛型數組

GenericArray.java

public class GenericArray<T> {
  private T [] arr;
  public GenericArray(Class<T> type, int n) {
    arr = (T[])Array.newInstance(type, n); // 利用反射創建泛型類型的數組
  }
  public void set (int i, T o) {
    this.arr[i] = o;
  }
  public T get (int i) {
    return (T)this.arr[i];
  }
}

 

Test.java

/**
 * @description: 測試代碼
 */
public class Test {
  public static void main (String args[]) {
  GenericArray<String> genericArr = new GenericArray<>(String.class, 5);
  genericArr.set(0, "penghuwan");
  System.out.println(genericArr.get(0)); // 輸出 "penghuwan"
  }
}

 

3. 不能在泛型類的靜態上下文中使用類型變量

public class Foo<T> {
  private static T t;
  public static T get () { // 報錯, 提示: 'Foo.this' can not be referenced from a static context
    return T;
  }
}

 


注意這里說的是泛型類的情況下。如果是在一個靜態泛型方法中是可以使用類型變量的

public class Foo {
  public static<T> T get (T t) { // 通過
    return t;
  }
}


(這里的泛型方法處在一個非泛型類中)

4. 不能拋出或者捕獲泛型類的實例

不能拋出或者捕獲泛型類的實例:

// 報錯 提示:Generic class may not extend java.lang.throwable
public class Problem<T> extends Exception {
}

 

甚至擴展Throwable也是不合法的

public class Foo {
  public static  <T extends Throwable> void doWork () {
    try {
      // 報錯 提示: Cannot catch type parameters
    }catch (T t) {
    }
  }
}

 

但在異常規范中使用泛型變量是允許的

// 能通過
public class Foo {
  public static  <T extends Throwable> void doWork  (T t) throws T {
    try {
      // ...
    }catch (Throwable realCause) {
      throw  t;
    }
  }
}

 

在使用泛型類時不能做的事

1. 不能使用基本類型的值作為類型變量的值

Foo<int> node = new Foo<int> (); // 非法

 

應該選用這些基本類型對應的包裝類型

Foo<Integer> node = new Foo<Integer> ();

 


2. 不能創建泛型類的數組

public static void main (String args[]) {
    Foo<Node> [] f =new Foo<Node> [6]; // 報錯
}

 

解決方法:
可以聲明通配類型的數組, 然后做強制轉換

Foo<Node> [] f =(Foo<Node> [])new Foo<?> [6]; // 通過

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM