Java泛型

泛型(Generic Type)是Java中重要的一部分。在J2SE 5.0之後新增。在使用Java標準庫中的內容的時候,經常會遇到泛型。這裏將泛型總結一下

什麼是泛型

  • 討論一個內容的時候,首先會來說什麼是什麼。在官方的文檔中說到

A generic type is a generic class or interface that is parameterized over types.

  • 泛型又可以稱作參數化類型,這是在Java SE新增添的特性。一對尖括號,中間包含類型信息。將類型獨立成參數,在使用的時候才指定實際的類型

如果沒有泛型會怎麼樣?我們考慮以下幾種情況:

  1. 你實現了一個存儲整數類型(Integer)的列表,這時候你又需要存儲字符串(String)的列表,兩種列表邏輯行爲完全一樣,只是存儲的類型不同

  2. 爲了保證列表的通用性,你將列表的類型改爲了Object,這樣就不用爲類型修改代碼了。但是每次從列表中取對象的時候都需要強制轉換,而且很很容易出錯

    • 有了泛型之後,可以將邏輯相同類型不同的代碼獨立出來,由編譯器負責進行類型轉換

泛型的聲明

泛型方法(Generic Method)

  • 泛型方法是在普通方法聲明上加入了泛型
public static < E > void printArray( E[] inputArray ){
    for ( E element : inputArray ){        
        System.out.printf( "%s ", element );
    }
    System.out.println();
}
  • 調用:
// Create arrays of Integer, Double and Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
System.out.println( "Array integerArray contains:" );
printArray( intArray  ); // pass an Integer array
System.out.println( "\nArray doubleArray contains:" );
printArray( doubleArray ); // pass a Double array
System.out.println( "\nArray characterArray contains:" );
printArray( charArray ); // pass a Character array
  • 輸出:

Array integerArray contains:
1 2 3 4 5 6
Array doubleArray contains:
1.1 2.2 3.3 4.4
Array characterArray contains:
H E L L O

Java泛型方法的聲明格式如下:

[權限] [修飾符] [泛型] [返回值] [方法名]  (    [參數列表]   ) {}
public static  < E >   void   printArray( E[] inputArray ) {}
  • 泛型的聲明,必須在方法的修飾符(public,static,final,abstract等)之後,返回值聲明之前。可以聲明多個泛型,用逗號隔開。泛型的聲明要用<>包裹

泛型方法的使用有兩種:

類型推導
  • 以聲明鍵值對的例子來說,通常的寫法會有一長串,不免有些痛苦
Map<String, List<String>> m = new HashMap<String, List<String>>();
  • 我們可以構造一個泛型方法作爲靜態工廠,來完成這一操作
public static <K, V> HashMap<K, V> newInstance() {
    return new HashMap<K, V>();
}
Map<String, List<String>> m = newInstance();
  • 編譯器在編譯代碼的時候推導出了K, V分別對應的類型。當然,編譯器的推導能力也是有限的,這裏也就不過多討論了
指定類型
  • 不贅述

泛型類(Generic Class)

  • 泛型類和普通類的聲明一樣,只是在類名後面加上了類型表示。就像泛型方法,泛型類可以有一個或多個類型表示,用逗號進行分隔
public class Box<T> {
  private T t;
  public void add(T t) {
    this.t = t;
  }
  public T get() {
    return t;
  }
  public static void main(String[] args) {
     Box<Integer> integerBox = new Box<Integer>();
     Box<String> stringBox = new Box<String>();

     integerBox.add(new Integer(10));
     stringBox.add(new String("Hello World"));
     System.out.printf("Integer Value :%d\n\n", integerBox.get());
     System.out.printf("String Value :%s\n", stringBox.get());
  }
}

Integer Value :10
String Value :Hello World

  • 在泛型類上聲明的類型,可以被用到類中任何表示類型的地方

  • 泛型類只能通過以指定類型的方式進行使用。在之後的Java版本中,加入了類型推導功能,可以將後面的泛型類型省略,但是還是需要保留尖括號

List<String> list = new ArrayList<String>(); // 普通的寫法
List<String> list = new ArrayList<>(); // 省略的寫法

泛型接口(Generic Interface)

  • 泛型接口是在聲明接口的時候指定,類在繼承接口的時候需要補充泛型類型
public interface Info<T> {
    public T getInfo();
}
  • 然後定義一個類實現這個接口
public InfoImp implements Info<String> {
    public String getInfo() {
        return "Hello World!";
    }
}
  • 可以發現實現接口裏的方法需要使用具體的類型

泛型接口的一般格式:

[訪問權限] interface [接口名] <泛型標識> {}
  • 當然,我們可以實現泛型接口的時候不指名泛型類型,這樣這個類就需要定義爲泛型類
public InfoImp<T> implements Info<T> {
    public T getInfo() {
        return null;
    }
}

泛型標識與泛型通配符

  • 理論上泛型標識可以使用任意的字母或字母序列,可以考慮以下的例子,但是不推薦這樣使用
public static <STRING> STRING sayHello(STRING word){
    System.out.println("Hello " + word);
    return a;
}
  • 但是Java內部有一套自己的規範,這樣在閱讀JDK代碼的時候會更加明確泛型標識的含義
E - Element (在集合中使用,因爲集合中存放的是元素)
T - Type(Java 類)
K - Key(鍵)
V - Value(值)
N - Number(數值類型)
? - 表示不確定的java類型
S、U、V - 2nd、3rd、4th types
  • 說到泛型標識符,再說一說泛型通配符。常用的泛型通配符有三種

任意類型 “<”?>

  • “<”?> 可以理解爲泛型中的 Object,爲什麼這麼說呢?因爲任意類型的通配符可以接受任意類型的泛型。下面的例子表示出了這種關係
