Java泛型詳解

概念

所謂泛型,通俗地講就是通過佔位符的方式聲明抽象的類型,然後在編譯期告訴編譯器具體傳入的類型。(我們只需要在使用的時候定義好具體的類型)。


demo

定義實體類

public class Customer {

    private String name;
    private String address;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }



    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

(1)jdk1.5之前(沒有泛型) :在jdk1.5之前,如果想要通過通用的模板設置不同類型的參數,常用的方式就是將參數聲明成Object類型,案例代碼如下:

public class Holder1 {
    public Object object;

    public Holder1(Object object) {
        this.object = object;
    }

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    public static void main(String[] args) {
        //set Customer
        Holder1 holder1 = new Holder1(new Customer());
        Customer customer = (Customer) holder1.getObject();

        //set other type
        holder1.setObject("object");
        String str = (String) holder1.getObject();

    }
}

通過上面的代碼可以看見,Holder1類型通過聲明不同類型set可以傳入不同的傳輸,但是在取值的時候需要進行類型的轉換。

(2)jdk1.5後(存在泛型) :當有了泛型之後不需要每次都聲明具體的參數類型,通過泛型的語法,我們只需要在使用的時候聲明好類型就解決了上面的問題,案例代碼如下:

public class Holder2<T> {

    public T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }


    public static void main(String[] args) {
        //set Customer
        Holder2<Customer> holder2 = new Holder2();
        holder2.setT(new Customer());
        //set String
        Holder2<String> holder21 = new Holder2<>();
        holder21.setT("object");
    }
}

通過泛型的聲明,只需要在使用Holder2類的指定好具體的類型編譯器就可以自己識別,並且在get是時候需要在指定返回的類型。


泛型接口

泛型支持接口,具體做法如下:首先定義需要用到的類

public class Fruit {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class Apple extends Fruit {
    public Apple() {

    }
}
public class Pear extends Fruit {
    public Pear() {

    }
}

接下來定義泛型接口

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

實現類

public class AppleGenerator implements Generator<Apple> {
    @Override
    public Apple generate() {
        return new Apple();
    }
}

可以看見Generotor接口定義了泛型,在創建具體實現對象的時候指定好生產的類就可以避免數據的轉換。

語法:當我們使用泛型接口的時候,我們需要將泛型參數放在接口名的後面,並且用尖括號包住。


泛型方法

泛型方法可以獨立於類存在,也就是說不管所在類是不是泛型類,都可以定義泛型方法。使用方式如下:

public class GenericMethod {

    public <T> void generate(T t) {
        System.out.println(t.getClass().getName());
    }


    public static void main(String[] args) {
        GenericMethod method = new GenericMethod();
        method.generate("hello");
        method.generate(1L);
        method.generate(1);
        method.generate(true);
    }

}

語法:當我們使用泛型方法的時候,我們需要將泛型參數放在返回值的前面,並且用尖括號包住。

泛型方法對可變參數的支持

泛型方法中也支持對可變參數的泛型化,使用方式如下:

public class GenericVarargs {

    public static <T> List<T> produceList(T... args) {
        List<T> list = Lists.newArrayList();
        for (T t : args) {
            list.add(t);
        }

        return list;
    }

    public static void main(String[] args) {
        List<Integer> integerList = produceList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        System.out.println(integerList);
    }
}

可以看見,上面的例子定義了帶有可變參數的泛型方法。在使用的時候,我們可以指定可變參數的類型然後生成指定類型的集合。通過這種方式我們可以很容易地創建我們想要的集合。

匿名內部類對泛型的支持

匿名內部類中也可以使用泛型,使用方式和泛型類類似,案例代碼如下:

public class GenericAnonymous {
    public static Generator<Apple> generator() {
        return new Generator<Apple>() {
            @Override
            public Apple generate() {
                return new Apple();
            }
        };
    }
    public static void main(String[] args) {
        Apple apple = GenericAnonymous.generator().generate();
    }
} 

泛型擦除

1. 首先讓我們看一個例子,代碼如下:

public class GenericErasureBootstrap {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();
        System.out.println(stringList.getClass() == integerList.getClass());
    }
}

可能以我們第一感覺會輸出false,但事實上是輸出的true。因爲對於Java來說,List和List是同一個類型,這是Java內部的泛型擦除機制讓這種情況得以發生,接下來我會詳細地講解什麼是泛型擦除。

2. 泛型參數信息:在Java中要想獲取到泛型參數信息可以通過如下的方式:

public static void getGenericType() {
    List<String> list = new ArrayList<>();
    System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
}

輸出的結果是==[E]==,可以看見只會拿到聲明的佔位符的信息,並沒有具體的String類型的信息。這樣就不難理解爲什麼上面聲明的兩個不同泛型類型的集合會被認爲是同一個類型,因爲對Java來說,他們都是List類型。

3. 泛型存在的問題:同樣我們先看一個例子:

public class Computer {
    public void inspect() {
    }
}
public class Calculate<T> {

    private T t;

    public Calculate(T t) {
        this.t = t;
    }

    public void cal() {
        //error Cannot resolve method inspect()
        t.inspect();
    }

    public static void main(String[] args) {
        Calculate<Computer> calculate = new Calculate<>(new Computer());
        calculate.cal();
    }
}

上面的例子編譯就不會通過,在編譯器Java並不知道T到底是什麼類型。(這也可以認識是Java泛型的一種缺陷),解決方式如下:

public class Calculate<T extends Computer> {
    ...
}

通過將泛型參數聲明爲T extends Computer,就可以保證參數類型是Computer的子類,這樣編譯就能夠通過。

4. 爲什麼需要泛型參數

Java這麼做主要是爲了兼容之前的代碼。假設如果有A,B兩個類,這兩個類是之前就已經存在的並且B還引用A。如果沒有泛型擦除,將A改成了泛型類,那麼B引用A必將出現錯誤。因此泛型擦除主要就是解決之前代碼的兼容性問題。

泛型擦除引發問題

(1) 由於存在泛型擦除,因此任何在運行時需要知道具體類型的操作都無法進行。比如instanceof,但是可以通過如下方式解決這個問題

    public void ofInstance(Object object) {
        if (c.isInstance(object)) {
            System.out.println("c instanceof "+object.getClass().getTypeName());
        }
    }

通過isInstance方法可以達到相同的目的。

(2) 如果相同對泛型類型進行實例化,使用new也是會發生錯誤,因此可以通過工廠方法來進行對象的創建

public interface Factory<T> {
    T create();
}
public class IntegerFactory implements Factory<Integer> {
    @Override
    public Integer create() {
        return new Integer(0);
    }
}

也可以通過模板的方式進行創建

public abstract class GeneralFactory<T> {

    final T element;

    public GeneralFactory() {
        element = create();
    }

    public abstract T create();
}
public class Creator extends GeneralFactory<Integer> {
    @Override
    public Integer create() {
        return new Integer(0);
    }
}

邊界

泛型的邊界(通過extends)用來限制泛型的範圍,默認會使Object,如下所示:

public class ScopeBootstrap<T extends Number> {

    private T t;

    public ScopeBootstrap() {

    }

    public ScopeBootstrap(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}
public class SubScopeBootstrap<T> extends ScopeBootstrap {

    private T t;

    public SubScopeBootstrap(T t) {
        this.t = t;
    }

    public T getSub() {
        return t;
    }
}

對於ScopeBootstrap來說,它的參數化類型需要是Number,而子類SubScopeBootstrap繼承了它因此泛型參數也必須是Number類型。


通配符

首先讓我們聲明幾個模型

public class Fruit {
}
public class Apple extends Fruit {
}
public class pear extends Fruit{
}

? extends T*:當我們通過這種方式聲明泛型的時候,代表聲明的泛型類型是T的子類列表(包括T)。具體例子如下

public static void main(String[] args) {
    List<? extends Fruit> list = new ArrayList<Apple>();
    //error
    list.add(new Apple());
    List<Apple> apples = Lists.newArrayList();
    Apple apple = new Apple();
    apples.add(apple);
    list = apples;
    boolean isContain = apples.contains(apple);
    int index = apples.indexOf(apple);
    System.out.println((Apple) list.get(0));
    System.out.println("iscontain = " + isContain);
    System.out.println("index = " + index);
}

運行上面這段代碼,會出現一些情況:

首先聲明瞭一個Apple容器但是用的是List<? extends Fruit>進行接收,這種情況下編譯器是不能判斷容器中具體存放的類型的,因此不能向list中加入任何的元素。
,但是可以通過賦值的形式將一個apples容器指向list因爲list獲取的元素肯定是Fruit這是編譯器可以檢測到的。並且對於list來說contains和indexof的操作都是合法的。因爲源碼中這兩個操作接收的就是Object類型。

? super T 超類型通配符,通過這種方式,傳入泛型的類型下限就是特定類型T,必須是T的超類或者本身纔可以,這在一定程度上放寬了泛型類型。案例如下

public <T> void addFruit(List<? super T> list, T item) {
    list.add(item);
}

List<Apple> appleList = new ArrayList<>();
List<Fruit> fruitList = new ArrayList<>();
addFruit(appleList, new Apple());
addFruit(fruitList, new Fruit());

通過super限定了傳入的容器類型必須是T或者是T的超類,這樣擴大了容器的可傳入類型。

? 無界通配符:某種程度上,它幾乎等同於任何類型。它只是用來聲明在用泛型編寫代碼。常常用來處理比如Map中某一個可能是特定類型,某一個是任何類型。可以使用例如Map<String,?>來聲明。

總結: 對於extends,常常在讀取例如容器中的值時候使用,比如容器中的類型是不確定的,需要通過傳入的參數來確定,那麼可以通過它來進行限定;對於super,如果設置的值類型也是不確定的,比如設置容器的值,但是它的類型需要動態的傳進來,那麼可以通過此方式將泛型類型限定在指定類型的超類範圍之內,這樣就保證了類型的安全。通俗地說extends保證了返回的類型是統一的,super可以保證設置的值是在指定範圍內的。


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