如何正確使用Java泛型

前言

  Java 1.5以前是沒有泛型的,之前從集合中讀取每一個對象都必須先進行轉換,若是不當心存入集合中對象類型是錯的,運行過程當中轉換處理會報錯。有了泛型以後編譯器會自動幫助轉換,使程序更加安全,可是要正確使用泛型才能取得事半功倍的效果。java

  本文主要從不要使用原生類型,泛型方法,限制通配符,類型安全的異構容器四個部分來講明如何正確使用Java泛型。主要參考資料《Effective Java》(PDF電子版,有須要的朋友能夠私信評論)數據庫

 


 

1、不要使用原生態類型

1. 什麼是原生態類型?

  原生態類型(Raw type),即不帶任何實際類型參數的泛型名稱。如與List<E>對應的原生態類型List。不推薦List list = new ArrayList()這樣的方式,主要就會丟掉安全性(爲何不安全呢?具體請往下看),應使用List<MyClass> list = new ArrayList()明確類型。或者使用List<Object>(那麼List與List<Object>有啥區別呢?具體能夠看泛型的子類型規則部分)數組

2. 爲何不推薦使用原生態類型?

當咱們使用原生態類型List建立一個集合,並往其中放入Stamp類與Coin類,並迭代循環獲取List集合中的元素。安全

public class RawType_Class {

    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add(new Stamp());
        list.add(new Coin());
        for (Iterator i = list.iterator(); i.hasNext();) {
            Stamp stamp = i.next();
        }
    }

}

此時必須使用Cast強轉,不然編譯會報錯,在編譯期報錯對於開發者來講是咱們最但願看到的。app

可是咱們根據提示,增長Cast,好了編譯是不會報錯了,可是運行時期會報錯! Exception in thread "main" java.lang.ClassCastException: ,這就對咱們開發者來講大大增長了難度。ide

public class RawType_Class {

    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add(new Stamp());
        list.add(new Coin());
        for (Iterator i = list.iterator(); i.hasNext();) {
            Stamp stamp = (Stamp) i.next();
        }
    }

}

因而可知,原生類型是不推薦使用,是不安全的!函數

問1:那爲何Java還要容許使用原生態類型呢?測試

是爲了提高兼容性,Java1.5以前已經存在不少的原生態類型的代碼,那麼爲了讓代碼保持合法,而且可以兼容新代碼,所以Java纔對原生態類型支持!spa

問2:那咱們使用List<Object>是否是就能夠了呢,兩個有啥區別呢?code

二者均可以插入任意類型的對象。不嚴格來講,前者原生態類型List逃避了泛型檢查,後者參數化類型List<Object>明確告訴編譯器可以持有任意類型的對象。可是兩個的區別主要是泛型存在子類型規則,具體請往下看

3. 泛型的子類型規則

子類型規則,即任何參數化的類型是原生態類型的一個子類型,好比List<String>是原生態類型List的一個子類型,而不是參數化List<Object>的子類型。

因爲子類型規則的存在,咱們能夠將List<String>傳遞給List類型的參數

public static void main(String[] args) {
  List<String> strings = new ArrayList<>();
   unsafeAdd(strings, new Integer(1));
   String s = strings.get(0);
}
private static void unsafeAdd(List list, Object o){
  list.add(o);
}

雖然編譯器是沒有報錯的,可是編譯過程會出現如下提示,代表編寫了某種不安全的未受檢的操做

可是咱們不能將List<String>傳遞給List<Object>類型參數

public static void main(String[] args) {
  List<String> strings = new ArrayList<>();
   unsafeAdd(strings, new Integer(1));
   String s = strings.get(0);
}
private static void unsafeAdd(List<Object> list, Object o){
  list.add(o);
}

編譯後就直接報錯,事實上編譯器就會自動提示有錯誤

