Java 泛型

什麼是泛型

《Java 核心技術》中對泛型的定義是:

“泛型” 意味着編寫的代碼可以被不同類型的對象所重用。


可見泛型的提出是爲了編寫重用性更好的代碼。

泛型的本質是參數化類型,也就是說所操作的數據類型被指定爲一個參數。 
比如常見的集合類 LinkedList:

public class LinkedList<E> extends AbstractSequentialList<E> implements
    List<E>, Deque<E>, Queue<E>, Cloneable, Serializable {
//...

transient Link<E> voidLink;

//...
}
可以看到,LinkedList<E> 類名及其實現的接口名後有個特殊的部分 “”,而且它的成員的類型 Link<E> 也包含一個 “”,這個符號的就是 類型參數,它使得在運行中,創建一個 LinkedList 時可以傳入不同的類型,比如 new LinkedList,這樣它的成員存放的類型也是 String。

爲什麼引入泛型

在引入泛型之前,要想實現一個通用的、可以處理不同類型的方法,你需要使用 Object 作爲屬性和方法參數,比如這樣:
public class Generic {
    private Object[] mData;

    public Generic(int capacity) {
        mData = new Object[capacity];
    }

    public Object getData(int index) {
        //...
        return mData[index];
    }

    public void add(int index, Object item) {
        //...
        mData[index] = item;
    }
}
它使用一個 Object 數組來保存數據,這樣在使用時可以添加不同類型的對象:
    Generic generic = new Generic(10);
    generic.add(0,"shixin");
    generic.add(1, 23);
然而由於 Object 是所有類的父類,所有的類都可以作爲成員被添加到上述類中;當需要使用的時候,必須進行強制轉換,而且這個強轉很有可能出現轉換異常:
    String item1 = (String) generic.getData(0);
    String item2 = (String) generic.getData(1);
上面第二行代碼將一個 Integer 強轉成 String,運行時會報錯 :
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

at net.sxkeji.shixinandroiddemo2.test.generic.GenericTest.getData(GenericTest.java:46)
可以看到,使用 Object 來實現通用、不同類型的處理,有這麼兩個缺點:
1、每次使用時都需要強制轉換成想要的類型
2、在編譯時編譯器並不知道類型轉換是否正常,運行時才知道,不安全

根據《Java 編程思想》中的描述,泛型出現的動機在於:
有許多原因促成了泛型的出現,而最引人注意的一個原因,就是爲了創建容器類。


事實上,在 JDK 1.5 出現泛型以後,許多集合類都使用泛型來保存不同類型的元素,比如 Collection:
public interface Collection<E> extends Iterable<E> {

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);
    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean removeAll(Collection<?> c);
}   
實際上引入泛型的主要目標有以下幾點:
  • 類型安全 
    • 泛型的主要目標是提高 Java 程序的類型安全
    • 編譯時期就可以檢查出因 Java 類型不正確導致的 ClassCastException 異常
    • 符合越早出錯代價越小原則
  • 消除強制類型轉換 
    • 泛型的一個附帶好處是,使用時直接得到目標類型,消除許多強制類型轉換
    • 所得即所需,這使得代碼更加可讀,並且減少了出錯機會
  • 潛在的性能收益 
    • 由於泛型的實現方式,支持泛型(幾乎)不需要 JVM 或類文件更改
    • 所有工作都在編譯器中完成
    • 編譯器生成的代碼跟不使用泛型(和強制類型轉換)時所寫的代碼幾乎一致,只是更能確保類型安全而已

泛型的使用方式

泛型的本質是參數化類型,也就是說所操作的數據類型被指定爲一個參數。

類型參數的意義是告訴編譯器這個集合中要存放實例的類型,從而在添加其他類型時做出提示,在編譯時就爲類型安全做了保證。

這種參數類型可以用在類、接口和方法的創建中,分別稱爲泛型類、泛型接口、泛型方法。

/**
 * <header>
 *      Description: 泛型類
 * </header>
 */
public class GenericClass<F> {
    private F mContent;

    public GenericClass(F content){
        mContent = content;
    }

