1. 程式人生 > >啃知識系列_泛型和泛型邊界

啃知識系列_泛型和泛型邊界

這兩天看Java程式設計思想,重新學習了一下泛型的知識。 以前很多不懂得地方也梳理了一下。

在沒有使用泛型之前,我們編寫一個類,想要持有其他型別的任何物件。

public class Holder {
    private Object a;
    public Holder(Object a){
        this.a = a;
    }

    public Object get() {
        return a;
    }

    public void set(Object a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Holder h = new Holder(new AutoMobile());
        AutoMobile m = (AutoMobile) h.get();
        h.set("Not AutoMobile");
        String s = (String) h.get();
    }
}

class AutoMobile{}
這個例子,我們先後存了兩種型別的物件,但是當我們從holder中取出物件的時候,需要強制型別轉換才可以。

泛型的主要目的之一就是用來制定容器要持有什麼型別的物件,而且編譯器來保證型別的正確性。

因此,與其我們用Object,更喜歡暫時不指定型別,而是之後決定使用什麼型別,這時候我們需要使用型別引數,下面例子中T就是型別引數。

public class HolderT<T> {
    private T t;
    public HolderT(T t){
        this.t = t;
    }

    public T get() {
        return t;
    }

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

    public static void main(String[] args) {
        HolderT<AutoMobile> h = new HolderT<AutoMobile>(new AutoMobile());
        AutoMobile a = h.get();
//        h.set("Not AutoMobile");  編譯器報錯,因為我們要制定儲存的事AutoMobile型別的物件
        HolderT<String> h2 = new HolderT<String>("Not AutoMobile");
        String s = h2.get();
    }
}
現在當建立HolderT的時候必須指明想要持有的物件,將其置於尖括號中。之後get和set的時候都已經制定了現在持有的物件。

這裡初步講下泛型的基本用法,接下來會寫一些我覺的需要注意的泛型的例子和用法。

現在我們實現一個堆疊類。

public class LinedStack<T> {
    private static class Node<U>{
        U item;
        Node next;
        Node(){
            this.item = null;
            this.next = null;
        }
        Node(U item,Node 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(item,top);
    }

    public T pop(){
        T result = top.item;
        if(!top.end()){
            top = top.next;
        }
        return result;
    }

    public static void main(String[] args) {
        LinedStack<Integer> stack = new LinedStack<Integer>();
        stack.push(1);
        stack.push(2);
        System.out.println(stack.pop());
        System.out.println(stack.pop());
    }

}
這裡的Node也是泛型的,他有自己的型別引數U。

這裡我們使用了末端哨兵來判斷堆疊何時為空。這裡我關注的是一點,如果我不給他的內部類Node設定自己的型別引數,而是跟外部的型別引數一樣。

如果直接去掉U的型別引數,會報錯。這裡就需要先說一說型別擦除的概念。這個稍後再說。先讓我們看看泛型方法。

之前我們看到的泛型都是作用在類上,但他同樣可以作用在方法上。泛型方法使得該方法獨立於類而產生變化。以下是一些基本的指導建議:

無論何時,只要你能做到,就應該只使用泛型方法,因為他使得事情更清楚明白,另外,對於一個static的方法而言,無法訪問泛型類的泛型型別引數,所以,如果static方法需要使用泛型能力,就必須使其成為泛型方法。

要定義泛型方法,只需要將泛型引數列表置於返回型別之前就可以。

public class GenericMehtods {
    public static <T> void f(T t){
        System.out.println(t.getClass().getName());
    }

    public static void main(String[] args) {
        f("");
        f(1);
        f(1.0d);
        f(1.0f);
        f(new GenericMehtods());
    }

}
執行結果:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
com.wyc.generics.GenericMehtods

對於為什麼傳入基本型別會輸出包裝型別的問題,這是由於自動裝箱導致的。這裡先不解釋這個問題。

這裡我們看到,方法f擁有型別引數。注意,當我們使用泛型類時我們必須在建立物件時指定型別引數的值,而使用泛型方法的時候,通常不必指明引數型別,因為編譯器會為我們找出具體的型別,這稱為型別引數推斷。

當我們平時使用容器的時候可能遇到過這種問題。

Map<String,Integer> map = new HashMap<String,Integer>();

我們會編寫多餘制定泛型引數的程式碼。我們可以使用型別引數推斷解決這種問題。(當然,JDK1.7之後已經支援直接去掉後面的指定型別的問題。只需要new HashMap<>()就可以)

public class New {
    public static <K,V> Map<K,V> map(){
        return new HashMap<K, V>();
    }