4. 無限制的通配符類型

  使用原生態類型是很危險的,可是若是不肯定或不關心實際的類型參數,那麼在Java 1.5以後Java有一種安全的替換方法,稱之爲無限制的通配符類型(unbounded wildcard type),能夠用一個「?」代替,好比Set<?>表示某個類型的集合,能夠持有任何集合。

  那麼無限制通配類型與原生態類型有啥區別呢?原生態類型是能夠插入任何類型的元素,可是無限制通配類型的話,不能添加任何元素(null除外)。

  

  問:那麼這樣的通配符類型有意義嗎?由於你並不知道它到底能加入啥樣的元素,可是又美其名曰「無限制」。

不能說沒有意義,由於它的出現歸根結底是爲了防止破壞集合類型約束條件,而且能夠根據須要使用泛型方法或者有限制的通配符類型(bound wildcard type)接口某些限制,提升安全性。

 

5. 泛型的可擦除性

咱們先看一下代碼,看看結果:

public static void main(String[] args) {
    List<String> l1 = new ArrayList<String>();
    List<Integer> l2 = new ArrayList<Integer>();
    //  輸出爲true,擦除後的類型爲List
    System.out.println(l1.getClass() == l2.getClass());

}

結果爲true,這是由於:泛型信息能夠在運行時被擦除,泛型在編譯期有效,在運行期被刪除,也就是說全部泛型參數類型在編譯後都會被清除掉。歸根結底無論泛型被參數具體化成什麼類型,其class都是RawType.class,好比List.class,而不是List<String>.class或List<Integer>.class

事實上,在類文字中必須使用原生態類型,不許使用參數化類型(雖然容許使用數組類型和基本類型),也就是List.class、String[].class和int.class都是合法的,而List<String>.class和List<?>.class不合法

 

2、泛型方法

一、基本概念

   以前說過,若是直接使用原生態類型編譯過程會有警告,運行過程可能會報異常,是很是不安全的一種方式。

private static Set union(Set s1, Set s2){
        Set result = new HashSet();
        result.add(s2);
        return result;
    }

  若是是在方法中使用,爲了修正這些警告,使方法變成類型安全的,能夠爲方法聲明一個類型參數。

 private static <E> Set<E> union(Set<E> s1, Set<E> s2){
        Set result = new HashSet();
        result.add(s2);
        return result;
    }

  static後面的<E>就是方法的類型參數,這樣的話三個集合的類型(兩個輸入參數與一個返回值)必須所有相同。這樣的泛型方法不須要明確指定類型參數的值,而是經過判斷參數的類型計算類型參數的值,對於參數Set<String>而言,編譯器天然知道返回的類型參數E也是String,這就是所謂的類型推導(type inference)

二、泛型單例工廠

  有時候咱們須要建立不可變但又適合許多不一樣類型的對象。以前的單例模式知足不可變,但不適合不一樣類型對象,此次咱們能夠利用泛型作到這點。

/**
 * apply方法接收與返回某個類型T的值
 * @param <T>
 */
public interface UnaryFunction<T> {
    T apply(T arg);
}

  如今咱們須要一個恆等函數(Identity function,f(x)=x,簡單理解輸入等於返回的函數,會返回未被修改的參數),若是每次須要的時候都要從新建立一個,這樣就會很浪費,若是泛型被具體化了,每一個類型都須要一個恆等函數,可是它們被擦除後,就只須要一個泛型單例。

   /**
     * 返回未被修改的參數arg
     */
    private static UnaryFunction<Object> IDENTITY_FUNCTION = (Object arg) -> {
        return arg;
    };

    /**
     * 泛型方法identityFunction:
     *      返回類型:UnaryFunction<T>
     *      類型參數列表;<T>
     * 忽略強制轉換未受檢查的警告:
     * 由於返回未被修改的參數arg,因此咱們知道不管T的值是什麼,都是類型安全的
     * @param <T>
     * @return
     */
    @SuppressWarnings("unchacked")
    public static <T> UnaryFunction<T> identityFunction(){
        return (UnaryFunction<T>) IDENTITY_FUNCTION;
    }

利用泛型單例編寫測試,下面代碼不會報任何的警告或錯誤。

