Java泛型

Java泛型(Generic)的引入加強了參數類型的安全性,減少了類型的轉換,它與C++中的模板templates比較類似。但是有一點,Java的泛型在編譯期有效,在運行期被刪除,也就是說所有的泛型參數類型在編譯後都會被清除掉。
類型參數使程序具有更好的可讀性和安全性。

  • 類型擦除
  • 泛型類
  • 泛型方法
  • 類型通配符(類型限定)
  • 運行時獲取泛型信息

一、類型擦除

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();
        System.out.println(list1.getClass() == list2.getClass()); //內存中只有一個Class對象

        //類型擦除:編譯時有效,編譯生成的class文件中:類型被擦除
        //因爲JVM運行時已經不包含類型信息,所以下面通過反射在運行時插入不同類型的數據
        List<Integer> list3 = new ArrayList<>();
        list3.add(1);
//        list3.add("asd"); //類型參數在編譯時有效
        try {
            list3.getClass().getMethod("add", Object.class).invoke(list3, "asd"); //獲取公共方法add
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < list3.size(); i++) {
            System.out.println(list3.get(i));
        }
//        list3.forEach(item -> {
//            System.out.println(item);
//        });

    }
}

輸出結果:

true
1
asd

上面的例子,說明泛型類型String和Integer都被擦除掉了,只剩下了原始類型,即Object。
當我們利用反射調用add方法的時候,卻可以存儲字符串。這說明了Integer泛型實例在編譯之後被擦除了,只保留了原始類型Object。
Java 之所以要避免在創建泛型實例時而創建新的類,從而避免運行時的過度消耗。


二、類型擦除後保留的原始類型 & 類型通配符

編譯後,類型變量被擦除,並使用其限定類型(無限定的變量用Object)替換。

下面是一個最簡單的泛型類

class Box<T> {

    private T data;

    public Box() {

    }

    public Box(T data) {
        setData(data);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

}
import java.util.ArrayList;
import java.util.List;

/**
 * 泛型之間沒有父子關係
 * 在邏輯上Box<Number>不能視爲Box<Integer>的父類
 * 但是ArrayList<E>可以繼承AbstractList<E>
 *
 * 類型通配符一般是使用 ? 代替具體的類型實參。
 * 注意了,此處是類型實參,而不是類型形參!且Box<?>在邏輯上是Box<Integer>、Box<Number>...等所有Box<具體類型實參>的父類。
 *
 * 泛型方法
 * 無論何時,只要你能做到,你就應該儘量使用泛型方法。
 * 也就是說,如果使用泛型方法可以取代將整個類泛型化,那麼就應該只使用泛型方法,因爲它可以使事情更清楚明白。
 * 另外,對於一個static的方法而言,無法訪問泛型類的類型參數。所以,如果static方法需要使用泛型能力,就必須使其成爲泛型方法。
 *
 * 不能創建泛型數組。一般的解決方案是任何想要創建泛型數組的地方都使用ArrayList.
 */
public class Test2 {

    public static void main(String[] args) {
        ArrayList list = new ArrayList(); //在泛型類中,不指定泛型的時候,這個時候的泛型類型爲Object
        list.add("adc");
        list.add(1.0);

        Box<Integer> a = new Box<>(712);
//        Box<Number> b = a;
        Box<Float> f = new Box<>(3.14f);
//        getData(f);
        Box<Number> n = new Box<>(12);
        Box<String> s = new Box<>("asd");

        getData2(a);
        getData2(f);
        getData2(s);

        getUpperNumberData(a);
        getUpperNumberData(f);
        getUpperNumberData(n);
//        getUpperNumberData(s);

//        getLowerNumberData(a);
//        getLowerNumberData(f);
//        getLowerNumberData(s);
        getLowerNumberData(n);

    }

    public static void getData(Box<Number> data) {
        System.out.println("data :" + data.getData());
    }

    public static void getData2(Box<?> data) {
        System.out.println("data :" + data.getData());
    }

    //類型通配符上限
    //需要定義一個功能類似於getData2()的方法,但對類型實參又有進一步的限制:只能是Number類及其子類
    public static void getUpperNumberData(Box<? extends Number> data){
        System.out.println("data :" + data.getData());
    }

    //類型通配符下限
    // Box<? super Number>形式,其含義與類型通配符上限正好相反: 只能是Number類及其父類
    public static void getLowerNumberData(Box<? super Number> data){
        System.out.println("data :" + data.getData());
    }

    //泛型方法
    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }
    //static的方法無法訪問泛型類的類型參數,所以必須將其定義爲泛型方法
    public static <T> List<T> makeList(T... args){
        List<T> result = new ArrayList<T>();
        for(T item:args)
            result.add(item);
        return result;
    }

}

有時,類或方法需要對類型變量加以約束。
如下,獲取數組中最小的元素:

class ArrayAlg {
    public static <T> T min(T[] arr) {
        if (arr == null || arr.length <= 0)
            return null;
        T smallest = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (smallest.compareTo(arr[i]) > 0) //編譯錯誤
                smallest = arr[i];
        }
        return smallest;
    }
}

不能保證 T 中有compareTo方法,所以必須對類型參數加以限定:

class ArrayAlg {
    public static <T extends Comparable<T>> T min(T[] arr) {
        if (arr == null || arr.length <= 0)
            return null;
        T smallest = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (smallest.compareTo(arr[i]) > 0)
                smallest = arr[i];
        }
        return smallest;
    }
}

