1. 程式人生 > 程式設計 >細說Java 泛型

細說Java 泛型

為了讓集合容器記住元素型別是jdk1.5引入泛型(Generic Types)的一個主要原因。

泛型看起來就是將實際的型別引數化,這樣就可以在使用的時候傳人實際的型別,或者推斷其代表的型別(如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泛型是一種偽泛型,這麼做也是為了相容舊版本。

2.泛型類,泛型介面

我們可以在介面,類上宣告型別行參而將其泛型話:

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後的第一個類或介面型別。

  • 基類劫持介面

    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(位元組碼)檔案

歡迎關注我的個人微信部落格