Java泛型詳解

因爲博客的特殊顯示緣由,尖括號用()代替

泛型概述

Java泛型(generics)是JDK 5中引入的一個新特性,容許在定義類和接口的時候使用類型參數(type parameter)。聲明的類型參數在使用時用具體的類型來替換。java

優缺點

從好的方面來講,泛型的引入能夠解決以前的集合類框架在使用過程當中一般會出現的運行時刻類型錯誤,由於編譯器能夠在編譯時刻就發現不少明顯的錯誤。而從很差的地方來講,爲了保證與舊有版本的兼容性,Java泛型的實現上存在着一些不夠優雅的地方。固然這也是任何有歷史的編程語言所須要承擔的歷史包袱。後續的版本更新會爲早期的設計缺陷所累。編程

舉例

List(Object)做爲形式參數,那麼若是嘗試將一個List(String)的對象做爲實際參數傳進去,卻發現沒法經過編譯。雖然從直覺上來講,Object是String的父類,這種類型轉換應該是合理的。可是實際上這會產生隱含的類型轉換問題,所以編譯器直接就禁止這樣的行爲。segmentfault

類型擦除

正確理解泛型概念的首要前提是理解類型擦除(type erasure)。數組

Java中的泛型基本上都是在編譯器這個層次來實現的。

在生成的Java字節代碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會被編譯器在編譯的時候去掉。這個過程就稱爲類型擦除。 如在代碼中定義的List(Object)和List(String)等類型,在編譯以後都會變成List。JVM看到的只是List,而由泛型附加的類型信息對JVM來講是不可見的。Java編譯器會在編譯時儘量的發現可能出錯的地方,可是仍然沒法避免在運行時刻出現類型轉換異常的狀況。類型擦除也是Java的泛型實現方式與C++模板機制實現方式之間的重要區別。安全

不少泛型的奇怪特性都與這個類型擦除的存在有關

1.泛型類並無本身獨有的Class類對象。好比並不存在List(String).class或是List(Integer).class,而只有List.class。bash

2.靜態變量是被泛型類的全部實例所共享的。對於聲明爲MyClass(T)的類,訪問其中的靜態變量的方法仍然是 MyClass.myStaticVar。不論是經過new MyClass(String)仍是new MyClass(Integer)建立的對象,都是共享一個靜態變量。markdown

3.泛型的類型參數不能用在Java異常處理的catch語句中。由於異常處理是由JVM在運行時刻來進行的。因爲類型信息被擦除,JVM是沒法區分兩個異常類型MyException(String)和MyException(Integer)的。對於JVM來講,它們都是 MyException類型的。也就沒法執行與異常對應的catch語句。框架

類型擦除的過程

類型擦除的基本過程也比較簡單,首先是找到用來替換類型參數的具體類。這個具體類通常是Object。若是指定了類型參數的上界的話,則使用這個上界。把代碼中的類型參數都替換成具體的類。同時去掉出現的類型聲明,即去掉<>的內容。好比T get()方法聲明就變成了Object get();List(String)就變成了List。接下來就可能須要生成一些橋接方法(bridge method)。這是因爲擦除了類型以後的類可能缺乏某些必須的方法。dom

實例分析

瞭解了類型擦除機制以後,就會明白編譯器承擔了所有的類型檢查工做。編譯器禁止某些泛型的使用方式,正是爲了確保類型的安全性。以上面提到的List(Object)和List(String)爲例來具體分析:編程語言

public void inspect(List(Object) list) {    
    for (Object obj : list) {        
        System.out.println(obj);    
    }    
    list.add(1); //這個操做在當前方法的上下文是合法的。 
}
public void test() {    
    List(String) strs = new ArrayList(String)();    
    inspect(strs); //編譯錯誤 
}  
複製代碼

這段代碼中,inspect方法接受List(Object)做爲參數,當在test方法中試圖傳入List(String)的時候,會出現編譯錯誤。假設這樣的作法是容許的,那麼在inspect方法就能夠經過list.add(1)來向集合中添加一個數字。這樣在test方法看來,其聲明爲List(String)的集合中卻被添加了一個Integer類型的對象。這顯然是違反類型安全的原則的,在某個時候確定會拋出ClassCastException。所以,編譯器禁止這樣的行爲。編譯器會盡量的檢查可能存在的類型安全問題。對於肯定是違反相關原則的地方,會給出編譯錯誤。當編譯器沒法判斷類型的使用是否正確的時候,會給出警告信息。

