細說Java 泛型

爲了讓集合容器記住元素類型是jdk1.5引入泛型(Generic Types)的一個主要緣由。java

泛型看起來就是將實際的類型參數化,這樣就能夠在使用的時候傳人實際的類型,或者推斷其表明的類型(如ArrayList)。但從本質上講jvm並不認識ArrayList這種類型,它只是java的語法糖,即只在源碼層面的表現,在編譯後jvm加載時就只是ArrayList而已。數組

1.爲何引入泛型

先看一個例子:安全

List list = new ArrayList();
list.add(100);
list.add("100");

// 第一個元素就是int類型,OK
System.out.println((int)list.get(0) + 1);
// 第二個元素實際爲String,所以會引起ClassCastException
System.out.println((int)list.get(1) + 1);
複製代碼

在引入泛型以前,list的元素類型固定爲Object,因此能夠添加任意類型的元素進去,編譯不會有問題,但取出來時須要從Object轉成實際的類型纔有意義,這樣就容易引起運行時類型轉換異常,尤爲在循環或做爲方法參數屢次傳遞後更難以分清起真實類型。微信

從實際使用角度來看,咱們更但願一個容器存儲相同類型或同一類(包括子類)的元素。經過泛型的編譯時檢查則能夠幫助咱們避免不當心把其餘類型的元素加進來。java的泛型是一種語法糖,其採用的方式是類型擦除,因此java泛型是一種僞泛型,這麼作也是爲了兼容舊版本。jvm

2.泛型類,泛型接口

咱們能夠在接口,類上聲明類型行參而將其泛型話:ide

public interface Collection<E> extends Iterable<E> {
  boolean add(E e);
  ...
}

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    public V put(K key, V value) {
    	//...
    }
}    
複製代碼

帶泛型的類在派生子類的時候須要傳入實際的類型,或者不帶泛型:工具

class Base<T> {}

// 錯誤
class Sub extends Base<T> {}

// Ok
class Sub extends Base<String> {}
// Ok
class Sub extends Base {}
// Ok
class Sub<T> extends Base<T> {}
複製代碼

經過extends爲泛型指定邊界:編碼

class Base<T extends Comparable & Serializable & Cloneable> {}
class Base<T extends ArrayList & Comparable & Serializable & Cloneable> {}
複製代碼

T被限定爲實現指定的類或接口。能夠指定多個接口,但只能指定一個類且類必須爲第一個。在編譯時T的類型會被替換爲extends後的第一個類或接口類型。spa

  • 基類劫持接口設計

    abstract class Animal implements Comparable<Animal> {}
    
    class Dog extends Animal implements Comparable<Dog> {
    		/** 不管CompareTo參數是Dog仍是Animal,都不行 */
        @Override
        public int compareTo(Dog o) {
            return 0;
        }
    }
    複製代碼

    Dog實現了Comparable,泛型參數是Dog,但不巧其基類Animal也實現了Comparable接口而且傳人了一個不一樣的泛型參數Animal,致使compareTo參數類型衝突,這種現象被稱爲基類劫持了接口。

3.泛型方法

使用泛型的另外一種場景是泛型方法,若是在接口或類上沒有定義泛型參數,但想在方法中使用泛型,則能夠像下面這樣定義一個泛型方法:

public static <T> Set<T> synchronizedSet(Set<T> s) {
    return new SynchronizedSet<>(s);
}

// 明確傳人泛型參數類型
Collections.<String>synchronizedSet(new HashSet<>());
// 隱式使用,由編譯器推導實際類型
Collections.synchronizedSet(new HashSet<String>());
複製代碼

4.類型通配符

假設有個統計列表中數字(<100)出現頻率的方法:

public static Map<Number, Long> count(List<Number> list) {
    return list.stream()
            .filter(n -> n.intValue() < 100)
            .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}
複製代碼

指望能夠像接受任何數字的列表:

List<Integer> numsA = Arrays.asList(1, 2, 3, 100, 200, 300);
// 錯誤
Map<Number, Long> countA = count(numsA);
       
List<Double> numsB = Arrays.asList(1D, 2D, 3.55D, 100D, 200D, 330D);
// 錯誤
Map<Number, Long> countB = count(numsB);
複製代碼

上面代碼會報錯,List<Integer>,List<Double>不是List<Number>的子類型。把方法參數改爲count(List<Object> list)也不行,它們也不是List<Object>的子類型,就算運行時傳進去的都是Object的List。由於若是這樣的話,傳人一個子類的List,可是試圖把它的元素轉成另外一個子類時就會有問題。