如果存在類,類必須位於extends或者super限定符後的第一個位置,如果是接口(Comparable),就無所謂。


三、泛型方法

在調用泛型方法的時候,可以指定泛型,也可以不指定泛型。
在不指定泛型的情況下,泛型變量的類型爲 該方法中的幾種類型的同一個父類的最小級,直到Object。
在指定泛型的時候,該方法中的幾種類型必須是該泛型實例類型或者其子類。

public static void main(String[] args) {  
        /**不指定泛型的時候*/  
        int i=Test2.add(1, 2); //這兩個參數都是Integer,所以T爲Integer類型  
        Number f=Test2.add(1, 1.2);//這兩個參數一個是Integer,以風格是Double,所以取同一父類的最小級,爲Number  
        Object o=Test2.add(1, "asd");//這兩個參數一個是Integer,以風格是String,所以取同一父類的最小級,爲java.io.Serializable

        /**指定泛型的時候*/  
        int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能爲Integer類型或者其子類  
        int b=Test2.<Integer>add(1, 2.2);//編譯錯誤,指定了Integer,不能爲Double  
        Number c=Test2.<Number>add(1, 2.2); //指定爲Number,所以可以爲Integer和Double  
}  

//這是一個簡單的泛型方法  
public static <T> T add(T x,T y){  
  return y;  
}

其實在泛型類中,不指定泛型的時候,也差不多,只不過這個時候的泛型類型爲Object,就比如ArrayList中,如果不指定泛型,那麼這個ArrayList中可以放任意類型的對象。


四、運行時泛型信息獲取

嘗試通過下面的方法獲取泛型信息:

List<Integer> list = new ArrayList<Integer>();  
Map<Integer, String> map = new HashMap<Integer, String>();  
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));  
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));  

/* Output 
[E] 
[K, V] 
*/

我們可能期望能夠獲得真實的泛型參數,但是僅僅獲得了聲明時泛型參數佔位符。
getTypeParameters方法的Javadoc也是這麼解釋的:僅返回聲明時的泛型參數。所以,通過 getTypeParamters方法無法獲得運行時的泛型信息。

可以通過如下方法獲取泛型信息:

Map<String, Integer> map = new HashMap<String, Integer>() {};  

Type type = map.getClass().getGenericSuperclass();  
ParameterizedType parameterizedType = ParameterizedType.class.cast(type);  
for (Type typeArgument : parameterizedType.getActualTypeArguments()) {  
    System.out.println(typeArgument.getTypeName());  
}  

/* Output 
java.lang.String 
java.lang.Integer 
*/

其中最關鍵的差別是本節的變量聲明多了一對大括號。其實是創建了一個匿名內部類。這個類是 HashMap 的子類,泛型參數限定爲了 String 和 Integer。
Java 引入泛型擦除的原因是避免因爲引入泛型而導致運行時創建不必要的類。那我們其實就可以通過定義類的方式,在類信息中保留泛型信息,從而在運行時獲得這些泛型信息。

簡而言之,Java 的泛型擦除是有範圍的,即類定義中的泛型是不會被擦除的。

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

/**
 * 運行時泛型信息獲取
 */
public class Test3 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();
        Map<Integer, String> map = new HashMap<Integer, String>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));

        System.out.println("--------------");

        Map<String, Integer> map2 = new HashMap<String, Integer>() {}; //匿名內部類
        Type type = map2.getClass().getGenericSuperclass();
        ParameterizedType parameterizedType = ParameterizedType.class.cast(type);
        for (Type typeArgument : parameterizedType.getActualTypeArguments()) {
            System.out.println(typeArgument.getTypeName());
        }

        System.out.println("--------------");

        Student st = new Student();
        Class clazz = st.getClass();
        //getSuperclass()獲得該類的父類
        System.out.println(clazz.getSuperclass());
        //getGenericSuperclass()獲得帶有泛型的父類
        //Type是 Java 編程語言中所有類型的公共高級接口。它們包括原始類型、參數化類型、數組類型、類型變量和基本類型。
        Type type2 = clazz.getGenericSuperclass();
        System.out.println(type2);
        //ParameterizedType參數化類型,即泛型
        ParameterizedType p = (ParameterizedType)type2;
        //getActualTypeArguments獲取參數化類型的數組,泛型可能有多個
        Class c = (Class) p.getActualTypeArguments()[0];
        System.out.println(c);
    }
}

class Person<T> {
}

//類定義中的泛型是不會被擦除的
class Student extends Person<Student> {
}

運行結果:

[E]
[K, V]
--------------
java.lang.String
java.lang.Integer
--------------
class Generic.Person
Generic.Person<Generic.Student>
class Generic.Student

很多情況下我們又需要在運行時獲得泛型信息,那我們可以通過定義類的方式(通常爲匿名內部類,因爲我們創建這個類只是爲了獲得泛型信息)在運行時獲得泛型參數,從而滿足例如序列化、反序列化等工作的需要。


五、約束與侷限性

1、不能用基本類型實例化類型參數

2、運行時類型查詢只適用於原始類型

JVM不認識泛型:

Pair<String> a = new Pair<>();
//        System.out.println(a instanceof Pair<String>);
        System.out.println(a instanceof Pair);

3、不能創建參數化類型的數組

Pair<String>[] table = new Pair<String>[10]; //ERROR

4、不能實例化類型變量

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