Java泛型

1. 引言

JDK1.5增長泛型支持很大程度上都是爲了讓集合能記住其元素的數據類型。在沒有泛型以前,一旦把一個對象放入Java集合中,集合就會忘記對象的類型,把全部的對象當成Object類型處理。
當程序從集合中取出對象後,就須要進行強制類型轉換,這種強制類型轉換不只使代碼臃腫,並且容易引發ClassCastException異常。
增長了泛型支持後的集合,徹底能夠記住集合中元素的類型,B並能夠在編譯時檢查集合中元素的類型,若是試圖想集合中添加不知足類型要求的對象,編譯器就會提示錯誤。
Java泛型能夠保證若是程序在編譯時沒有發出警告,運行時就不會產生ClassCastException異常。java

從JDK1.5之後,Java引入了「參數化類型(parameterized type)」的概念,容許程序在建立集合時指定集合元素的類型,Java的參數化類型被稱爲泛型(Generic)。
所謂泛型,就是容許在定義類、接口、方法時使用類型形參,這個類型形參將在聲明變量、建立對象、調用方法時動態地指定(即傳入實際的類型參數,也可稱爲類型實參)。數組

2. 泛型類和接口

2.1 定義泛型接口和類

包含泛型聲明的類型能夠在定義變量、建立對象時傳入一個類型實參,從而能夠動態地生成無數多個邏輯上的子類,但這種子類在物理上並不存在。
當建立帶泛型聲明的自定義類,爲該類定義構造器時,構造器名仍是原來的類名,不要增長泛型聲明。
例如,Apple<T>類定義構造器,其構造器名依然是Apple,而不是Apple<T>,調用該構造器時卻可使用Apple<T>的形式,固然應該爲T形參傳入實際的類型參數。app

public interface Generic<E> {

    void add(E e);
    Iterator<E> iterator();
    E next();
}

interface MyMap<K, V> {
    Set<K> keySet();
    V put(K k, V v);
}

2.2 從泛型類派生子類 

當建立了帶泛型聲明的接口、父類以後,能夠爲該接口建立實現類,或從該父類派生子類,須要指出的是,當使用這些接口、父類時不能再包含類型形參。ide

interface ListString<T> extends List<T> {
    @Override
    boolean add(T x);
}

interface ListInteger extends List<Integer> {

}

2.3 並不存在泛型類

無論泛型的實際類型參數是什麼,他們在運行時總有相同的類(class),在內存中也只佔用一塊內存空間,所以在靜態方法、靜態初始化塊或靜態變量的聲明和初始化中不容許使用類型形參。  this

3. 類型通配符

List<String>類並非List<Object>類的子類。
Java泛型設計原則是,只要代碼在編譯時沒有出現警告,就不會遇到運行時ClassCastException異常。設計

3.1 使用類型通配符

爲了表示各類泛型List的父類,可使用類型通配符,類型通配符是一個問號(?)將一個問號做爲類型實參傳給List集合,寫做:List<?>,意思是元素類型未知的List。
這個問號被稱爲通配符,它的元素類型能夠匹配任何類型。
但這種帶通配符的List僅表示它是各類泛型List的父類,並不能把元素加入到其中,好比對象

List<?> list = new ArrayList<Object>();
list.add(new Object());