泛型類

容器類應該算得上最具重用性的類庫之一。先來看一個沒有泛型的狀況下的容器類如何定義:

public class Container {
    private String key;
    private String value;

    public Container(String k, String v) {
        key = k;
        value = v;
    }
    
    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}
複製代碼

Container類保存了一對key-value鍵值對,可是類型是定死的,也就說若是我想要建立一個鍵值對是String-Integer類型的,當前這個Container是作不到的,必須再自定義。那麼這明顯重用性就很是低。

固然,我能夠用Object來代替String,而且在Java SE5以前,咱們也只能這麼作,因爲Object是全部類型的基類,因此能夠直接轉型。可是這樣靈活性仍是不夠,由於仍是指定類型了,只不過此次指定的類型層級更高而已,有沒有可能不指定類型?有沒有可能在運行時才知道具體的類型是什麼?

因此,就出現了泛型。

public class Container(K, V) {
    private K key;
    private V value;

    public Container(K k, V v) {
        key = k;
        value = v;
    }

    public K getKey() {
        return key;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }
}
複製代碼

在編譯期,是沒法知道K和V具體是什麼類型,只有在運行時纔會真正根據類型來構造和分配內存。能夠看一下如今Container類對於不一樣類型的支持狀況:

public class Main {

    public static void main(String[] args) {
        Container<String, String> c1 = new Container<String, String>("name", "findingsea");
        Container<String, Integer> c2 = new Container<String, Integer>("age", 24);
        Container<Double, Double> c3 = new Container<Double, Double>(1.1, 2.2);
        System.out.println(c1.getKey() + " : " + c1.getValue());
        System.out.println(c2.getKey() + " : " + c2.getValue());
        System.out.println(c3.getKey() + " : " + c3.getValue());
    }
}
複製代碼
輸出:

name : findingsea
age : 24
1.1 : 2.2
複製代碼

泛型接口

在泛型接口中,生成器是一個很好的理解,看以下的生成器接口定義:

public interface Generator<T> {
    public T next();
}
而後定義一個生成器類來實現這個接口:

public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}
調用:

public class Main {

    public static void main(String[] args) {
        FruitGenerator generator = new FruitGenerator();
        System.out.println(generator.next());
        System.out.println(generator.next());
        System.out.println(generator.next());
        System.out.println(generator.next());
    }
}
輸出:

Banana
Banana
Pear
Banana
複製代碼

泛型方法

一個基本的原則是:不管什麼時候,只要你能作到,你就應該儘可能使用泛型方法。也就是說,若是使用泛型方法能夠取代將整個類泛化,那麼應該有限採用泛型方法。下面來看一個簡單的泛型方法的定義:

public class Main {

    public static <T> void out(T t) {
        System.out.println(t);
    }

    public static void main(String[] args) {
        out("findingsea");
        out(123);
        out(11.11);
        out(true);
    }
}
複製代碼

能夠看到方法的參數完全泛化了,這個過程涉及到編譯器的類型推導和自動打包,也就說原來須要咱們本身對類型進行的判斷和處理,如今編譯器幫咱們作了。這樣在定義方法的時候沒必要考慮之後到底須要處理哪些類型的參數,大大增長了編程的靈活性。

再看一個泛型方法和可變參數的例子:

public class Main {

    public static <T> void out(T... args) {
        for (T t : args) {
            System.out.println(t);
        }
    }

    public static void main(String[] args) {
        out("findingsea", 123, 11.11, true);
    }
}
複製代碼

通配符與上下界