    public static void main(String[] args) {
        Map<String,Integer> map = new HashMap<String, Integer>();
        map = map();
    }
}

當然,型別引數判斷會讓閱讀程式碼的人必須去分析New方法中的map()所隱含的功能。在泛型方法中,我們也可以顯示的指定型別,雖然這種方式不常用。

public class ExplicitTypeSpecification {

    static void f(Map<String,List<String>> petPeople){}

    public static void main(String[] args) {
        New2 new2 = new New2();
        f(new2.<String, List<String>>map());
    }

}
class New2{
    public <K,V> Map<K,V> map(){
        return new HashMap<K, V>();
    }
    public static <K,V> Map<K,V> map2(){
        return new HashMap<K, V>();
    }
    static void f(Map<String,List<String>> petPeople){}
    public void test(){
        f(this.<String,List<String>>map());
        f(New2.<String, List<String>>map2());
//        f(map());
//        f(map2());
    }
}

這種語法抵消了New類為我們帶來的好處。(既省去大量的型別說明),不過只有在編寫非賦值語句時才需要這樣額外的說明。

接下來我們說一說型別擦除。請考慮下面情況。

public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<Integer>().getClass();
        Class c2 = new ArrayList<String>().getClass();
        System.out.println(c1 == c2);
        System.out.println(c1);
        System.out.println(c2);
    }
}
這種c1和c2很容易被認為是兩種型別,但上面認為兩種型別是相同。下面做下補充。
public class LostInfomation {

    public static void main(String[] args) {
        List<Frob> list = new ArrayList<Frob>();
        Map<Frob,Fnorkle> map = new HashMap<Frob, Fnorkle>();
        Quark<Fnorkle> quark = new Quark<Fnorkle>();
        Particle<Long,Double> particle = new Particle<Long, Double>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
    }
}
class Frob{}
class Fnorkle{}
class Quark<Q>{}
class Particle<POSITION,MOMENTUM>{}
Class.getTypeParameters()將返回TypeVariable物件陣列,表示有泛型宣告所宣告的型別引數,你會發現輸出的都是用作引數佔位符的識別符號,並不是有用的資訊。

在泛型程式碼內部,無法獲得任何有關泛型引數型別的資訊

因此,在你使用Java泛型的時候,這意味著任何具體的型別資訊都被擦除掉了,你唯一知道的就是你在使用的物件。

如果我們想編寫如下程式碼,是不行的。

public class HasF {
    public void f(){
        System.out.println("HasF.f()");
    }

    public static void main(String[] args) {
        HasF hasF = new HasF();
        Mainpulator<HasF> mainpulator = new Mainpulator<HasF>(hasF);
        mainpulator.mainpulate();
    }
}

class Mainpulator<T>{
    private T obj;
    public Mainpulator(T t){
        obj = t;
    }
    public void mainpulate(){
        obj.f(); //編譯器報錯
    }
}

由於有了擦除,編譯器我們是不知道T的型別的,所以也沒有辦法呼叫obj的方法。這個時候我們必須協助泛型類,給定泛型類的邊界,以此告知編譯器只能接受遵循這一邊界的型別。這裡我們重用了extends關鍵字。由於有了邊界,下面程式碼可以編譯。

public class HasF {
    public void f(){
        System.out.println("HasF.f()");
    }

    public static void main(String[] args) {
        HasF hasF = new HasF();
        Mainpulator<HasF> mainpulator = new Mainpulator<HasF>(hasF);
        mainpulator.mainpulate();
    }
}

class Mainpulator<T extends HasF>{
    private T obj;
    public Mainpulator(T t){
        obj = t;
    }
    public void mainpulate(){
        obj.f();
    }
}

邊界<T extends HasF>宣告T必須具有型別HasF或者從HasF匯出的型別。

泛型型別引數將擦除到它的第一個邊界。(他可能有很多邊界,之後說明),像上面的例子,他擦除到HashF,所以就好像類的宣告時候用HasF替換T一樣,可以直接使用T來呼叫相關的HasF的行為。