    /**
     * 泛型方法
     * @return
     */
    public F getContent() {
        return mContent;
    }

    public void setContent(F content) {
        mContent = content;
    }

    /**
     * 泛型接口
     * @param <T>
     */
    public interface GenericInterface<T>{
        void doSomething(T t);
    }
}

泛型類

泛型類和普通類的區別就是類名後有類型參數列表 <E>,既然叫“列表”了,當然這裏的類型參數可以有多個,比如 public class HashMap<K, V>,參數名稱由開發者決定。

類名中聲明參數類型後,內部成員、方法就可以使用這個參數類型,比如上面的 GenericClass<F> 就是一個泛型類,它在類名後聲明瞭類型 F,它的成員、方法就可以使用 F 表示成員類型、方法參數/返回值都是 F 類型。

泛型類最常見的用途就是作爲容納不同類型數據的容器類,比如 Java 集合容器類。

泛型接口

和泛型類一樣,泛型接口在接口名後添加類型參數,比如上面的 GenericInterface<T>,接口聲明類型後,接口方法就可以直接使用這個類型。

實現類在實現泛型接口時需要指明具體的參數類型,不然默認類型是 Object,這就失去了泛型接口的意義。

未指明類型的實現類,默認是 Object 類型:

public class Generic implements GenericInterface{

    @Override
    public void doSomething(Object o) {
        //...
    }
}
指明瞭類型的實現:

public class Generic implements GenericInterface<String>{
    @Override
    public void doSomething(String s) {
        //...
    }
}

泛型方法

泛型方法是指使用泛型的方法,如果它所在的類是個泛型類,那就很簡單了,直接使用類聲明的參數。

如果一個方法所在的類不是泛型類,或者他想要處理不同於泛型類聲明類型的數據,那它就需要自己聲明類型,舉個例子:

/**
 * 傳統的方法,會有 unchecked ... raw type 的警告
 * @param s1
 * @param s2
 * @return
 */
public Set union(Set s1, Set s2){
    Set result = new HashSet(s1);
    result.addAll(s2);
    return result;
}

/**
 * 泛型方法,介於方法修飾符和返回值之間的稱作 類型參數列表 <A,V,F,E...> (可以有多個)
 *      類型參數列表 指定參數、返回值中泛型參數的類型範圍,命名慣例與泛型相同
 * @param s1
 * @param s2
 * @param <E>
 * @return
 */
public <E> Set<E> union2(Set<E> s1, Set<E> s2){
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}
注意上述代碼在返回值前面也有個 <E>,它和類名後面的類型參數列表意義一致,指明瞭這個方法中類型參數的意義、範圍。

泛型的通配符

有時候希望傳入的類型有一個指定的範圍,從而可以進行一些特定的操作,這時候就是通配符邊界登場的時候了。

泛型中有三種通配符形式:

<?> 無限制通配符

<? extends E> extends 關鍵字聲明瞭類型的上界,表示參數化的類型可能是所指定的類型,或者是此類型的子類

<? super E> super 關鍵字聲明瞭類型的下界,表示參數化的類型可能是指定的類型,或者是此類型的父類

無限制通配符 < ?>

要使用泛型,但是不確定或者不關心實際要操作的類型,可以使用無限制通配符(尖括號裏一個問號,即 <?> ),表示可以持有任何類型。

大部分情況下,這種限制是好的,但這使得一些理應正確的基本操作都無法完成,比如交換兩個元素的位置,看代碼:

private void swap(List<?> list, int i, int j){
    Object o = list.get(i);
    list.set(j,o);
}
這個代碼看上去應該是正確的,但 Java 編譯器會提示編譯錯誤,set 語句是非法的。編譯器提示我們把方法中的 List<?> 改成 List<Object> 就好了,這是爲什麼呢? ? 和 Object 不一樣嗎?

的確因爲 ? 和 Object 不一樣,List<?> 表示未知類型的列表,而 List<Object> 表示任意類型的列表。

比如傳入個 List<String> ,這時 List 的元素類型就是 String,想要往 List 裏添加一個 Object,這當然是不可以的。

