歡迎光臨
每天分享高質量文章

泛型趣談

(點擊上方公眾號,可快速關註)


來源:四火的嘮叨 ,

www.raychase.net/1932

Java中的泛型帶來了什麼好處?規約。就像接口定義一樣,可以幫助對於泛型型別和物件的使用上,保證型別的正確性。如果沒有泛型的約束,程式員大概需要在代碼裡面使用大量的型別強制轉換陳述句,而且需要非常清楚沒有標註的物件實際型別,這是容易出錯的、惱人的。但是話說回來,泛型可不只有規約,還有很多有趣的用法,容我一一道來。

泛型擦除

Java的泛型在編譯階段實際上就已經被擦除了(這也是它和C#泛型最本質的區別),也就是說,對於使用泛型的定義,對於編譯執行的過程,並沒有任何的幫助(有誰能告訴我為什麼按著泛型擦除來設計?)。所以,單純利用泛型的不同來設計接口,會遇到預期之外的問題,比如說:

public interface Builder {

    public void add(List keyList);

    public void add(List valueList);

}

想這樣設計接口?僅僅靠泛型型別的不同來設計多載接口?那是痴人說夢。但是如果代碼變成這樣呢?

public class GenericTypes {

 

    public static String method(List list) {

        System.out.println(“invoke method(List list)”);

        return “”;

    }

 

    public static int method(List list) {

        System.out.println(“invoke method(List list)”);

        return 1;

    }

 

    public static void main(String[] args) {

        method(new ArrayList());

        method(new ArrayList());

    }

}

這個情況就有點特殊了,Sun的Javac編譯器居然可以通過編譯,而其它不行,這個例子來自IcyFenix的文章,有興趣不妨移步參閱IcyFenix的文章以及下麵的討論

http://icyfenix.iteye.com/blog/1021949

方法泛型

在JDK的java.util.List接口裡面,定義了這樣一個方法:

public interface List extends Collection {

    T[] toArray(T[] a);

}

事實上,這個方法泛型T表示的是任意型別,它可是和此例中的接口/類泛型E毫不相干啊。

如果我去設計方法,我可能寫成這樣:

T[] toArray();

其實這個T[ ] a引數的作用也容易理解:

  1. 確定了陣列型別;

  2. 如果給定的陣列a能夠容納得下結果,就會把結果放進a裡面(JDK的註釋有說明“If the list fits in the specified array, it is returned therein.”),同時也把a傳回。

如果沒有這個T[ ] a引數的話,光光定義一個方法泛型是沒有任何意義的,因為這個T是什麼型別完全是無法預料的,例如:

public class Builder {

    public E call(){

        return null;

    }

 

    public static void main(String[] args) {

        String s = new Builder().call(); // ①

        Integer i = new Builder().call(); // ②

        new Builder().call(); // ③

    }

}

可以看到,call()方法傳回的是型別E,這個E其實沒有任何約束,它可以表示任何物件,但是代碼上不需要強制轉換就可以賦給String型別的物件s(①),也可以賦給Integer的物件i(②),甚至,你可以主動告知編譯器傳回的物件型別(③)。

鏈式呼叫

看看如下示例代碼:

public class Builder {

    public Builder change(S left, E right){

        // 省略實現

    }

 

    public static void main(String[] args) {

        new Builder().change(“3”, 3).change(3, 3.0f).change(3.0f, 3.0d);

    }

}

同樣一個change方法,接收的引數變來變去的,上例中方法引數從String-int變到int-float,再變為float-double,這樣的泛型魔法在設計鏈式呼叫的方法的時候,特別是定義DSL語法的時候特別有用。

使用問號 

其實問號幫助表示的是“通配符型別”,通配符型別 List > 與原始型別 List 和具體型別 List都不相同,List >表示這個list內的每個元素的型別都相同,但是這種型別具體是什麼我們卻不知道。註意,List >和List可不相同,由於Object是最高層的超類,List表示元素可以是任何型別的物件,但是List >可不是這個意思。

來看一段有趣的代碼:

class Wrapper {

    private E e;

    public void put(E e) {

        this.e = e;

    }

 

    public E get(){

        return e;

    }

}

 

public class Builder {

    public void check(Wrapper > wrapper){

        System.out.println(wrapper.get()); // ①

        wrapper.put(new Object()); // ② wrong!

        wrapper.put(wrapper.get()); // ③ wrong!

        wrapper.put(null); // ④ right!

    }

}

Wrapper的類定義裡面指定了它包裝了一個型別為E的物件,但是在另一個使用它的類Builder裡面,指定了Wrapper的泛型引數是?,這就意味著這個被包裝的物件的型別是完全不可知的:

  • 現在我可以呼叫Wrapper的get方法把物件取出來看看(①),

  • 但是我不能放任意型別確定的物件進去,Object也不行(②),

  • 即便是從wrapper裡面get出來也不行(編譯器太不聰明瞭是吧?③)

  • 可奇葩的是,放一個null是可以被允許的,因為null根本就不是任何一個型別的物件(④,註意,不能放int這類的原語型別,雖然它不是物件,但因為它會被自動裝箱成Integer,從而變成具體型別,所以是會失敗的)。

現在思考一下,如果要表示這個未知物件是某個類的子類,上面代碼的Wrapper定義不變,但是check方法寫成:

public void check(Wrapper extends String> wrapper){

    wrapper.put(new String());

}

這樣呢?

……

依然報錯,因為new String()確實是String的子類(或它自己)的物件,一點都沒錯,但是它可不見得和同為String子類(或它自己)的“?”屬於同一個型別啊!

如果寫成這樣呢(註意extends變成了super)?

public void check(Wrapper super String> wrapper){

    wrapper.put(new String());

}

這次對了,為什麼呢?

……

因為wrapper要求put的引數“?”必須是String的父類(或它自己),而不管這個型別如何變化,它一定是new String()的父類(或它自己)啊!

泛型遞迴

啥,泛型還能遞迴?當然能。而且這也是一種好玩的泛型使用:

class Wrapper> implements Comparable> {

 

    @Override

    public int compareTo(Wrapper wrapper) {

        return 0;

    }

 

}

好玩吧?泛型也能遞迴。這個例子指的是,一個物件E由包裝器Wrapper所包裝,但是,E也必須是一個包裝器,這正是包裝器的遞迴;同時,包裝器也實現了一個比較接口,使得兩個包裝器可以互相比較大小。

別暈!泛型只不過是一個普普通通的語言特性,但是也挺有趣的。

【2014-1-9】補充,來自kidneyball的回覆:

為什麼要按著型別擦除來設計。據我所知,Java1.5引入泛型的最大壓力來自於沒有泛型的容器API相比起C++的標準模板庫來太難用,太多不必要的顯式轉型,完全違背了DRY原則也缺乏精細的型別檢查。但Java與C++不同,C++的物件沒有公共父類,不使用泛型根本無法建立一個能存放所有型別的容器,所以必須在費大力氣在編譯後的運行代碼中支持泛型,保留泛型信息自然是順水推舟。而Java所有物件都有一個共同父類Object,當時已有的容器實現已經在運行期表現良好。所以Sun的考慮是加入一層簡單的編譯期泛型語法糖進行自動轉換和型別檢查,而在編譯後的位元組碼中則擦除掉泛型信息,仍然走Object容器的舊路。這種升級方案對jdk的改動是最小的,Runtime根本不用改,改編譯器就行了。

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

赞(0)

分享創造快樂