Box<?> box = new Box<String>();
  • 類似於將後面的類型轉換到前面的類型。但是
class Box<?> {} //錯誤的泛型類定義
public static <?> void sayHello(? helloString) {} //錯誤的泛型方法定義
interface Box<?> {} //錯誤的泛型接口定義

上限類型 “<”? extends 類>

  • “<”? extends 類> 表示泛型只能使用這個類或這個類的子類。舉個例子
public static <T extends String> void sayHello(T helloString) {}
  • 在該方法中調用sayHello(“xiaoming”);是正確的,但是調用sayHello(2333);就是錯誤的

  • 考慮一種更通用的情況

public static void printList(List<? extends String> list){
    for(String str:list){
        System.out.println(str);
    }
}
  • 這個例子指定了泛型類的具體類型的範圍。在JDK中經常可以看到這樣的使用方法

下限類型 “<”? super 類>

  • 同上限方法,”<”? super 類> 表示泛型只能使用這個類或這個類的父類

類型擦除

  • 泛型只在編譯時有效,編譯成字節碼的過程中會進行類型擦除的操作。當然並不是所有的泛型類型都被擦除,有些也會保留下來

  • 一個簡單的類型擦除的例子:

List<String> list = new ArrayList<>();
Iterator<String> it = list.iterator();
while (it.hasNext()) {
   String s = it.next();
}
  • 我們對其編譯,然後再反編譯,反編譯引擎用的CFR:
ArrayList arrayList = new ArrayList();
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
    String string = (String)iterator.next();
}
  • 從上面的結果可以看出,泛型類上面的類型被去掉了,但是增加了一個類型強制轉換。解釋器默認認爲裏面的類型都會是String。這是因爲在編譯的時候會進行類型檢查,如果發現使用的類型與泛型聲明類型不符,編譯是不會通過的

  • 那能不能繞過這個檢查呢?這個時候就需要使用反射來進行操作了。就上面的例子來說,ArrayList可以放入任意類型,所以使用反射只要保證類型強制轉換不出問題,程序還是可以使用的

在Java文檔中提到,類型擦除主要進行以下工作:

  1. 將泛型中的所有類型參數更換成類型界限,或者無界的類型替換成Object。所以生成的字節碼只包含普通類、接口和方法

  2. 爲了確保類型安全,必要時插入強制類型轉換

  3. 生成橋接方法保持擴展泛型類型中的多態性

可變參數

  • 使用泛型方法可以使用可變參數:
public class Main {
    public static <T> void out(T... args) {
        for (T t : args) {
            System.out.println(t);
        }
    }
    public static void main(String[] args) {
        out("findingsea", 123, 11.11, true);
    }
}

new T()?

  • 能不能在泛型方法(類)中創建泛型類型的實例呢?答案是不可以的
public static <E> void append(List<E> list) {
    E elem = new E();  // 編譯錯誤
    list.add(elem);
}
  • 不過可以使用反射來創建實例
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}
List<String> ls = new ArrayList<>();
append(ls, String.class);
  • 因爲類型擦除的緣故,部分類型信息會丟失,我們在運行時不會獲取到相應的類型,所以也就無法將該類型實例化成對象

泛型和數組

在Java中,直接創建泛型數組是非法的(要注意的一點是,Java中沒有所謂的泛型數組一說)。泛型設計的初衷是爲了簡化程序員類型轉換的操作,保證類型安全。數組是協變的,如果Sub爲Super的子類型,那麼數組Sub[]就是Super[]的子類型。這樣做就很難保證存儲上的安全

  • 但在實際使用過程中,往往需要創建泛型數組:
public static <E> E[] newArray(int n) {
    return new E[n];
}
  • 這個時候運行程序,會拋出異常

Exception in thread “main” java.lang.Error: Unresolved compilation problem:
Cannot create a generic array of E

  • 在這種情況下,可以使用 List 來代替數組
public static <E> List<E> newList() {
    return new ArrayList<E>();
  • 但是,Java不是生來就有List的,如果遇到必須使用數組的情況該怎麼辦?

  • 在這裏可以參考Java裏聚合類型的實現,以一個簡單的例子說明

public class Stack<E> {
    private E[] elements;
    private int size = 0;

    public Stack() {
        elements = new E[10];
    }

    public void push(E e) {
        elements[size++] = e;
    }

    public E pop() {
        E result = elements[--size];
        elements[size] = null;
        return result;
    }
}
  • 上面的例子和java.util.Stack還是有區別的,只是爲了說明如何處理泛型數組問題

  • 同樣運行的時候會出錯

Exception in thread “main” java.lang.Error: Unresolved compilation problem:
Cannot create a generic array of E

  • 一種方法是在創建泛型數組的時候創建一個Object數組,然後轉換成E數組
elements = (E[]) new Object[10];
  • 第二種方法將數組類型改爲Object,在彈出元素的時候進行轉換
public class Stack<E> {
    private Object[] elements;
    ...
    public E pop() {
        E result = (E) elements[--size];
        ...
    }
}
  • 泛型數組的返回和傳遞
package generics;

public class GenericsArray {
  /**
   * 返回一個泛型數組
   * @param ts
   * @return
   */
  @SafeVarargs
  public static <T> T[] getArray(T...ts){
     return ts;
  }
  /**
   * 爲方法傳遞一個泛型數組
   * @param param
   */
  public static <T> void print(T[] param){

     System.out.println("the Param is:");

     for(T c:param){

       System.out.print(c);

    }
  }
  public static void main(String[] args){

     Integer[] arr = getArray(1,2,3,4,5);

     System.out.println(arr);

     print(arr);
  }
}

參考資料

發佈了67 篇原創文章 · 獲贊 26 · 訪問量 73萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章