Java——泛型

前言

通常的類和方法,使用的都是具體的類型:基本類型或者自定義的類。若是咱們要編寫出適用於多種類型的通用代碼,那麼確定就不能使用具體的類型。前面咱們介紹過多態,多態算是一種泛化機制,可是也會拘泥於繼承體系,使得代碼不夠通用。咱們應該是但願編寫更通用的代碼,使代碼能夠應用於「某種不具體的類型」,而不是一個具體的接口或者是類。html

因而Java SE5便引入了「泛型」。泛型實現了參數化類型的概念,使代碼能夠應用於多種類型。泛型出如今編程語言中最初的目的就是但願類或者方法具備更普遍的表達能力。咱們將經過解耦類或者方法所使用的類型類型之間的約束來實現這個目的。java

Java中的泛型機制引入的比較晚,相較與如C++之類的語言產生的一開始便具有泛型的編程語言來講,是比較侷限的。下面將介紹Java中泛型的基本機制、實現原理以及其侷限之處。ios

簡單的泛型類

引入泛型有不少緣由,其中最重要的緣由即是爲了建立容器類。通常持有單個對象的類,能夠明確指定其持有的對象類型。編程

class AppleJuice{}
public class Cup1{
    private AppleJuice aj;
    public Cup1(AppleJuice aj) { this.aj = aj; }
    public AppleJuice get() { return this.aj; }
}

咱們能夠看出Cup1類的可重用性並很差,它只能持有單一的AppleJuice類型,如果想持有OrangeJuice類型對象則須要從新寫一個類。設計模式

在Java SE5以前可讓這個類持有Object類型對象,使得這個類存儲任何類型的對象。由於Object類是全部類的基類,那麼就可使用向上轉型,使用基類引用去指向這些子類對象。數組

class AppleJuice{}
class OrangeJuice{
    public String toString() { return "OrangeJuice"; }
}

public class Cup2 {
    private Object juice;   //使用Object類型引用
    public Cup2(Object juice) { 
        this.juice = juice; 
    }
    public Object get() { return juice; }
    public void set(Object otherJuice) {
        this.juice = otherJuice;
    }
    public static void main(String[] args) {
        Cup2 cup = new Cup2(new AppleJuice());
        cup.set(new OrangeJuice());
        System.out.println((OrangeJuice)cup.get());
    }
}
/*
output:
OrangeJuice
*/

以上即是使用一個Cup2對象存儲前後存儲了兩個不一樣類型的對象。某些狀況下,咱們確實但願容器能持有多種類型的對象。可是,一般而言,咱們只會使用容器來存儲一種類型的對象。泛型的主要目的之一即是:用來指定容器要持有什麼類型的對象,並且由編譯器來保證類型的正確性安全

與其使用Object類型,更偏向於不指定類型,在要使用時再決定使用什麼類型。爲達到這個目的,須要使用類型參數用尖括號括住,放在類名後面,類型參數名沒有要求但通常是大寫單字母T或者是其餘字母(我的認爲多是模仿C++中的模板)。而後在使用這個類的時候,再用實際的類型替換此類型參數。例如:app

public class Cup3 <T>{
    private T juice;
    public Cup3 ( T juice) {
        this.juice = juice;
    }
    public void set(T otherJuice) { juice = otherJuice;}
    public T get() { return juice;}
    public static void main(String[] args) {
        Cup3<AppleJuice> cup = new Cup3<AppleJuice>(new AppleJuice());
        AppleJuice appleJuice = cup.get();  //不須要再向下轉型
//      cup.set(new OrangeJuice()); Error
    }
}

在Cup3對象中能夠存入指定在<>中的類型以及其子類型對象(多態和泛型不衝突)。而且咱們注意到,咱們在取出對象時不用像使用Object時須要強制類型轉換。編程語言

使用泛型自定義堆棧類

在上一篇博客中提到,LinkedList類擁有實現Stack的方法,可使用LinkedList實現一個棧。如今咱們不使用LinkedList,本身來實現鏈式存儲的棧。優化

public class LinkedStack<T> {
    //結點
    private static class Node<U>{
        U item;     //結點數據
        Node<U> next;   //指向下一個結點的引用
        Node() { item = null; next = null;}
        Node(U item, Node<U> next){
            this.item = item;
            this.next = next;
        }
        boolean end() {
            return item==null && next==null;
        }
    }
    
    private Node<T> top = new Node<T>();    //末端哨兵
    //壓棧
    public void push(T item) {
        top = new Node<T>(item, top);
    }
    //出棧
    public T pop() {
        T result = top.item;
        if(!top.end()) {    //若top引用不是指向末端哨兵 則top指向next結點
            top = top.next;
        }
        return result;
    }
    
    public static void main(String[] args) {
        LinkedStack<String> lStack = new LinkedStack<>();//能夠省略後面的<>中的參數 編譯器會依據前面<>中的參數推斷
        //壓棧 壓棧順序爲Happy Day !
        for(String s : "Happy Day !".split(" ")) {
            lStack.push(s);
        }
        String s;
        //出棧 出棧順序爲 ! Day Happy
        while((s=lStack.pop()) != null) {
            System.out.println(s);
        }
    }
}
/*
!
Day
Happy
*/

泛型接口