這種編譯時檢查雖然增長的程序的安全性,但下降了編碼的靈活性,若是有多種類型須要統計,咱們不得不爲每一種類型編寫一份count方法,還有就是count方法不能重載,在一個類中可能寫出countInt,countDouble...這樣的代碼。

4.1 通配符

爲了解決上述問題,咱們可使用通配符:

// list的元素能夠是任意類型
public static Map<Number, Long> count(List<?> list) {
    return list.stream()
        .map(n -> (Number)n)
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}
複製代碼

?就是通配符,表明任意類型。這樣就能夠接收任何類型的List了,大大提升了靈活性,代碼也很簡潔,但安全性缺又下降了,試想有人傳了一個List<String> s = Arrays.asList("1", "2", "3", "4", "5");進去會發生什麼?

4.2 通配符上界

繼續上面的問題,咱們真實的需求並非傳人任意類型,而是任意Number的子類。這時能夠對通配符作進一步的限制:

public static Map<Number, Long> count(List<? extends Number> list) {
    return list.stream()
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}
複製代碼

<? extends Number>指定了傳人的列表元素必須是Number及其子類,即?所表明類型的上界是Number,通配符上界一樣能夠用在類或接口泛型定義上。

在count方法中依然不能經過list.add(1);添加一個Number或其子類元素進去,WHY?

4.3 通配符下界
List<? super Number> list = new ArrayList<>();
list.add(Integer.valueOf(1));//ok
list.add(Long.valueOf(2L));//ok
// 由於只指定下屆,因此元素類型爲Object
Object object = list.get(0);
複製代碼

<? super Number>代表List的元素類型是Number及其基類,即?的下限是Number,通配符下界一樣能夠用在類或接口泛型定義上。 爲何通配符上界能夠添加Number的子類進去呢?

其實不難理解,由List<? super Number>可知,List中的元素必是Number或其基類,Integer,Long等是Number的子類,必然也是Number的父類的子類。A是B的子類,B是C的子類,A必然是C的子類。因此根據LSP這是可行的。

4.4 逆變與協變

逆變: 當某個類型A能夠由其子類B替換,則A是支持協變的。

協變: 當某個類型A能夠由其基類B替換,則A是支持逆變的。

由前面咱們知道既不能List<Number> list = new ArrayList<Integer>();,也不能List<Integer> list = new ArrayList<Number>();,由於Java泛型設計爲不可變的(數組除外)。

但咱們能夠經過通配符實現逆變與協變:

// 協變
List<? extends Number> list = new ArrayList<Integer>();
// 逆變
List<? super Integer> list = new ArrayList<Number>();
複製代碼

另外一個例子:

class Animal {}
class Pet extends Animal {}
class Cat extends Pet {}

static class Person<T extends Animal> {
    T pet;
}

// 協變
Person<? extends Pet> lily = new Person<Cat>();
// error
lily.pet = new Cat();
// 逆變
Person<? super Pet> alien = new Person<Animal>();
// ok
alien.pet = new Cat();
複製代碼
  • 泛型參數相同的時候,在泛型類上是支持協變的,如ArrayList<String> -> List<String> -> Collection<String>
  • 泛型參數使用通配符的時候,即在泛型類自身上支持協變,又可在泛型參數類型上支持協變,如Collection<? extends Number>,子類型能夠是List<? extends Number>,Set<? extends Number>,又能夠是Collection<Integer>Collection<Long>,經過傳遞能夠知道HashSet<Long>Collection<? extends Number>的子類型。
  • 包含多個泛型類型參數,對每一個類型參數分別適用上面的規則,HashMap<String, Long>Map<? extends CharSequence, ? extends Number>的子類型。
4.5 PECS

應該在何時用通配符上界,何時用通配符下界呢?《Effective Java》提出了PECS(producer-extends, consumer-super),即一個對象產生泛型數據時用extends,一個對象接收(消費)泛型數據時,用super。

/** * Collections #copy方法 * src產生了copy須要的泛型數據,用extens * dest消費了copy產生的泛型數據,用super */
public static <T> void copy(List<? super T> dest, List<? extends T> src) 複製代碼
4.6 通配符與泛型方法

用泛型方法實現以前的count方法:

/** 與以前通配符實現相同功能,同時在方法中能夠添加新元素 */
public static <T extends Number> Map<T, Long> count(List<T> list) {
    return list.stream()
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}
複製代碼

再來一個🌰,假設有個工具類方法,實現將一個非空的數字添加到傳人的列表中:

public static void safeAdd(List<? extends Number> list, Number num) {
    if (num == null) {
        return;
    }

  	//error,雖然使用通配符限定了泛型的範圍,但具體類型還是不肯定的
    list.add(num);
}