在使用泛型類的時候,既能夠指定一個具體的類型,如List(String)就聲明瞭具體的類型是String;也能夠用通配符?來表示未知類型,如List就聲明瞭List中包含的元素類型是未知的。 通配符所表明的實際上是一組類型,但具體的類型是未知的。List所聲明的就是全部類型都是能夠的。可是List並不等同於List(Object)。List(Object)實際上肯定了List中包含的是Object及其子類,在使用的時候均可以經過Object來進行引用。而List則其中所包含的元素類型是不肯定。其中可能包含的是String,也多是 Integer。若是它包含了String的話,往裏面添加Integer類型的元素就是錯誤的。正由於類型未知,就不能經過new ArrayList(?)()的方法來建立一個新的ArrayList對象。由於編譯器沒法知道具體的類型是什麼。可是對於 List(?)中的元素確老是能夠用Object來引用的,由於雖然類型未知,但確定是Object及其子類。考慮下面的代碼:

public void wildcard(List(?) list) { list.add(1);//編譯錯誤 }
如上所示,試圖對一個帶通配符的泛型類進行操做的時候,老是會出現編譯錯誤。其緣由在於通配符所表示的類型是未知的。

由於對於List(?)中的元素只能用Object來引用,在有些狀況下不是很方便。在這些狀況下,可使用上下界來限制未知類型的範圍。 如List(? extends Number)說明List中可能包含的元素類型是Number及其子類。而List(? super Number)則說明List中包含的是Number及其父類。當引入了上界以後,在使用類型的時候就可使用上界類中定義的方法。好比訪問 List(? extends Number)的時候,就可使用Number類的intValue等方法。

類型系統

在Java中,你們比較熟悉的是經過繼承機制而產生的類型體系結構。好比String繼承自Object。根據Liskov替換原則,子類是能夠替換父類的。當須要Object類的引用的時候,若是傳入一個String對象是沒有任何問題的。可是反過來的話,即用父類的引用替換子類引用的時候,就須要進行強制類型轉換。編譯器並不能保證運行時刻這種轉換必定是合法的。這種自動的子類替換父類的類型轉換機制,對於數組也是適用的。 String[]能夠替換Object[]。可是泛型的引入,對於這個類型系統產生了必定的影響。正如前面提到的List(String)是不能替換掉List(Object)的。

引入泛型以後的類型系統增長了兩個維度:

一個是類型參數自身的繼承體系結構,另一個是泛型類或接口自身的繼承體系結構。第一個指的是對於 List(String)和List(Object)這樣的狀況,類型參數String是繼承自Object的。而第二種指的是 List接口繼承自Collection接口。對於這個類型系統,有以下的一些規則:

相同類型參數的泛型類的關係取決於泛型類自身的繼承體系結構。

即List(String)是Collection(String) 的子類型,List(String)能夠替換Collection(String)。這種狀況也適用於帶有上下界的類型聲明。 當泛型類的類型聲明中使用了通配符的時候, 其子類型能夠在兩個維度上分別展開。如對Collection(? extends Number)來講,其子類型能夠在Collection這個維度上展開,即List(? extends Number)和Set(? extends Number)等;也能夠在Number這個層次上展開,即Collection(Double)和 Collection(Integer)等。如此循環下去,ArrayList(Long)和 HashSet(Double)等也都算是Collection(? extends Number)的子類型。 若是泛型類中包含多個類型參數,則對於每一個類型參數分別應用上面的規則。 理解了上面的規則以後,就能夠很容易的修正實例分析中給出的代碼了。只須要把List(Object)改爲List(?)便可。List(String)是List(?)的子類型,所以傳遞參數時不會發生錯誤。

泛型的命名規範

爲了更好地去理解泛型,咱們也須要去理解java泛型的命名規範。爲了與java關鍵字區別開來,java泛型參數只是使用一個大寫字母來定義。各類經常使用泛型參數的意義以下: E — Element,經常使用在java Collection裏,如:List(E),Iterator(E),Set(E) K,V — Key,Value,表明Map的鍵值對 N — Number,數字 T — Type,類型,如String,Integer等等 S,U,V etc. - 2nd, 3rd, 4th 類型,和T的用法同樣 參考: http://www.infoq.com/cn/articles/cf-java-generics https://segmentfault.com/a/1190000002646193 http://peiquan.blog.51cto.com/7518552/1302898

相關文章
相關標籤/搜索