藉助帶類型參數的泛型方法,這個問題可以這樣解決:

private <E> void swapInternal(List<E> list, int i, int j) {
    //...
    list.set(i, list.set(j, list.get(i)));
}

private void swap(List<?> list, int i, int j){
    swapInternal(list, i, j);
}

swap 可以調用 swapInternal,而帶類型參數的 swapInternal 可以寫入。Java容器類中就有類似這樣的用法,公共的 API 是通配符形式,形式更簡單,但內部調用帶類型參數的方法。

(這個例子引自: http://mp.weixin.qq.com/s/te9K3alu8P8jRUUU2AkO3g )

上界通配符 < ? extends E>

在類型參數中使用 extends 表示這個泛型中的參數必須是 E 或者 E 的子類,這樣有兩個好處:

  • 如果傳入的類型不是 E 或者 E 的子類,編輯不成功
  • 泛型中可以使用 E 的方法,要不然還得強轉成 E 才能使用

舉個例子:

/**
 * 有限制的通配符之 extends (有上限),表示參數類型 必須是 BookBean 及其子類,更靈活
 * @param arg1
 * @param arg2
 * @param <E>
 * @return
 */
private <K extends ChildBookBean, E extends BookBean> E test2(K arg1, E arg2){
    E result = arg2;
    arg2.compareTo(arg1);
    //.....
    return result;
}
可以看到,類型參數列表中如果有多個類型參數上限,用逗號分開。

下界通配符 < ? super E>

在類型參數中使用 super 表示這個泛型中的參數必須是 E 或者 E 的父類。

根據代碼介紹吧:

private <E> void add(List<? super E> dst, List<E> src){
    for (E e : src) {
        dst.add(e);
    }
}
可以看到,上面的 dst 類型 “大於等於” src 的類型,這裏的“大於等於”是指 dst 表示的範圍比 src 要大,因此裝得下 dst 的容器也就能裝 src。

通配符比較

通過上面的例子我們可以知道,無限制通配符 < ?> 和 Object 有些相似,用於表示無限制或者不確定範圍的場景。

兩種有限制通配形式 < ? super E> 和 < ? extends E> 也比較容易混淆,我們再來比較下。

它們的目的都是爲了使方法接口更爲靈活,可以接受更爲廣泛的類型。

  • < ? super E> 用於靈活寫入或比較,使得對象可以寫入父類型的容器,使得父類型的比較方法可以應用於子類對象。
  • < ? extends E> 用於靈活讀取,使得方法可以讀取 E 或 E 的任意子類型的容器對象。

用《Effective Java》 中的一個短語來加深理解:

爲了獲得最大限度的靈活性,要在表示 生產者或者消費者 的輸入參數上使用通配符,使用的規則就是:生產者有上限、消費者有下限:

PECS: producer-extends, costumer-super

因此使用通配符的基本原則:

  • 如果參數化類型表示一個 T 的生產者,使用 < ? extends T>;
  • 如果它表示一個 T 的消費者,就使用 < ? super T>;
  • 如果既是生產又是消費,那使用通配符就沒什麼意義了,因爲你需要的是精確的參數類型。

小總結一下:

  • T 的生產者的意思就是結果會返回 T,這就要求返回一個具體的類型,必須有上限纔夠具體;
  • T 的消費者的意思是要操作 T,這就要求操作的容器要夠大,所以容器需要是 T 的父類,即 super T;

舉個例子:

    private  <E extends Comparable<? super E>> E max(List<? extends E> e1){
        if (e1 == null){
            return null;
        }
        //迭代器返回的元素屬於 E 的某個子類型
        Iterator<? extends E> iterator = e1.iterator();
        E result = iterator.next();
        while (iterator.hasNext()){
            E next = iterator.next();
            if (next.compareTo(result) > 0){
                result = next;
            }
        }
        return result;
    }

上述代碼中的類型參數 E 的範圍是 <E extends Comparable<? super E>>,我們可以分步查看:

  1. 要進行比較,所以 E 需要是可比較的類,因此需要 extends Comparable<…>(注意這裏不要和繼承的 extends 搞混了,不一樣)
  2. Comparable< ? super E> 要對 E 進行比較,即 E 的消費者,所以需要用 super
  3. 而參數 List< ? extends E> 表示要操作的數據是 E 的子類的列表,指定上限,這樣容器纔夠大

泛型的類型擦除

Java 中的泛型和 C++ 中的模板有一個很大的不同:

  • C++ 中模板的實例化會爲每一種類型都產生一套不同的代碼,這就是所謂的代碼膨脹。
  • Java 中並不會產生這個問題。虛擬機中並沒有泛型類型對象,所有的對象都是普通類。

(摘自:http://blog.csdn.net/fw0124/article/details/42295463

在 Java 中,泛型是 Java 編譯器的概念,用泛型編寫的 Java 程序和普通的 Java 程序基本相同,只是多了一些參數化的類型同時少了一些類型轉換。

實際上泛型程序也是首先被轉化成一般的、不帶泛型的 Java 程序後再進行處理的,編譯器自動完成了從 Generic Java 到普通 Java 的翻譯,Java 虛擬機運行時對泛型基本一無所知。

當編譯器對帶有泛型的java代碼進行編譯時,它會去執行類型檢查類型推斷,然後生成普通的不帶泛型的字節碼,這種普通的字節碼可以被一般的 Java 虛擬機接收並執行,這在就叫做 類型擦除(type erasure)

實際上無論你是否使用泛型,集合框架中存放對象的數據類型都是 Object,這一點不僅僅從源碼中可以看到,通過反射也可以看到。

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());//true
上面代碼輸出結果並不是預期的 false,而是 true。其原因就是泛型的擦除。

擦除的實現原理

一直有個疑問,Java 編譯器在編譯期間擦除了泛型的信息,那運行中怎麼保證添加、取出的類型就是擦除前聲明的呢?

這篇文章瞭解到,原來泛型也只是一個語法糖,摘幾段話加深理解:

The buzzing keyword is “Type Erasure”, you guessed it right it’s the same thing we used to in our schools for erasing our mistakes in writing or drawing :).