public static void main(String[] args) {
        String[] strings = {"hello","world"};
        UnaryFunction<String> sameString = identityFunction();
        for (String s: strings) {
            System.out.println(sameString.apply(s));
        }
        Number[] numbers = {1,2.0};
        UnaryFunction<Number> sameNumber = identityFunction();
        for (Number n: numbers) {
            System.out.println(sameNumber.apply(n));
        }
        UnaryFunction<Stamp> sameAnotherString = identityFunction();
        System.out.println(sameAnotherString.apply(new Stamp()));
    }

 返回的都是未被修改的參數

 

3. 遞歸類型限制

遞歸類型限制(recursive type bound):經過某個包含該類型自己的表達式來限制類型參數,最廣泛的就是與Comparable一塊兒使用。好比<T extends Comparable<T>>

public interface Comparable<T> {    
    public int compareTo(T o);
}

類型參數T定義的類型,能夠與實現Comparable<T>的類型進行比較,實際上,幾乎全部類型都只能與它們自身類型的元素相比較,好比String實現Comparable<String>Integer實現Comparable<Integer>

實現compareTo方法

String之間能夠相互使用compareTo比較:

String s1 = "a";
String s2 = "b";
s1.compareTo(s2);

一般爲了對列表進行排序,並在其中進行搜索,計算出它的最小值或最大值等,就要求列表中的每一個元素都可以與列表中每一個其它元素能進行比較,換句話說,列表的元素能夠互相比較。每每就須要實現Comparable接口的元素列表。

/**
 * @author jian
 * @date 2019/4/1
 * @description 遞歸類型限制
 */
public class Recursive_Type_Bound {

    /**
     * 遞歸類型限制(recursive type bound)
     * <T extends Comparable<T>>表示能夠與自身進行比較的每一個類型T,即實現Comparable<T>接口的類型均可以與自身進行比較,能夠查看String、Integer源碼
     * <T extends Comparable<T>>類型參數,表示傳入max方法的參數必須實現Comparable<T>接口,才能使用compareTo方法
     * @param list
     * @param <T>
     * @return
     */
    public static <T extends Comparable<T>> T max(List<T> list) {
        Iterator<T> iterator = list.iterator();
        T result = iterator.next();
        while (iterator.hasNext()) {
            T t = iterator.next();
            if (t.compareTo(result) > 0) {
                result = t;
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("1","2");
        System.out.println(max(list));
    }
}

 

3、有限制的通配符類型

以前提到過的無限制的通配符類型就提到過,無限制的通配符單純只使用"?"(如Set<?>),而有限制的通配符每每有以下形式,經過有限制的通配符類型能夠大大提高API的靈活性。

  (1)E的某種超類集合(接口):Collection<? super E>、Interface<? super E>、

  (2)E的某個子類集合(接口):Collection<? extends E>、Interface<? extends E>

問1:那麼何時使用extends關鍵字,什麼什麼使用super關鍵字呢?

有這樣一個PECS(producer-extends, consumer-super)原則:若是參數化類型表示一個T生產者,就使用<? extends T>,若是表示消費者就是<? super T>。能夠這樣助記

問2:什麼是生產者,什麼是消費者

1)生產者:產生T不能消費T,針對collection,對每一項元素操做時,此時這個集合時生產者(生產元素),使用Collection<? extends T>。只能讀取,不能寫入

2)消費者:不能生產T,只消費使用T,針對collection,添加元素collection中,此時集合消費元素,使用Collection<? super T>,只能添加T的子類及自身,用Object接收讀取到的元素

舉例說明:生產者

1)你不能在List<? extends Number>中add操做,由於你增長Integer可能會指向List<Double>,你增長Double可能會指向Integer。根本不能確保列表中最終保存的是什麼類型。換句話說Number的全部子類從類關係上來講都是平級的,毫無聯繫的。並不能依賴類型推導(類型轉換),編譯器是沒法確實的實際類型的!

 

2)可是你能夠讀取其中的元素,並保證讀取出來的必定是Number的子類(包括Number),編譯並不會報錯,換句話說編譯器知道里面的元素都是Number的子類,無論是Integer仍是Double,編譯器均可以向下轉型

舉例說明:消費者

 1)編譯器不知道存入列表中的Number的超類具體是哪個,只能使用Object去接收

2)可是只能夠添加Interger及其子類(由於Integer子類也是Integer,向上轉型),不能添加Object、Number。由於插入Number對象能夠指向List<Integer>對象,你插入Object,由於可能會指向List<Ineger>對象

 

 

注意:Comparable/Comparator都是消費者,一般使用Comparator<? Super T>),能夠將上述的max方法進行改造:
 public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
        Iterator<? extends T> iterator = list.iterator();
        T result = iterator.next();
        while (iterator.hasNext()) {
            T t = iterator.next();
            if (t.compareTo(result) > 0) {
                result = t;
            }
        }
        return result;
    }

 

4、類型安全的異構容器

  泛型通常用於集合,如Set和Map等,這些容器都是被參數化了(類型已經被具體化了,參數個數已被固定)的容器,只能限制每一個容器只能固定數目的類型參數,好比Set只能一個類型參數,表示它的元素類型,Map有兩個參數,表示它的鍵與值。

  可是有時候你會須要更多的靈活性,好比關係數據庫中能夠有任意多的列,若是以類型的方式全部列就行了。有一種方法能夠實現,那就是使用將鍵進行參數化而不是容器參數化,而後將參數化的鍵提交給容器,來插入或獲取值,用泛型來確保值的類型與它的鍵相符。

  咱們實現一個Favorite類,能夠經過Class類型來獲取相應的value值,鍵能夠是不一樣的Class類型(鍵Class<?>參數化,而不是Map<?>容器參數化)。利用Class.cast方法將鍵與鍵值的類型對應起來,不會出現  favorites.putFavorite(Integer.class, "Java") 這樣的狀況。

 
/**
 * @author jian
 * @date 2019/4/1
 * @description 類型安全的異構容器
 */
public class Favorites {

    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance){
        if (type == null) {
            throw new NullPointerException("Type is null");
        }
        favorites.put(type, type.cast(instance));
    }

    public <T> T getFavorite(Class<T> type){
        return type.cast(favorites.get(type));
    }

}
 

  Favorites實例是類型安全(typesafe)的,你請求String時,不會返回給你Integer,同時也是異構(heterogeneous)的,不像普通map,它的鍵均可以是不一樣類型的。所以,咱們將Favorites稱之爲類型安全的異構容器(typesafe heterogeneous container)。

 public static void main(String[] args) {
        Favorites favorites = new Favorites();
        favorites.putFavorite(String.class, "Java");
        favorites.putFavorite(Integer.class, 64);
        favorites.putFavorite(Class.class, Favorites.class);
        String favoriteString = favorites.getFavorite(String.class);
        Integer favoriteInteger = favorites.getFavorite(Integer.class);
        Class<?> favoriteClass = favorites.getFavorite(Class.class);
     // 輸出 Java 40 Favorites System.out.printf(
"%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getSimpleName()); }

  Favorites類侷限性在於它不能用於在不可具體化的類型中,換句話說你能夠保存String,String[],可是你不能保存List<String>,由於你沒法爲List<String>獲取一個Class對象:List<String>.class是錯誤的,無論是List<String>仍是List<Integer>都會公用一個List.class對象。

 
  List<String> list = Arrays.asList("1","2");
  List<Integer> list2 = Arrays.asList(3,4);
  // 只能選一種,不能有List<String>.class或者List<Integer>.class
  favorites.putFavorite(List.class, list2);
  // favorites.putFavorite(List.class, list)

 

 


 

附1:相關泛型術語

  1)參數化的類型:List<String>

  2)實際類型參數:String

  3)泛型:List<E>

  4)形式類型參數:E

  5)無限制通配符類型:List<?>

  6)原生態類型:List

  7)遞歸類型限制:<T extends Comparable<T>>

  8)有限制的通配符類型:List<? extends Number>

  9)泛型方法:static <E> List<E> union()

  10)類型令牌:String.class


附2:經常使用的形式類型參數

  1)T 表明通常的任何類。

  2)E 表明 Element 的意思,或者 Exception 異常的意思。

  3)K 表明 Key 的意思。

  4)V 表明 Value 的意思,一般與 K 一塊兒配合使用。

  5)S 表明 Subtype 的意思

相關文章
相關標籤/搜索