1. 程式人生 > >java泛型,怎麼這麼難

java泛型,怎麼這麼難

泛型,就是引數化型別。好吧,這是我抄的定義,自己都覺得難以理解。還是舉個簡單例子吧。

public class SimpleJava<T> {
    T t;
    public static void main(String[] args) {
        SimpleJava<Integer> sj = new SimpleJava<>();
        sj.t = Integer.valueOf(1);
    }
}

看見沒,就是型別不確定,使用引數進行表示,那這麼寫有什麼好處呢?我們繼續討論。

為什麼使用泛型?

我們看段程式碼,來感受下泛型的便利。

public class SimpleJava<T> {
    private T t;

    public SimpleJava(T t) {
        this.t = t;
    }
    public T getT() {
        return t;
    }
    public static void main(String[] args) {
        SimpleJava<Integer> sj = new SimpleJava<Integer>(1);
        Integer result = sj.getT();

        SimpleJava<String> sj1 = new
SimpleJava<String>("a"); String result1 = sj1.getT(); } }

看上面程式碼,我們發現T和直接用object型別沒什麼兩樣。仔細看,發現沒,sj.getT()和sj1.getT()的返回值能夠自動進行正確的型別轉換。我們知道,所有的型別轉換都是在執行期進行,如果出錯,會丟擲型別轉換異常:ClassCastException,而使用泛型則不會,我們看下編譯後的彙編:

public static void main(java.lang.String[]);
    Code:
       0: new
#1 // class com/esy/rice/tool/simple/SimpleJava 3: dup 4: iconst_1 5: invokestatic #29 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 8: invokespecial #35 // Method "<init>":(Ljava/lang/Object;)V 11: astore_1 12: aload_1 13: invokevirtual #37 // Method getT:()Ljava/lang/Object; //返回的還是Object 16: checkcast #30 // class java/lang/Integer //編譯器自動插入型別轉換指令 19: astore_2 20: new #1 // class com/esy/rice/tool/simple/SimpleJava 23: dup 24: ldc #39 // String a 26: invokespecial #35 // Method "<init>":(Ljava/lang/Object;)V 29: astore_3 30: aload_3 31: invokevirtual #37 // Method getT:()Ljava/lang/Object;//返回的還是Object 34: checkcast #41 // class java/lang/String //編譯器自動插入型別轉換指令 37: astore 4 39: return } }

我們進行有泛型和沒泛型編碼的對比:

public class SimpleJava {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(1);
        list.add("2");

        List<Integer> list2 = new ArrayList<Integer>();
        list2.add(1);
        list2.add("2"); //編譯期會直接報錯
    }
}

從上面可以看出,這是編譯器對程式碼的優化,幫助進行型別轉換,人會犯錯,而編譯器則不允許(不然誰用)。編譯器既然能夠進行正確的型別轉換,那麼必然的知道正確的型別,機制就是型別宣告時<>中的具體型別資訊。
由此看來,就編譯期型別檢查這一項的便利,就有足夠的理由讓我們使用泛型了。

泛型的不變性

不變性?說出來有些唬人,什麼叫不變呢?來個比較,可能更明瞭,看程式碼:

    public static void main(String[] args) {
        List<Object> list = new ArrayList<Integer>(); //語法錯誤,編譯器報錯,泛型不變
        Object[] arr = new Integer[10]; //正常
        arr[0] = "aa"; //執行期報錯ArrayStoreException
    }

通過程式碼 ,我們可以看見List的構造會報錯,如果不用泛型進行例項化,卻又能正常執行,這是為什麼呢?大家還記得不,前部分講得,泛型的重要特性:編譯期型別檢查,如果上面的list能夠成功例項化,那麼由於List的引數化型別,任何物件都可以加入正確的加入到list中,而實際我們想要的卻是Integer型別,從而破壞泛型的安全機制,所以,不變性換句話說,就是同一個類的不同引數化型別造成了不同的型別,直白點就是,List< A >,List< B >是完全不同的型別。有程式碼可知,陣列是另一種行為,協變性,從程式碼中可以看到,是非常不安全的。
由此,我們有理由認為:非必要的場景(如:效能需要極致等),都不應該使用陣列,而應該用list集合代替。

由於泛型的不變,使的有些情況,變的非常的複雜,舉個例子:

interface A {}
class B implements A {}
public class SimpleJava{
    static void fn(List<A> list) {
        System.out.println(list);
    }
    public static void main(String[] args) {
        List<B> list = new ArrayList<B>();
//      f(list); //直接語法報錯
    }
}

看上面的fn方法,應該是我們程式設計的規範吧,介面與實現分離,利用介面進行程式設計。然而,泛型的不變性,使的list< B >型別,也就是介面的具體實現的泛型無法使用。難道使用泛型後,就只能編寫處理具體類的程式碼了嗎?可想而知,肯定不是,不能對介面進行程式設計,那泛型也太失敗了,如何解決呢?通過限制的萬用字元,看下面程式碼:


interface A {}
class B implements A {}

public class SimpleJava{

    static void fn(List<? extends A> list) {
        for (A a : list) {
        }
    }
    static void fn1(List<? super A> list) {
        list.add(new B());
    }
    public static void main(String[] args) {
        List<B> list = new ArrayList<B>();
        fn(list);
        List<A> list1 = new ArrayList<A>();
        fn1(list1); 
    }
}

我們利用< ? extends A >和< ? super A >進行泛型邊界的限定,進行安全控制同時提升了泛型的靈活性,這到底是什麼原因呢,看點簡單點的程式碼,看下面:

interface A {}
class B implements A {}

public class SimpleJava{

    public static void main(String[] args) {
        List<? extends A> list = new ArrayList<B>(); //合法,萬用字元特性
//      list.add(new B());//非法,因為泛型的不變性<? extends A>並不一定是<B>這種固定的型別
        List<? super A> list2 = new ArrayList<A>(); ////合法,萬用字元特性
        list2.add(new B());//合法,因為繼承關係,B肯定是A型別,
    }
}

現在,我們可以確定,變數的賦值操作,可以萬用字元,尤其是方法呼叫時候的賦值,可以提高編碼的靈活性(不用面向具體的類),可以編寫出更符合面向物件的程式碼。
同時為了保證型別安全,萬用字元有著嚴格的使用限制,例如:List< ? extends A >時,不允許寫入除了null之外的任何型別,因為編譯器並不能知道A的子類具體是哪個,為了型別安全考慮,不允許寫入,使用List< ? super A >時,可以進行寫入A的子類,因為A的子類肯定是A,但是取出時卻會失去型別資訊,編譯器並不知道< ? super A >具體是哪個,唯一可以肯定的是Object。
這也是《Effective Java》中所說的生產者,消費者,有興趣可以自行研讀。

由上面的描述可以看出,java泛型中有著極其嚴格的限制,不過只要知道了,泛型使用的邊界,也就不是太難了。