The Same thing is done by Java compiler, when it sees code written using Generics it completely erases that code and convert it into raw type i.e. code without Generics. All type related information is removed during erasing. So your ArrayList becomes plain old ArrayList prior to JDK 1.5, formal type parameters e.g. < K, V> or < E> gets replaced by either Object or Super Class of the Type.

Also, when the translated code does not have correct type, the compiler inserts a type casting operator. This all done behind the scene so you don’t need to worry about what important to us is that Java compiler guarantees type-safety and flag any type-safety relate error during compilation.

In short Generics in Java is syntactic sugar and doesn’t store any type related information at runtime. All type related information is erased by Type Erasure, this was the main requirement while developing Generics feature in order to reuse all Java code written without Generics.


大概意思就是:

Java 編輯器會將泛型代碼中的類型完全擦除,使其變成原始類型。

當然,這時的代碼類型和我們想要的還有距離,接着 Java 編譯器會在這些代碼中加入類型轉換,將原始類型轉換成想要的類型。這些操作都是編譯器後臺進行,可以保證類型安全。

總之泛型就是一個語法糖,它運行時沒有存儲任何類型信息。

擦除導致的泛型不可變性

泛型中沒有邏輯上的父子關係,如 List 並不是 List 的父類。兩者擦除之後都是List,所以形如下面的代碼,編譯器會報錯:

/**
 * 兩者並不是方法的重載。擦除之後都是同一方法,所以編譯不會通過。
 * 擦除之後:
 * 
 * void m(List numbers){}
 * void m(List strings){} //編譯不通過,已經存在相同方法簽名
 */
void method(List<Object> numbers) {

}

void method(List<String> strings) {

}

泛型的這種情況稱爲 不可變性,與之對應的概念是 協變、逆變:

  • 協變:如果 A 是 B 的父類,並且 A 的容器(比如 List< A>) 也是 B 的容器(List< B>)的父類,則稱之爲協變的(父子關係保持一致)
  • 逆變:如果 A 是 B 的父類,但是 A 的容器 是 B 的容器的子類,則稱之爲逆變(放入容器就篡位了)
  • 不可變:不論 A B 有什麼關係,A 的容器和 B 的容器都沒有父子關係,稱之爲不可變

