如何正確使用Java泛型


前言

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

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

 


 

一、不要使用原生態類型

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強轉,否則編譯會報錯,在編譯期報錯對於開發者來說是我們最希望看到的。

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

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才對原生態類型支持!

問2:那我們使用List<Object>是不是就可以了呢,兩個有啥區別呢?

兩者都可以插入任意類型的對象。不嚴格來說,前者原生態類型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不合法

 

二、泛型方法

1、基本概念

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

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)

2、泛型單例工廠

  有時候我們需要創建不可變但又適合許多不同類型的對象。之前的單例模式滿足不可變,但不適合不同類型對象,這次我們可以利用泛型做到這點。

/**
 * 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));
    }
}

 

三、有限制的通配符類型

之前提到過的無限制的通配符類型就提到過,無限制的通配符單純只使用"?"(如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;
    }

 

四、類型安全的異構容器

  泛型一般用於集合,如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 的意思

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM