深刻理解 Java 泛型

轉:https://blog.csdn.net/u011240877/article/details/53545041

首先提個問題:
Java 泛型的做用是什麼?泛型擦除是什麼?泛型通常用在什麼場景?html

若是這個問題你答不上來,那這篇文章可能就對你有些價值。java

讀完本文你將瞭解到:android

什麼是泛型

泛型是Java SE 1.5 的新特性,《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。bash

爲何引入泛型

在引入泛型以前,要想實現一個通用的、能夠處理不一樣類型的方法,你須要使用 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>
 * <p>
 *      Author: shixinzhang
 */
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) {
        //...
    }
}
複製代碼

泛型接口比較實用的使用場景就是用做策略模式的公共策略,好比 Java 解惑:Comparable 和 Comparator 的區別 中介紹的 Comparator,它就是一個泛型接口:

public interface Comparator<T> {

    public int compare(T lhs, T rhs);

    public boolean equals(Object object);
}
複製代碼

泛型接口定義基本的規則,而後做爲引用傳遞給客戶端,這樣在運行時就能傳入不一樣的策略實現類。

泛型方法

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

若是一個方法所在的類不是泛型類,或者他想要處理不一樣於泛型類聲明類型的數據,那它就須要本身聲明類型,舉個例子:

/**
 * 傳統的方法,會有 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 是通配符形式,形式更簡單,但內部調用帶類型參數的方法。

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

上界通配符 < ? 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 中並不會產生這個問題。虛擬機中並無泛型類型對象,全部的對象都是普通類。

(摘自:blog.csdn.net/fw0124/arti…

在 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> 的方法,由於泛型的不可變性,會產生編譯錯誤。

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

相關文章
相關標籤/搜索