//將其替換爲:
public static <T extends Number> void safeAdd(List<T> list, T num) {
    if (num == null) {
        return;
    }

  	//ok,不過num是什麼類型,它都和list元素是同一類型
    list.add(num);
}
複製代碼

總結:

  • 當方法中不須要改變容器時,用通配符,不然用泛型方法
  • 當方法其餘參數,返回值與泛型參數具備依賴關係,使用泛型方法

5.類型擦除(type erasure)

上面所說泛型參數都是java在語法層面的規範定義,是面向編譯器的,在jvm中運行時並不存在泛型,類型被擦除了,全部泛型類型都被替換成Object或者通配符上界類型,若是是容器類型如List則變成List。

ArrayList<Integer> listA = new ArrayList<>();
ArrayList<String> listB = new ArrayList<>();

// listA和listB運行時的類型都是java.util.ArrayList.class, 返回true
System.out.println(listA.getClass() == listB.getClass());
複製代碼

因爲類型擦除的緣由,不能在靜態變量,靜態方法,靜態初始化塊中使用泛型,也不能使用obj instanceof java.util.ArrayList<String>判斷泛型類,接口中定義的泛型。

6.經過反射獲取泛型信息

存在泛型擦除的緣由,運行時是沒法獲取類上的泛型信息的。但對於類的field,類的method上的泛型信息,在編譯器編譯時,將它們存儲到了class文件常量池中(確切是Signature Attrbute),因此能夠經過反射獲取field,method的泛型信息。

在java.lang.reflect中提供Type(Type是java中全部類型的父接口,class就實現了Type)及其幾個子接口用來獲取相關泛型信息,以List爲例:

TypeVariable: 表明類型變量,E

ParameterizedType: 表明類型參數,如List,參數爲String

WildcardType: 通配符類型,如List<?>,List<? extends Number>中的?, ? extends Number

GenericArrayType: 泛型數組,如List[],它的基本類型又是一個ParameterizedType List<java.lang.Integer>

具體API能夠看javadoc,一個簡單演示:

public class GenericCls<T> {

    private T data;

    private List<String> list;

    private List<Integer>[] array;

    public <T> List<String> strings(List<T> data) {
        return Arrays.asList(data.toString());
    }

    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
        Class<GenericCls> cls = GenericCls.class;

        System.out.println("============== class - GenericCls ==============\n");
        TypeVariable<Class<GenericCls>> classTypeVariable = cls.getTypeParameters()[0];
        System.out.println(classTypeVariable.getName());

        Field field = cls.getDeclaredField("list");
        Type genericType = field.getGenericType();
        ParameterizedType pType = (ParameterizedType) genericType;
        System.out.println("============== filed - list ==============\n");
        System.out.println("type: " + genericType.getTypeName());
        System.out.println("rawType: " + pType.getRawType());
        System.out.println("actualType: " + pType.getActualTypeArguments()[0]);

        Method method = cls.getDeclaredMethod("strings", List.class);
        Type genericParameterType = method.getGenericParameterTypes()[0];
        ParameterizedType pMethodType = (ParameterizedType) genericParameterType;
        System.out.println("============== method - strings parameter ==============\n");
        System.out.println("type: " + genericParameterType.getTypeName());
        System.out.println("rawType: " + pMethodType.getRawType());
        System.out.println("actualType: " + pMethodType.getActualTypeArguments()[0]);

        Field array = cls.getDeclaredField("array");
        GenericArrayType arrayType = (GenericArrayType) array.getGenericType();
        System.out.println("============== filed - array ==============\n");
        System.out.println("array type: " + arrayType.getTypeName());
        ParameterizedType arrayParamType = (ParameterizedType) arrayType.getGenericComponentType();
        System.out.println("type: " + arrayParamType.getTypeName());
        System.out.println("rawType: " + arrayParamType.getRawType());
        System.out.println("actualType: " + arrayParamType.getActualTypeArguments()[0]);

    }
}
複製代碼

關於反射與泛型我會在另外的文章中再詳細介紹

7.泛型與數組

java數組是協變的:Pet[] pets = new Cat[10];,但卻沒法建立泛型的數組,能夠建立不帶泛型的數組而後強轉,也能夠聲明泛型數組的引用。

Person<Pet>[] people = new Person<Pet>[10];//error
Person<Pet>[] people = new Person[10];//ok
Person<Pet>[] people = (Person<Pet>[])new Person[10];//ok
public static void consume(Person<? extends Pet>[] people){}//ok
複製代碼

問題:爲何異常類不能使用泛型?


下期預告:詳解class(字節碼)文件

歡迎關注個人我的微信博客

相關文章
相關標籤/搜索