Java 中數組是協變的,泛型是不可變的。

如果想要讓某個泛型類具有協變性,就需要用到邊界。

擦除的拯救者:邊界

我們知道,泛型運行時被擦除成原始類型,這使得很多操作無法進行.

如果沒有指明邊界,類型參數將被擦除爲 Object。

如果我們想要讓參數保留一個邊界,可以給參數設置一個邊界,泛型參數將會被擦除到它的第一個邊界(邊界可以有多個),這樣即使運行時擦除後也會有範圍。

比如:

public class GenericErasure {
    interface Game {
        void play();
    }
    interface Program{
        void code();
    }

    public static class People<T extends Program & Game>{
        private T mPeople;

        public People(T people){
            mPeople = people;
        }

        public void habit(){
            mPeople.code();
            mPeople.play();
        }
    }
}
上述代碼中, People 的類型參數 T 有兩個邊界,編譯器事實上會把類型參數替換爲它的第一個邊界的類型。

泛型的規則

  • 泛型的參數類型只能是類(包括自定義類),不能是簡單類型。
  • 同一種泛型可以對應多個版本(因爲參數類型是不確定的),不同版本的泛型類實例是不兼容的。
  • 泛型的類型參數可以有多個
  • 泛型的參數類型可以使用 extends 語句,習慣上稱爲“有界類型”
  • 泛型的參數類型還可以是通配符類型,例如 Class

泛型的使用場景

當類中要操作的引用數據類型不確定的時候,過去使用 Object 來完成擴展,JDK 1.5後推薦使用泛型來完成擴展,同時保證安全性。

總結

1.上面說到使用 Object 來達到複用,會失去泛型在安全性和直觀表達性上的優勢,那爲什麼 ArrayList 等源碼中的還能看到使用 Object 作爲類型?

根據《Effective Java》中所述,這裏涉及到一個 “移植兼容性”:

泛型出現時,Java 平臺即將進入它的第二個十年,在此之前已經存在了大量沒有使用泛型的 Java 代碼。人們認爲讓這些代碼全部保持合法,並且能夠與使用泛型的新代碼互用,非常重要。

這樣都是爲了兼容,新代碼裏要使用泛型而不是原始類型。

2.泛型是通過擦除來實現的。因此泛型只在編譯時強化它的類型信息,而在運行時丟棄(或者擦除)它的元素類型信息。擦除使得使用泛型的代碼可以和沒有使用泛型的代碼隨意互用。

3.如果類型參數在方法聲明中只出現一次,可以用通配符代替它。

比如下面的 swap 方法,用於交換指定 List 中的兩個位置的元素:

private <E> void swap(List<E> list, int i, int j) {
    //...
}
只出現了一次 類型參數,沒有必要聲明,完全可以用通配符代替:
private void swap(List<?> list, int i, int j){
    //...
}

對比一下,第二種更加簡單清晰吧。

4.數組中不能使用泛型

這可能是 Java 泛型面試題中最簡單的一個了,當然前提是你要知道 Array 事實上並不支持泛型,這也是爲什麼 Joshua Bloch 在 《Effective Java》一書中建議使用 List 來代替 Array,因爲 List 可以提供編譯期的類型安全保證,而 Array 卻不能。

5.Java 中 List<Object> 和原始類型 List 之間的區別?

原始類型和帶參數類型 之間的主要區別是:

  • 在編譯時編譯器不會對原始類型進行類型安全檢查,卻會對帶參數的類型進行檢查
  • 通過使用 Object 作爲類型,可以告知編譯器該方法可以接受任何類型的對象,比如String 或 Integer
  • 你可以把任何帶參數的類型傳遞給原始類型 List,但卻不能把 List< String> 傳遞給接受 List< Object> 的方法,因爲泛型的不可變性,會產生編譯錯誤。

這道題的考察點在於對泛型中原始類型的正確理解。







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章