泛型也能夠應用在接口中。例如生成器(generator),這是一種專門負責建立對象的類。生成器是工廠方法設計模式的一種應用。可是,使用生成器建立對象不須要傳入任何參數,而工廠方法卻須要參數。

通常而言,一個生成器只定義一個方法,該方法用於生成對象。這裏定義next()方法完成此功能。

public interface Generator<T>{ 
    T next();
}

看泛型應用在接口中,與應用在類中並沒有差異。

Generator<T>接口能夠生成Fibonacci數列的生成器實現:

public class FibonacciGenerator implements Generator<Integer>{
    private int count = 0;
    public Integer next() {
        return fib(count++);
    }
    private int fib(int n) {
        if(n < 2) return 1;
        return fib(n-2) + fib(n-1);
    }
    
    public static void main(String[] args) {        
        FibonacciGenerator fGen = new FibonacciGenerator();
        for(int i=0; i<18; i++) {
            System.out.print(fGen.next() + " ");
        }
    }
}
/*
output:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 
*/

咱們的類型參數是Integer,可是咱們使用的數據類型倒是int。是由於Java SE5具有了自動裝箱和拆箱功能,使得基本類型能夠轉換爲相應的包裝器類型。這裏就出現了Java泛型的一個侷限性:基本類型沒法做爲類型參數使用

點擊我查看什麼是裝箱和拆箱

基本類型和它對應的封裝對象之間的相互轉換能夠自動進行
裝箱是指基本類型轉換爲對應的封裝實例,好比int轉換爲java.lang.Integer
拆箱是指封裝實例轉換爲基本類型,好比Byte轉換爲byte

咱們還能夠編寫實現了Iterable的Fibonacci生成器。在實際開發中,如果咱們擁有類源碼則能夠直接重寫這個類,如果沒有源碼控制權,咱們也能夠經過適配器設計模式來實現所須要的接口。

下面將是兩種方式的實現

public class IterableFibonacci1 implements Generator<Integer>, Iterable<Integer>{
    private int count;
    private int n = 0;
    
    public IterableFibonacci1(int count) {
        this.count = count;
    }
    
    public Integer next() {
        return fib(n++);
    }
    
    private int fib(int n) {
        if(n < 2) return 1;
        return fib(n-2) + fib(n-1);
    }

    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            public boolean hasNext() {
                return count > 0;
            }
            public Integer next() {
                count--;
                return IterableFibonacci1.this.next();
            }
        };
    }
    
    public static void main(String[] args) {
        for(Integer i : new FibonacciGenerator(18)) {
            System.out.print(i +" ");
        }
    }
}

使用適配器模式(繼承原有類,在原有類的基礎上增長新的接口,以達到咱們想要完成的功能)

public class IterableGenerator2 extends FibonacciGenerator implements Iterable<Integer>{
    private int n;
    public IterableGenerator2(int count) {
        n = count;
    }
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            public Integer next() {
                n--;
                return IterableGenerator2.this.next();
            }
            public boolean hasNext() {
                return n > 0;
            }
            public void  remove() { //沒有實現
                throw new UnsupportedOperationException();
            }
        };
    }
    
    public static void main(String[] args) {
        for(int i : new IterableGenerator2(18)) {
            System.out.print(i + " ");
        }
    }
}

泛型方法

前面咱們介紹了泛型應用於整個類上,其實泛型還能夠單獨的應用於方法上。泛型方法使得該方法能夠獨立於類而產生變化。如下,是一個基本的指導原則:若是隻使用泛型方法就能夠取代整個泛型類,那麼就只應該使用泛型方法,它顯得更加清楚明瞭。

要定義泛型方法,只需將泛型參數列表置於返回值以前:

public class GenericMethod {
    public <T> void print(T x) {
        System.out.println(x.getClass().getName());
    }
    public static void main(String[] args) {
        GenericMethod gm = new GenericMethod();
        gm.print(12);
        gm.print("123");
        gm.print(12.0);
    }
}
/*
output:
java.lang.Integer
java.lang.String
java.lang.Double
*/

注意,在使用泛型類時,必須在建立對象的同時指定類型參數,可是使用泛型方法的同時卻沒必要指明類型參數,編譯器會幫咱們推斷出具體的類型,這也叫作類型參數推斷(type argument inference)。若是調用gm傳入的參數是基類數據類型,那麼自動裝箱機制就會被啓用。

類型推斷只對賦值操做有效,其餘時候並不起做用。若是將泛型方法的調用結果傳遞給另外一個方法,這時編譯器並不會執行類型參數推斷。

Java泛型的實現原理——擦除

看下面這個程序

public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }
}
/*
output:
true
*/

如果在沒有看見輸出以前咱們確定認爲ArrayList<String>和ArrayList<Integer>是不一樣的類型,可是輸出顯示它們是相同的類型。

看下面的例子,會對這個「奇怪」的現象進行更進一步說明:

class A {}
class B {}
class C <T> {}
class D<P, M>{}

public class LostInformation {
    public static void main(String[] args) {
        List<A> list = new ArrayList<A>();
        Map<A, B> map = new HashMap<A, B>();
        C<B> c = new C<B>();
        D<String, Integer> d = new D<String, Integer>();
        
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(c.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(d.getClass().getTypeParameters()));
    }
}
/*
output:
[E]
[K, V]
[T]
[P, M]
*/

根據JDK文檔的描述

TypeVariable<Class<T>>[] getTypeParameters() 
/*Returns an array of TypeVariable objects that represent the type variables declared by the generic declaration represented by this GenericDeclaration object, in declaration order. */

Class.getTypeParameters()將「返回一個TypeVariable對象數組,表示有泛型聲明所聲明的類型參數」,這個方法好像能夠看出參數類型信息。但是咱們從輸出中只看見了參數佔位符的標識符,沒有具體的類型信息。

事實上,在Java中的泛型代碼內部,咱們沒法獲取任何有關泛型參數類型的信息

咱們能夠知道類型參數標識符和泛型類型邊界(後面介紹什麼是邊界)這類的信息,可是卻取法知道用來建立某個特定實例的實際類型參數。

看了這些奇怪了例子,不由想知道Java的泛型是怎樣實現的。Java的泛型是使用擦除來實現的,這意味着在使用泛型時,任何具體的類型信息都會被擦除(如果沒有定義邊界,則會將類型擦除爲Object類型),而惟一知道的就是本身在使用一個對象。所以,ArrayList<String>和ArrayList<Integer>在運行時事實上是相同的類型。這兩種形式都被擦除成它們的「原生」類型,即ArrayList(或者說是ArrayList<Object>)。

定義擦除的邊界

下面一個使用模板的C++示例

#include <iostream>
using namespace std;

template <class T> class Manipulator {
    T obj;
public:
    Manipulator(T x) { obj = x; }
    void manipulate() { obj.f(); }  //調用了未知類型對象的f()方法
};

class HasF {
public:
    void f() { cout << "HasF::f()" << endl; }
};

int main() {
    HasF hf;
    Manipulator<HasF> manipulator(hf);
    manipulator.manipulate();
}
/*
output:
HasF::f()
*/

以上代碼有一個比較奇怪的地方,maniplate()方法中,在obj上調用f()方法,它怎麼知道參數類型T擁有f()方法呢?原來,在實例化這個模板的時候,C++編譯器將進行檢查,所以在Maniplator<HasF>被實例化的這一刻,它就看到了HasF有一個方法f()。如果沒有,則會獲得一個編譯期錯誤,這樣類型安全就獲得了保證。這也就說明了,C++在模板實例化的時候是知道模板的參數類型的

如果Java來實現這樣的代碼,這樣的代碼是不能編譯的!

因爲有了擦除,Java編譯器沒法將manipulate()必須可以在obj上調用f()這一需求映射到HasF用於f()這一事實上。爲了能夠調用f(),咱們必須協助泛型類,給定泛型類的邊界,以告知編譯器只能接受遵循這個邊界的類型。給定邊界時重用了extend關鍵字。添加了邊界後代碼就能夠運行了。

邊界<T extends HasF>聲明T必須具備類型HasF或者從HasF導出的類型。若是建立對象時符合這個要求,那麼就能夠安全地在obj上調用f()。

泛型類類型參數將擦除到它的第一個邊界(它可能會有多個邊界)。編譯器實際上會將類型參數替換爲它的擦除。上面的例子中,T擦除到了HasF,就好像是在類的聲明中使用了HasF替換了T同樣。

在這個例子中,其實泛型的做用沒有多大咱們其實可使用如下代碼代替以上泛型。

class Manipulator{
    private HasF obj;
    public Manipulator(HasF x) { obj = x; }
    public void manipulate(){ obj.f(); }
}

因此,只有咱們但願代碼跨多個類工做時,使用泛型纔有所幫助。

擦除的緣由——遷移兼容性

泛型類型只有在靜態類型檢查期間纔出現,在此以後,程序中全部泛型類型都將被擦除,替換爲它們的非泛型上界。例如,List<T>將被擦除爲List,普通的類型變量在未指定邊界的狀況下將被擦除爲Object類型。

擦除的核心動機是它可使得泛化的客戶端可使用非泛化的類庫,反之亦然,這常被稱爲「遷移兼容性」。容許非泛型代碼與泛型代碼共存,擦除使得這種向泛型的遷移稱爲可能

邊界處的檢查與轉型

由於有了擦除,在程序運行過程當中,泛型類中的泛型類型將不會有任何意義

public class ArrayMaker<T> {
    private Class<T> kind;
    public ArrayMaker(Class<T> kind) {
        this.kind = kind;
    }
    
    @SuppressWarnings("unchecked")
    T[] create(int size) {
        return (T[]) Array.newInstance(kind, size);
    }
    
    public static void main(String[] args) {
        ArrayMaker<String> stringMaker = new ArrayMaker<>(String.class);
        String[] stringArray = stringMaker.create(10);
        System.out.println(Arrays.toString(stringArray));
    }
}
/*
output:
[null, null, null, null, null, null, null, null, null, null]
*/

Array.newInstance()實際上沒有擁有kind所蘊含的類型信息,所含有的類型信息爲Object,向上例代碼強轉後,也沒有獲得使人滿意的結果。

可是看下面在這個例子,往返回泛型類型對象以前,向其中添加一些信息,會不會獲得使人滿意的結果

public class FilledListMaker<T> {
    
    List<T> create(T t, int n){
        List<T> result = new ArrayList<T>();
        for(int i=0; i<n; i++) {
            result.add(t);
        }
        return result;
    }
    
    public static void main(String[] args) {
        FilledListMaker<String> stringMaker = new FilledListMaker<>();
        List<String> list = stringMaker.create("Hello", 4);
        System.out.println(list);
    }
}
/*
output:
[Hello, Hello, Hello, Hello]
*/

從代碼中咱們能夠看出,即便編譯器沒法知道有關create()中T的任何信息,可是它仍舊能夠在編譯時期確保你放置到result中的對象具備T類型,使其適合ArrayList<T>。所以,即便擦除在方法或類內部移除了有有關實際類型的信息,編譯器仍舊能夠確保方法或類使用的類型的內部一致性。那麼該如何確保呢?

由於擦除在方法體中移除了類型信息,因此在運行時的問題就是邊界:對象進入和離開方法的地點。(此邊界和類型參數的邊界不一樣)這些正是編譯器在編譯期執行類型檢查並插入轉型代碼的地點

擦除的補償——類型標籤

泛型類中建立泛型類型對象不成功

由於擦除會丟失確切信息,因此在運行時須要知道確切類型信息的操做都將沒法完成。可是,咱們能夠引入類型標籤來暫時避免這種問題,對擦除機制進行補償。類型標籤就是能夠用來表示當前類型的對象。咱們能夠在方法中顯示傳遞類型的Class對象,以便在咱們須要使用確切類型機制時使用。

class Building {}
class House extends Building {}

public class ClassTypeCapture<T> {
    Class<T> kind;
    //引入類型標籤 
    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }
    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }
    public static void main(String[] args) {
         ClassTypeCapture<Building> ctc = new ClassTypeCapture<>(Building.class);
         System.out.println(ctc.f(new Building()));
         System.out.println(ctc.f(new House()));
         
         ClassTypeCapture<House> ctc2 = new ClassTypeCapture<>(House.class);
         System.out.println(ctc2.f(new Building()));
         System.out.println(ctc2.f(new House()));
    }
}
/*
output:
true
true
false
true
*/

咱們引入類型標籤(即傳入Class對象)後,即可以使用動態的isInstance()方法。咱們須要注意,編譯器會確保類型標籤能夠匹配泛型參數

建立泛型類型實例

在Erased.java中出現以下錯誤:

T var = new T();    // Cannot instantiate the type T

部分緣由是由於擦除,而另外一部分緣由是由於編譯器不能驗證T具備默認無參構造器Java中想要在泛型類中建立類的實例的解決方案即是傳入一個工廠對象,並使用它來建立實例最便利的工廠對象就是Class對象,所以使用Class對象做爲類型標籤傳入,那麼就可使用newInstance()來建立這個類型的對象。

class ClassAsFactory<T>{
    T x;
    public ClassAsFactory(Class<T> kind) {
        try {
            x = kind.newInstance();     //建立T類型的實例
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

class Employee{}

public class InstantiateGenericType {
    public static void main(String[] args) {
        
        ClassAsFactory<Employee> fe = new ClassAsFactory<>(Employee.class);
        System.out.println("ClassAsFactory<Employee> succeeded.");
        
        try {
            ClassAsFactory<Integer> fi = new ClassAsFactory<>(Integer.class);
        } catch (Exception e) {
            System.out.println("ClassAsFactory<Integer> failed.");
        }
    }
}
/*
output:
ClassAsFactory<Employee> succeeded.
ClassAsFactory<Integer> failed.
*/

代碼能夠編譯,可是建立Integer的實例卻會失敗,是由於Integer沒有任何默認的構造器。這個錯誤不會再編譯時發現,而是在運行時捕獲。因此Sun的工程師們建議使用顯示的工廠,並限制其類型,只能接受實現了這個工廠的類

//工廠接口
interface Factory<T>{
    T create();
}
//Integer工廠
class IntegerFactory implements Factory<Integer>{
    public Integer create() {
        return new Integer(7);
    }
}

class OtherClass{
    //靜態內部類工廠建立外部類對象
    public static class FactoryOther implements Factory<OtherClass>{
        public OtherClass create() {
            return new OtherClass();
        }
        
    }
}
//生成泛型類型對象
class GenericFactory<T> {
    T x;
    public <F extends Factory<T>> GenericFactory(F factory) {
        x = factory.create();
    }
    //....
}

public class FactoryConstraint {
    public static void main(String[] args) {
        new GenericFactory(new IntegerFactory());
        new GenericFactory(new OtherClass.FactoryOther());
    }
}

傳入顯示工廠的方法只是傳入Class<T>的一種變體。實際上,兩種方法都傳遞了工廠對象,Class<T>碰巧是內建的工廠。顯示的工廠對象可使咱們得到編譯時期的檢查。

還有一種建立泛型類型對象的方法即是模板設計模式。下面示例中,create()是模板方法,create()在子類中定義,用來產生子類類型的對象。

abstract class GenericWithCreate<T>{
    final T element;
    public GenericWithCreate() {
        element = create();
    }
    abstract T create();
}

class Tree{}

class Creator extends GenericWithCreate<Tree>{
    Tree create() {
        return new Tree();
    }
    
    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}

public class CreatorGeneric {
    public static void main(String[] args) {
        Creator creator = new Creator();
        creator.f();
    }
}
/*
output:
Tree
*/

建立泛型數組

正如在Erased.java中看見的不能夠建立泛型數組

T[] array1 = new T[SIZE]; // Cannot create a generic array of T

通常的解決方案是在任何想要建立泛型數組的地方都使用ArrayList:

public class ListOfGenerics <T> {
    private List<T> array = new ArrayList<T>();
    public void add(T item){ array.add(item);}
    public T get(int index) { return array.get(index);}
}

如果真想建立一個泛型數組,那麼惟一的方式就是建立一個被擦除類型的新數組,而後對其轉型。

public class GenericArray <T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArray(int size) {
//      array = new T[size];    //Cannot create a generic array of T
        array = (T[]) new Object[size];
    }
    
    public void put(int index, T item) {
        array[index] = item;
    }
    
    public T get(int index) {
        System.out.println("array[index].getClass().getSimpleName() = " + array[index].getClass().getSimpleName());
        return array[index];
    }
    
    public T[] rep() {
        System.out.println("array.getClass().getSimpleName() = " + array.getClass().getSimpleName());
        return array;
    }
    
    public static void main(String[] args) {
        GenericArray<Integer> ga = new GenericArray<>(10);
        
//      java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
//      Integer[] ia = (Integer[])ga.rep();
        
        ga.put(0, 1);
        ga.get(0);
        Object[] oa = ga.rep();
    }
}
/*
output:
array[index].getClass().getSimpleName() = Integer
array.getClass().getSimpleName() = Object[]
*/

咱們不能建立array = new T[size];,因此咱們建立了一個對象數組,並將其轉型。rep()方法返回的是T[],那麼在main()中,按理說會返回Integer[],可是卻出現ClassCastException,這隻能說明程序實際運行時,數組的類型爲Object

由於有了擦除,數組運行時的類型就只能是Object。若是咱們當即將其轉型爲T[],那麼在編譯期該數組的類型就會丟失,而編譯器可能會錯過某些潛在的錯誤檢查。正是由於這樣,最好在集合內部就使用Object[],而後使用數組元素時再添加一個對T的轉型。

public class GenericArray2<T> {
    private Object[] array;
    
    public GenericArray2(int size) {
        array = new Object[size];
    }
    
    public void put(int index, T item) {
        array[index] = item;
    }
    
    @SuppressWarnings("unchecked")
    public T get(int index) {
        System.out.println("array[index].getClass().getSimpleName() = "+ array[index].getClass().getSimpleName());
        return (T) array[index];
    }
    
    @SuppressWarnings("unchecked")
    public T[] rep() {
        System.out.println("array.getClass().getSimpleName() = "+ array.getClass().getSimpleName());
        return (T[]) array;
    }
    
    public static void main(String[] args) {
        GenericArray2<Integer> ga2 = new GenericArray2<>(5);
        ga2.put(0, 7);
        ga2.get(0);
        try {
            Integer[] ia = ga2.rep();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}
/*
array[index].getClass().getSimpleName() = Integer
array.getClass().getSimpleName() = Object[]
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
*/

在調用rep()時,嘗試將Object[]轉換爲T[],是不正確的。所以,沒有任何方式推翻底層的數組類型,它只能是Object[]。將內部數組類型做爲Object而不是T[],是使咱們能夠隨時記着泛型類中數組運行時的類型爲Object。

其實,咱們真正要建立泛型數組,應該要想建立泛型類對象同樣,傳入一個類型參數(類型標記)

public class GenericArrayWithTypeToken <T>{
    private T[] array;
    
    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int size) {
        array = (T[]) Array.newInstance(type, size);
    }
    
    public void put(int index, T item) {
        array[index] = item;
    }
    
    public T get(int index) {
        return array[index];
    }
    
    public T[] rep() {
        System.out.println("array.getClass().getSimpleName() = "+ array.getClass().getSimpleName());
        return array;
    }
    
    public static void main(String[] args) {
        GenericArrayWithTypeToken<Integer> ga = new GenericArrayWithTypeToken<>(Integer.class, 7);
        Integer[] ia = ga.rep();
    }
}
/*
output:
array.getClass().getSimpleName() = Integer[]
*/

類型標記Class<T>被傳入到構造器中,以便從擦除中恢復,使得咱們能夠建立須要的實際類型的數組。

邊界

咱們在「定義擦除的邊界」那兒提到了邊界。邊界使得咱們能夠在泛型的類型參數上設置限制條件:能夠強制泛型能夠應用的類型,以及能夠按照本身的邊界來調用方法。   

由於擦除機制移除了類型信息,因此如果沒有給類型參數指定邊界,那麼調用的方法就只能是Object的方法。如果將類型參數限制爲某個類的子集,那麼咱們就用這些子集來調用這個類的方法。爲了執行這種限制,Java泛型重用了extends關鍵字(須要注意與繼承關係中的含義區分)。

下面示例展現了邊界的基本要素:

import java.awt.Color;

interface HasColor {
    java.awt.Color getColor();
}

class Colored <T extends HasColor>{
    T item;
    public Colored( T item) { this.item = item; }
    T getItem() {return item;}
    //有了邊界 容許調用getColor()方法
    Color color(){ return item.getColor();}
}

class Dimension { public int x, y, z;}

//多邊界 類要放在第一個 接口放在後面
//class ColoredDimension<T extends HasColor & Dimension> 
class ColoredDimension <T extends Dimension & HasColor>{
    //...
}

interface Wight{int wight();}

//擁有多個邊界的泛型類  多邊界只能有一個具體類  可是能夠有多個接口
class Solid <T extends Dimension & HasColor & Wight>{
    T item;
    public Solid(T item) { this.item = item; }
    T getItem() { return item;}
    Color color(){ return item.getColor();}
    int getX() {return item.x; }
    int getY() {return item.y; }
    int getZ() {return item.z; }
    int weight() {return item.wight(); }
}

class Bounded extends Dimension implements HasColor, Wight{
    public int wight() { return 0; }
    public Color getColor() { return null;}
}

public class BasicBounds {
    public static void main(String[] args) {
        Solid<Bounded> solid = new Solid<>(new Bounded());
        solid.color();
        solid.getX();
        solid.weight();
    }
}
  • 泛型類的類型參數被限制爲多邊界時,具體類要放在第一個,接口放在後面
  • 多邊界時,具體類只能有一個,能夠有多個接口

通配符

在介紹通配符以前,咱們先舉一個關於數組的特殊例子:使用基類的引用指向子類的對象,將致使一些問題

class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}

public class CovriantArrays {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple();
        fruit[1] = new Jonathan();
        
        try {
            fruit[2] = new Fruit(); 
        }catch (Exception e) {
            System.out.println(e);
        }
        
        try {
            fruit[3] = new Orange();
        } catch (Exception e) {
            System.out.println(e);
        }
        
        System.out.println(fruit.getClass().getSimpleName());
    }
}
/*
java.lang.ArrayStoreException: blogTest.Fruit
java.lang.ArrayStoreException: blogTest.Orange
Apple[]
*/

咱們將Apple數組賦值給Fruit數組,是由於Apple也是一種Fruit。咱們將Fruit放到Fruit數組中,這是被編譯器容許的,由於引用類型就是Fruit。向Fruit中添加Orange也是被容許的,由於Orange也是一種Fruit。雖然在編譯時期,這種賦值是被容許的,可是在運行時期卻拋出了異常。緣由是由於,運行時期數組機制知道它處理的是Apple[],添加除Apple以及Apple子類以外的對象都是不容許的數組對象能夠保留它們包含的對象類型的規則

對數組的這種賦值,將在運行時期才能夠看出錯誤。可是泛型的主要目標之一就是將這種錯誤檢查移入到編譯期!當咱們使用泛型容器代替以上數組時:

編譯時的報錯信息爲:不能將一個Apple容器賦值給一個Fruit容器。可是更準確的說法是:不能將一個涉及Apple的泛型賦值給一個涉及Fruit的泛型。

咱們討論的是容器的類型,不是容器持有的類型,因此Apple的List不是Fruit的List。與數組不一樣,泛型沒有內建的協變類型。數組中Apple能夠賦值給Fruit,是由於編譯器知道Apple是Fruit的協變類型,所以能夠向上轉型。泛型中,如果想在兩個類之間創建相似這種向上轉型的關係,就須要使用通配符(即類型參數中的?)。

List<? extends Fruit>能夠理解爲:具備任何從Fruit繼承的類型的列表。這樣的List持有的類型將是不穩定的,編譯器沒法確保能夠安全地向其中添加對象。

返回一個Fruit則是安全的,由於列表中存的就是Fruit或者其子類。

查看List的實現源碼,咱們能夠發現add()的參數會變成? extends Fruit,所以編譯器不能知道須要Fruit的哪一個子類型,所以它不會接受任何的Fruit。編譯器將直接拒絕對參數列表中涉及通配符的方法的調用(例如add())

超類型通配符

如果咱們想向基類型列表中寫入子類型,完成上述add()方法的功能,那麼咱們可使用超類型通配符。這裏,能夠聲明通配符是由某個特定類的任何基類來界定的,方法是指定<? super MyClass> 或者使用類型參數<? super T>(可是不能對泛型類型參數給出一個超類型邊界,即不能聲明<T super MyClass>)。這樣,咱們即可以安全地傳遞一個類型對象到泛型類型中。所以,有了超類型通配符,咱們能夠作以下插入:

public class SuperTypeWildcards{
    static void writeTo(List<? super Apple> apples){
        apples.add(new Apple());
        apples.add(new Jonathan());
        // apples.add(new Fruit()); //Error
    }
}

咱們能夠向apples中添加Apple或者Apple的子類型是安全的。

超類型邊界放鬆了在能夠向方法傳遞參數上所作的限制

public class GenericWriting {
    
    static List<Apple> apples = new ArrayList<Apple>();
    static List<Fruit> fruits = new ArrayList<Fruit>();
    
    static <T> void writeExact(List<T> list, T item) {
        System.out.println(item.getClass().getSimpleName());
        list.add(item);
    }
    
    //在「精確」類型下 也能夠向fruit中添加對象
    static void f1() {
        writeExact(fruits, new Fruit());
        writeExact(fruits, new Apple());
        writeExact(fruits, new Orange());
//      writeExact(fruits, new Object()); //Error
    }
    
    static <T> void writeWithWildcard(List<? super T> list, T item) {
        System.out.println(item.getClass().getSimpleName());
        list.add(item);
    }
    
    static void f2() {
        writeWithWildcard(fruits, new Fruit());
        writeWithWildcard(fruits, new Apple());
        writeWithWildcard(fruits, new Orange());
//      writeWithWildcard(fruits, new Object()); //Error
    }
    
    public static void main(String[] args) {
        f1();
        System.out.println("--------------");
        f2();
    }
}
/*
output:
Fruit
Apple
Orange
--------------
Fruit
Apple
Orange
*/

Java編寫思想中

writeExact(fruits, new Apple());或者 writeExact(fruits, new Orange());

在「精確」類型中是不能夠向列表中添加的,然而在JDK 1.8上運行,確實是能夠添加的。看來是作了優化?仍是我理解理解錯了?這裏暫時有點迷惑。望各位看官解答。

咱們在作一個相同的聯繫,對協變和通配符作一個複習,也與超類型通配符比較下:

public class GenericReading {
    
    //Arrays.asList()生成大小不可變的列表
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruits = Arrays.asList(new Fruit());
    
    //使用「精確」的泛型
    static class Reader<T> {
        T readExact(List<T> list) {
            System.out.println(list.get(0).getClass().getSimpleName());
            return list.get(0);
        }
    }
    
    static void f1() {
        Reader<Fruit> fruitReader = new Reader<Fruit>();
        Fruit f = fruitReader.readExact(fruits);
//      Fruit a = fruitReader.readExact(apples);   //Error
    }
    
    //協變
    static class CovariantReader<T> {
        //能夠接受T類型或者是T導出的類型
        T readCovariant(List<? extends T> list) {
            System.out.println(list.get(0).getClass().getSimpleName());
            return list.get(0);
        }
    }
    
    static void f2() {
        CovariantReader<Fruit> fReader = new CovariantReader<>();
        Fruit f = fReader.readCovariant(fruits);
        Fruit a = fReader.readCovariant(apples);
    }
    
    public static void main(String[] args) {
        f1();
        System.out.println("---");
        f2();
    }
}
/*
output:
Fruit
---
Fruit
Apple
*/

我的的理解,<? extends T> 經常使用於一個泛型類型中「讀取」(從一個方法返回);<? super T> 經常使用於向一個泛型類型「寫入」(傳遞給一個方法)。

無界通配符

無界通配符<?>表示具備某種特定類型,不過暫時還未知,與Object類型仍是有區別的。

自限定類型

在Java泛型中,有這樣一個奇怪的慣用法:

class SelfBounded<T extends SelfBounded<T>>

SelfBounded將接受一個泛型參數T,這個T由一個邊界限定,而這個邊界就是擁有T做爲其參數的SelfBounded。這樣的使用方法一眼看去有點難以理解,咱們看看下面的解釋,就會理解這種用法的效果了。

古怪的循環泛型

爲了理解自限定類型的含義,咱們先從這個慣用法的一個簡單版本入手,它沒有包含自限定的邊界(即 不包含extends SelfBounded<T>這句代碼)。

咱們不能直接繼承一個帶有類型參數的泛型類,可是咱們卻被容許繼承 將本身的類做爲類型參數傳給泛型類的這種狀況
即:

class GenericType<T>{}
public class CuriouslyRecurringGeneric extends GenericType<CuriouslyRecurringGeneric>{}

咱們稱這個爲古怪的循環泛型(CRG)來源C++中古怪的循環模板模式的命名方式。「古怪的循環」指的是當前類出如今基礎的基類中。

那麼這個泛型基類有什麼做用呢?

咱們能夠產生 使用導出類做爲泛型基類參數和泛型基類方法返回類型的 基類,還能夠將導出類型做爲基類的域類型,甚至那些將被擦除爲Object的類型。 下面舉例說明:

class BasicHolder<T>{
    T element;
    void set(T arg) {element = arg;}
    T get() {return element;}
    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}

class SubType extends BasicHolder<SubType>{}

public class CRGExample {
    public static void main(String[] args) {
        SubType t1 = new SubType();
        SubType t2 = new SubType();
        
        t1.set(t2);
        SubType t3 = t1.get();
        t1.f();
    }
}
/*
output:
SubType
*/

咱們須要注意:新類SubType接受的參數和返回的值具備SubType類型,而不只僅是基類BasicHolder類型。因此CRG的核心在於:基類用導出類代替其參數。能夠說泛型基類變成了一種其全部導出類的公共功能的模板,可是這些功能的全部參數和返回值將使用導出類型。

可是BasicHolder可使用任何類型做爲其泛型參數,咱們即將要介紹的自限定則能夠強制將正在定義的類做爲本身的邊界參數使用

自限定

class SelfBounded<T extends SelfBounded<T>>{
    T element;
    public SelfBounded<T> set(T arg) {
        element = arg;
        return this;
    }
    
    public T get() {
        return element;
    }
}

class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{}

class C extends SelfBounded<C>{
    C setAndGet(C arg) {
        set(arg);
        return get();
    }
}
//The type D is not a valid substitute for the bounded parameter <T extends SelfBounded<T>> of the type SelfBounded<T>
class D{}
//class E extends SelfBounded<D>{}

class F extends SelfBounded{}

public class SelfBounding {
    public static void main(String[] args) {
        A a = new A();
        a.set(new A());
        a = a.set(new A()).get();
        a = a.get();
        C c = new C();
        c = c.setAndGet(new C());
    }
}

自限定要求的就是在繼承關係中,像下面這樣使用這個類

class A extends SelfBouned<A>

那麼咱們又想知道自限定的參數有什麼做用呢?

它能夠保證參數類型必須與正在被定義的類相同!

咱們從代碼中能夠看出雖然可使用,B雖然能夠繼承從SelfBounded導出的A,可是B類中的類型參數都是爲A類。A類的那種繼承爲最經常使用的用法。對E類進行定義說明不能使用不是SelfBounded的類型參數。F能夠編譯,不會有任何警告,說明自限定慣用法不是可強制執行的。

自限定類型只能強制做用於繼承關係,若是使用了自限定,就應該瞭解這個類的全部類型參數將與使用這個參數的類具備相同類型。即類型參數與類具備相同類型。

還能夠將自限定用於泛型方法:

public class SelfBoundingMethods{
    static <T extends SelfBounded<T>> T f(T arg){
        return arg.set(arg).get();
    }
    public static void main(String args[]){
        A a = f(new A());
    }
}

這能夠放置這個方法被應用於除以上形式的自限定類型參數以外的任何事物上。

參數協變

自限定類型的價值在於:能夠產生協變參數類型(方法參數類型會隨着子類而變化)

class GenericGetter<T extends GenericGetter<T>>{
    T element;
    void set(T element) { this.element = element; }
    T get() { return element; }
}

class Getter extends GenericGetter<Getter>{
}

public class GenericAndReturnTypes {
    static void test(Getter g) {
        Getter result = g.get();
        GenericGetter genericGetter = g.get();
    }
    
    public static void main(String[] args) {
        Getter getter = new Getter();
        test(getter);
    }
}

可是在非泛型代碼中,參數類型卻不能夠隨子類變化而變化。

class Base{}
class Derived extends Base{}

class OrdinarySetter{
    void set(Base base) {
        System.out.println("OrdinarySetter.set(Base)");
    }
}

class DerivedSetter extends OrdinarySetter{
    void set(Derived derived) {
        System.out.println("DerivedSetter.set(Derived)");
    }
}

public class OrdinaryArguments {
    public static void main(String[] args) {
        Base base = new Base();
        Derived derived = new Derived();
        DerivedSetter ds = new DerivedSetter();
        
        ds.set(base);
        ds.set(derived);
    }
}
/*
output:
OrdinarySetter.set(Base)
DerivedSetter.set(Derived)
*/

ds.set(base);和ds.set(derived);均可以能夠的,是由於DerivedSetter.set()沒有重寫OrdinarySetter.set()中的方法,而是重載了。因而DerivedSetter中含有兩個set方法。ds.set(base);調用的是父類OrdinarySetter的set的。

可是使用自限定類型,在導出類中就只會有一個方法,而且這個方法接受導出類型而不是基類型爲參數!!!

interface SelfBoundSetter<T extends SelfBoundSetter<T>>{
    void set(T arg);
}

interface Setter extends SelfBoundSetter<Setter>{}

public class SelfBoundingAndCovariantAruguments {
    void test(Setter s1, Setter s2, SelfBoundSetter sbs) {
        s1.set(s2);
        //The method set(Setter) in the type SelfBoundSetter<Setter> 
        //is not applicable for the arguments (SelfBoundSetter)
        //s1.set(sbs);
    }
}

如果使用了自限定類型,基類型就不能夠傳入到子類型方法中。

如果不使用自限定類型,而使用普通泛型,則子類中就是重載基類的方法,結果就像在OrdinaryArguments.java中同樣。

能夠看出不使用自限定類型將重載參數,使用自限定類型將只能得到方法的一個版本,它將接受確切的參數類型。

小結

在看過《Java編程思想》對Java泛型的介紹後,總結了以上的內容。再次整體回顧,感受到Java中的泛型仍是有不少不足的。畢竟Java語言也不是一開始就有泛型,並且引入了泛型以後還有兼顧之前的舊代碼。

  • 因而泛型的實現原理就是在運行時將實際類型擦除爲指定的第一個邊界類型(未指定則擦除爲Object),從而應用於多個類型。
  • 擦除的代價也是顯著的,不能用於顯示地引用運行時類型信息的操做之中,例如轉型、instanceof操做和new表達式。
  • 而後介紹了類型擦除的補償,能夠指定類型標籤,讓泛型類知道確切類型
  • 最後又總結了邊界、通配符、自限定類型的含義以及用法
  • 還有一個文中沒有提到可是要知道的:不能不捕獲泛型類型的異常,由於在編譯時和運行時都必需要知道異常的確切類型,泛型類也不能直接或者間接繼承Throwable,阻止定義不能捕獲的泛型異常。

參考:

《Java編程思想》第四版

相關文章
相關標籤/搜索