會報編譯錯誤:The method add(capture#1-of ?) in the type List<capture#1-of ?> is not applicable for the arguments (Object)
由於程序沒法肯定list集合中元素的類型,因此不能向其中添加對象。
根據其我們List<E>接口定義的diamante能夠發現:add方法有類型參數E做爲集合的元素類型,因此傳給add的參數必須是E類的對象或其子類的對象。
但由於在該例中不知道E是什麼類型,因此程序沒法將任何對象放入該集合。惟一的例外是null,它是全部引用類型的實例。
另外一方面,程序能夠調用get方法來返回List<?>集合指定索引處的元素,其返回值是一個未知類型,但能夠確定的是,它老是一個Object。
所以,把get的返回值賦值給一個Object類型的變量,或者放在任何但願是Object類型的地方均可以。blog

3.2 設定類型通配符的上限

當直接使用List<?>這種形式時,即代表這個List集合可使任何泛型List的父類。但還有一種特殊的情形,程序不但願這個List<?>是任何泛型List的父類,只但願它表明某一類泛型List的父類。
List<? extends A>標示全部A泛型List的父類--只要List尖括號裏的類型是A的子類型便可,把A稱爲這個通配符的上限。
相似,因爲程序沒法肯定這個受限制通配符的具體類型,因此不能把A對象或其子類的對象加入這個泛型集合中。繼承

3.3 設定類型通配符下限

Java容許設定通配符的下限:<? super Type>, 這個通配符標示它必須是Type自己,或時Type的父類。
實現將src集合裏面的元素複製到dest集合裏面,而後返回最後一個被複制的元素。索引

public class GenericSuper {
    public static void main(String[] args) {
        List<Number> list1 = new ArrayList<>();
        List<Integer> list2 = new ArrayList<>();
        // copy實際返回的T類型爲Number類型不對
        // Integer i = copy(list1, list2);
        Integer in = copy1(list1, list2);
    }

    public static <T> T copy(Collection<T> dest, Collection<? extends T> src) {
        T last = null;
        for (T ele : src) {
            last = ele;
            dest.add(ele);
        }
        return last;
    }

    public static <T> T copy1(Collection<? super T> dest, Collection<T> src) {
        T last = null;
        for (T ele : src) {
            last = ele;
            dest.add(ele);
        }
        return last;
    }
}

  

3.4 設定類型形參的上限

Java泛型不只容許在使用通配符形參時設定上限,並且能夠在定義類型形參時設定上限,用於表示傳給該類型形參的實際類型要麼是上限類型,要麼是該上限類型的子類。

class A {

}
class Human<T extends A> { // 設置上限
    private Human<A> h = new Human<>();
}

定義了一個Human泛型類,該Human類型的類型形參的上限是A類,這代表使用Human類時爲T形參傳入的實際類型參數只能是A或A類的子類。
在一種極端狀況下,須要爲類型形參設定多個上限(至多有一個父類上限,能夠有多個接口上限),代表該類型形參必須是其父類的子類,而且上限多個上限接口。

class A {

}

interface B {

}

class AB extends A implements B {

}

class Human<T extends A & B> { // 設置上限
    private Human<AB> h = new Human<>();
}

與類同時繼承父類,實現接口相似的是,爲類型形參指定多個上限時,全部的接口上限必須位於類上限以後。
也就是說,若是須要爲類型形參指定類上限,類上限必須位於第一位。

4. 泛型方法

在定義類、接口時可使用類型形參,在該類的方法定義和成員變量定義、接口的方法定義中,這些類型形參可被當成普通類型來用。在另一些狀況下,定義類、接口時沒有使用類型形參,但定義方法時想本身定義而理性形參,這也是能夠的,Java5提供了堆泛型方法的支持。
泛型方法的定義格式以下:
修飾符 <T, S> 返回值類型 方法名(形參列表) {

}
泛型方法方法的方法定義比普通方法的定義多了類型形參聲明,類型形參聲明以尖括號括起來,多個類型形參之間以逗號分隔,全部的類型形參聲明放在方法修飾符和方法返回值類型之間。

class Haha {
    public <T> void smile(T t) {

    }
}

與接口、類聲明中定義的類型形參不一樣的是,方法聲明中定義的形參只能在該方法裏使用,好比Human裏面的S,而接口、類聲明中定義的類型形參則能夠在整個接口、類中使用,好比Human裏面的T。方法中的泛型參數無須顯式傳入實際類型參數。

class Human<T extends A & B> { // 設置上限
    private Human<AB> h = new Human<>();

    public <S> boolean eat(S s) {
        return true;
    }

    public <S> void run(T t, S s) {

    }
}

4.1 泛型方法和類型通配符的區別  

大多數是均可以使用泛型方法來代替類型通配符。
採用類型通配符:

interface Collction1<E> {
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
}

採用泛型方法:

interface Collction2<E> {
    <T> boolean containsAll(Collection<T> c);
    <T extends E> boolean addAll(Collection<T> c);
}

上面方法使用<T extends E>泛型形式,這時定義類型形參時設定上限(其中E是接口定義的類型形參,在接口裏E可當成普通類型使用)
上面兩個方法中類型形參T只使用了一次,類型形參T產生的惟一效果是能夠在不一樣的調用點傳入不一樣的實際類型。對於這種狀況,應該使用通配符:通配符就是被設計用來支持靈活的子類化的。
泛型方法容許類型形參被用來標示方法的一個或多個參數之間的類型依賴關係,或者方法返回值與參數之間的類型依賴關係。若是沒有這樣的類型依賴關係,就不該該使用泛型方法。

若是有須要,也能夠同時使用泛型方法和通配符。

interface Collections1 {
    public <T> void copy(List<T> dest, List<? extends T> src);
    public <T, S extends T> void copy1(List<T> dest, List<S> src);
}

5. Java的「菱形」語法與泛型構造器

Java也容許在構造器中聲明類型形參,這就是所謂的泛型構造器。
一旦定義了泛型構造器,可讓Java根據數據參數的類型來推斷類型形參的類型,也能夠顯式爲構造器中的類型形參指定實際的類型。

public class GenericConstructor {
    public static void main(String[] args) {
        new Foo("sdf");
        new Foo(12);
        new  <String>Foo("ddd");
    }
}

class Foo {
    public <T> Foo(T t) {
        System.out.println(t);
    }
}

菱形語法容許調用構造器時在構造器後使用一對<>表明泛型信息。但若是程序顯式指定了泛型構造器中聲明的而理性形參的實際類型,則不可使用菱形語法。

public class GenericConstructor {
    public static void main(String[] args) {
        new Foo<>("sdf");
        new <Integer>Foo<String>(12);
        // 若是顯式指定泛型構造器中聲明的T形參是String,此時不能使用菱形語法。
        // new  <String>Foo<>("ddd");
    }
}

class Foo<E> {
    public <T> Foo(T t) {
        System.out.println(t);
    }
}

6. 泛型與數組

Java泛型有一個很重要的設計原則:若是一段代碼在編譯時沒有提出「未經檢查的轉換」警告,則程序在運行時不會引起ClassCastException異常。基於這個緣由,因此數組元素的類型不能包含類型變量或類型形參,除非是無上限的類型通配符。但能夠聲明元素類型包含類型變量或類型形參的數組。
也就是說,只能聲明List<String>[]形式的數組,但不能建立ArrayList<String>[10]這樣的數據對象,但能夠new ArrayList<?>[10]

7. 擦除和轉換  

爲了與老的Java老的代碼保持一致,也容許在使用帶泛型聲明的類時不指定實際的類型參數,若是沒有爲這個泛型指定實際的類型參數,則該類型參數被稱爲raw type(原始類型),默認是聲明該類型參數時指定的第一個上限類型。
當吧一個具備泛型信息的對象賦給另外一個沒有泛型信息的變量時,全部在<>之間的類型信息都將被扔掉。好比一個List<String>類型被轉換爲List,則該List對集合元素的類型檢查變成了類型參數的上限(即Object)。

public class GenericSweep {
    public static void main(String[] args) {
        Orange<Integer> or = new Orange<>(6);
        // 當把or賦給一個不帶泛型信息的or1變量時,編譯器就會丟失or對象的泛型信息
        // 由於Orange的類型形參的上限是Number類,因此編譯器依然知道or1的getSize返回Number類型,但具體是Number的哪一個子類就不清楚了。
        Orange or1 = or;
        Number n1 = or1.getSize();
        // or1只知道size的類型是Number
        //Integer i = or1.getSize();
    }

}

class Orange<T extends Number> {
    T size;

    public Orange(T size) {
        this.size = size;
    }

    public T getSize() {
        return size;
    }
}

從邏輯上,List<String>是List的子類,若是直接把一個List對象賦給一個List<String>對象應該引發編譯錯誤,但實際上不會。
對泛型而言,能夠直接把一個List對象賦給一個List<String>對象,編譯器僅僅提示「未經檢查的轉換」。

public class GenericSweep1 {

    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(8);
        li.add(9);
        List list = li;
        List<String> ls = list;
        // 運行時異常
        System.out.println(ls.get(0));

    }
}

定義一個List<Integer>對象,這個List對象保留了集合元素的類型信息。當把這個List對象賦給一個List類型的list後,編譯器就會丟失前者的泛型信息,即丟失list集合裏元素的類型信息,這是典型的擦除。

相關文章
相關標籤/搜索