泛型型別只有在靜態型別檢查期間才出現,在此之後,程式中的所有的泛型型別都將被擦除,替換為他們的非泛型上界。例如,List<T>這樣的型別註解,將被擦除為List,二普通的型別變數在未指定邊界的情況下將被擦除為Object。

擦除的主要正當理由是從非泛化程式碼到泛化程式碼的轉變過程,以及在不破換現有類庫的情況下,將泛型融入Java語言的一種手段。

當我們想建立一個型別例項的時候new T(),我們是無法實現的,部分原因是擦除,另一部分則是因為我們不知道T是否具有預設的構造方法。我們可以通過工廠方法實現。

public class InstantiateGenericType {
    public static void main(String[] args) {
        ClassFactory<Employee> classFactory = new ClassFactory<Employee>(Employee.class);
        System.out.println(classFactory.t);
        ClassFactory<Employee1> classFactory1 = new ClassFactory<Employee1>(Employee1.class); //這裡會導致報錯,因為Employee1沒有預設的構造方法
        System.out.println(classFactory1.t);
    }
}
class Employee{}
class Employee1{
    public Employee1(int i ){

    }
}

class ClassFactory<T>{
    T t;
    public ClassFactory(Class<T> kind){
        try {
            t = kind.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
所以上面這種方式是不可取的,我們最好建立一個顯示的工廠物件。
public class FactoryConstrain {
    public static void main(String[] args) {
        new Foo2<Integer>(new IntegerFacotry());
        new Foo2<Widget>(new Widget.WidgetFacotry());
    }
}

class Foo2<T>{
    private T obj;
    public <F extends Factory<T>> Foo2(F f){
        obj = f.create();
    }
}

interface Factory<T>{
    T create();
}

class IntegerFacotry implements Factory<Integer>{

    @Override
    public Integer create() {
        return new Integer(0);
    }
}

class Widget {
    public static class WidgetFacotry implements Factory<Widget>{
        @Override
        public Widget create() {
            return new Widget();
        }
    }

}

這種方式是一種變體,這種方式能夠讓們在編譯期就檢查我們的物件建立。

還有一種是模板方法的設計模式。

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

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

class X{}

class Creator extends GenericWithCreate<X>{

    @Override
    X create() {
        return new X();
    }

    public void f(){
        System.out.println(element.getClass().getName());
    }
}

泛型陣列,我們一般是不能建立泛型陣列的,一般解決方案是在任何想要建立泛型陣列的地方都是用ArrayList來代替。如果建立泛型陣列我們看到如下情況。

public class GenericArray<T> {
    private T[] array;
    public GenericArray(int sz){
//        array = new T[sz];
        array = (T[]) new Object[sz];
    }
    public void put(int index,T item){
        array[index] = item;
    }
    public T get(int index){
        return array[index];
    }

    public T[] rep(){
        return array;
    }
    public static void main(String[] args) {
        GenericArray<Integer> gai = new GenericArray<Integer>(10);
        gai.put(0,1);
        Integer[] ia = gai.rep();
    }
}
我們需要在構建的時候使用強制型別轉換從Object[]轉換。因為直接使用new T[]我們編譯器是不知道他是什麼型別的,所以無法直接例項化。下面的程式碼執行會報錯。錯誤資訊如下。
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
	at com.wyc.generics.GenericArray.main(GenericArray.java:25)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
說無法從Object陣列轉換到Integer陣列的意思。那麼這裡我理解是,雖然編譯器是ok的,因為我們給T一個指定型別是Integer,但是執行時期,實際上是Object型別的。所以無法直接直接賦值給ia。我們debug模式看下就會發現。


這裡你會看到array是一個Object陣列,裡面存了一個之前我賦值的一個Integer物件。並且看程式碼中我試圖想用

(T[])array
將返回的rep強轉成Integer[].但是因為T在執行期其實是Object看待的話,那麼我們其實最後這句話也是相當於(Object[])array。那麼也不會起作用,這裡大家可能也看到了在
Integer[] ia = (Integer[])gai.rep();
這行程式碼 中我試圖將返回的Object[]陣列強轉成Integer[]陣列。但是最後也會報錯。

這是因為陣列不同型別之間轉換,基類無法向他的子類強轉型別轉換,但是子類可以直接賦值給基類的陣列。

這可能是一個常識性問題,但是我居然一直沒有注意到= =原諒我自己太不用心了。

好了,這裡我主要說明的是,對於編譯期,會給我們一種錯覺,這種程式碼是可以的,因為T的泛型我們已經指定了明確的型別,所以rep是認為是正確的,但是在執行期間,我們的泛型T其實都被當作Object來使用,所以這程式碼會報執行期錯誤。當我們看到泛型的時候,只要將型別擦除看成Object使用就更容易理解了。

但是有一種方式可以讓我們在執行期將T的型別的擦除恢復,如下程式碼。

public class GenericArrayWIthTypeToken<T> {
    private T[] array;
    public GenericArrayWIthTypeToken(Class<T> type,int sz){
        array = (T[]) Array.newInstance(type,sz);
    }
    public void put(int index,T item){
        array[index] = item;
    }
    public T[] req(){
        return array;
    }

    public static void main(String[] args) {
        GenericArrayWIthTypeToken<Integer> gai = new GenericArrayWIthTypeToken<Integer>(Integer.class,10);
        Integer[] a = gai.req();
    }
}
這裡我們看到。建立的時候我們使用Array.newInstance。雖然這個返回型別是Object[]我們需要強轉,但我理解這個強轉就是給編譯器看的,讓他知道編譯期型別的轉換是正確的。 現在程式碼執行是正確的,因為我們已經在執行期恢復了T的型別,通過Class<T> type來指定。

接下來我們詳細的說一下邊界。

看如下程式碼。

interface HasColor{
    Color getColor();
}
class Colored<T extends HasColor>{
    T item;
    Colored(T item){
        this.item = item;
    }
    T getItem(){
        return item;
    }
    //我們會發現這裡我們可以直接呼叫HasColor的方法,因為我們使用邊界這種方式
    //T的執行期已經擦除到第一個邊界,即是HasColor
    Color color(){
        return item.getColor();
    }
}

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

//這裡會編譯不通過,因為 如果extends class和interface的話 , class必須是第一個
//class ColoredDimension<T extends HasColor & Dimension> {}
class ColoredDimension<T extends Dimension & HasColor > {
    T item;
    ColoredDimension(T item){
        this.item = item;
    }
    Color color(){
        return item.getColor();
    }
    int getX(){
        return item.x;
    }
    int getY(){
        return item.y;
    }
    int getZ(){
        return item.z;
    }
}

interface Weight{
    int weight();
}
//邊界裡只能有一個類,多個介面
class Solid<T extends Dimension & HasColor & Weight>{
    T item;
    Solid(T item){
        this.item = 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.weight();
    }
}

class Bounded extends Dimension implements HasColor , Weight{

    @Override
    public Color getColor() {
        return null;
    }

    @Override
    public int weight() {
        return 0;
    }
}
public class BasicBounds {
    public static void main(String[] args) {
        Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
        solid.color();
        solid.getX();
        solid.weight();
    }
}
這段程式碼可能有些長,但是希望大家能打一遍,有一個更好的理解。

這裡對於T extends Dimension & HasColor 可能會有些問題,為什麼裡面可以直接使用 item.color()到底最後執行時擦除成什麼型別。這裡我通過javap -c 反編譯看下。


看構造方法發現他確實擦除成第一個邊界,即是Dimension。在我們的color()方法中我們發現,他getfield的是Dimension但是會有一個checkcast,即是判斷下他是否是HasColor型別的,然後在直接呼叫HashColor.getColor。

所以這就是我們為何也能呼叫其他邊界方法的原因。

說到了這裡我對於泛型和邊界的理解是。

泛型,編譯期是帶著型別的,為了編譯器判斷我們寫的是否有問題,但是執行期間,我們的型別就被擦除掉了,而統一被看成了Object的,如果我們有了邊界,那麼我們的這個泛型型別就會被看成是第一個邊界的型別,如果有其他邊界,他也會有他們的行為。

由於這個泛型還有很大一部分沒有說,篇幅太長,分成兩部分來寫。,總結下的我們主要說了幾點,

1. 泛型會在執行期將型別擦除成為Object。

2.邊界可以讓我們將泛型的型別引數擦除為第一個邊界。

3.不能直接建立泛型陣列。