本篇文章給大家帶來的內容是關於java中萬用字元的詳細分析(代碼),有一定的參考價值,有需要的朋友可以參考一下,希望對你有所協助。
在本文的前面的部分裡已經說過了泛型型別的子類型的不相關性。但有些時候,我們希望能夠像使用普通類型那樣使用泛型型別:
◆ 向上造型一個泛型對象的引用
◆ 向下造型一個泛型對象的引用
向上造型一個泛型對象的引用
例如,假設我們有很多箱子,每個箱子裡都裝有不同的水果,我們需要找到一種方法能夠通用的處理任何一箱水果。更通俗的說法,A是B的子類型,我們需要找到一種方法能夠將C<A>類型的執行個體賦給一個C<B>類型的聲明。
為了完成這種操作,我們需要使用帶有萬用字元的擴充聲明,就像下面的例子裡那樣:
List<Apple> apples = new ArrayList<Apple>();List<? extends Fruit> fruits = apples;
“? extends”是泛型型別的子類型相關性成為現實:Apple是Fruit的子類型,List<Apple> 是 List<? extends Fruit> 的子類型。
向下造型一個泛型對象的引用
現在我來介紹另外一種萬用字元:? super。如果類型B是類型A的超類型(父類型),那麼C<B> 是 C<? super A> 的子類型:
List<Fruit> fruits = new ArrayList<Fruit>();List<? super Apple> = fruits;
為什麼使用萬用字元標記能行得通?
原理現在已經很明白:我們如何利用這種新的文法結構?
? extends
讓我們重新看看這第二部分使用的一個例子,其中談到了Java數組的子類型相關性:
Apple[] apples = new Apple[ 1 ];Fruit[] fruits = apples;fruits[ 0 ] = new Strawberry();
就像我們看到的,當你往一個聲明為Fruit數組的Apple對象數組裡加入Strawberry對象後,代碼可以編譯,但在運行時拋出異常。
現在我們可以使用萬用字元把相關的代碼轉換成泛型:因為Apple是Fruit的一個子類,我們使用? extends 萬用字元,這樣就能將一個List<Apple>對象的定義賦到一個List<? extends Fruit>的聲明上:
List<Apple> apples = new ArrayList<Apple>();List<? extends Fruit> fruits = apples;fruits.add( new Strawberry());
這次,代碼就編譯不過去了!Java編譯器會阻止你往一個Fruit list裡加入strawberry。在編譯時間我們就能檢測到錯誤,在運行時就不需要進行檢查來確保往列表裡加入不相容的類型了。即使你往list裡加入Fruit對象也不行:
fruits.add( new Fruit());
你沒有辦法做到這些。事實上你不能夠往一個使用了? extends的資料結構裡寫入任何的值。
原因非常的簡單,你可以這樣想:這個? extends T 萬用字元告訴編譯器我們在處理一個類型T的子類型,但我們不知道這個子類型究竟是什麼。因為沒法確定,為了保證型別安全,我們就不允許往裡面加入任何這種類型的資料。另一方面,因為我們知道,不論它是什麼類型,它總是類型T的子類型,當我們在讀取資料時,能確保得到的資料是一個T類型的執行個體:
Fruit get = fruits.get( 0 );
? super
使用 ? super 萬用字元一般是什麼情況?讓我們先看看這個:
List<Fruit> fruits = new ArrayList<Fruit>();List<? super Apple> = fruits;
我們看到fruits指向的是一個裝有Apple的某種超類(supertype)的List。同樣的,我們不知道究竟是什麼超類,但我們知道 Apple和任何Apple的子類都跟它的類型相容。既然這個未知的類型即是Apple,也是GreenApple的超類,我們就可以寫入:
fruits.add( new Apple());fruits.add( new GreenApple());
如果我們想往裡面加入Apple的超類,編譯器就會警告你:
fruits.add( new Fruit());fruits.add( new Object());
因為我們不知道它是怎樣的超類,所有這樣的執行個體就不允許加入。
從這種形式的類型裡擷取資料又是怎麼樣的呢?結果表明,你只能取出Object執行個體:因為我們不知道超類究竟是什麼,編譯器唯一能保證的只是它是個Object,因為Object是任何Java類型的超類。
存取原則和PECS法則
總結 ? extends 和 the ? super 萬用字元的特徵,我們可以得出以下結論:
◆ 如果你想從一個資料類型裡擷取資料,使用 ? extends 萬用字元
◆ 如果你想把對象寫入一個資料結構裡,使用 ? super 萬用字元
◆ 如果你既想存,又想取,那就別用萬用字元。
這就是Maurice Naftalin在他的《Java Generics and Collections》這本書中所說的存取原則,以及Joshua Bloch在他的《Effective Java》這本書中所說的PECS法則。
Bloch提醒說,這PECS是指”Producer Extends, Consumer Super”,這個更容易記憶和運用。
上面的接最底下的:
The Java Tutorial
java Generics and Collections, by Maurice Naftalin and Philip Wadler
Effective Java中文版(第2版), by Joshua Bloch.
儘管有這麼多豐富的資料,有時我感覺,有很多的程式員仍然不太明白Java泛型的功用和意義。這就是為什麼我想使用一種最簡單的形式來總結一下程式員需要知道的關於Java泛型的最基本的知識。
Java泛型由來的動機
理解Java泛型最簡單的方法是把它看成一種便捷文法,能節省你某些Java類型轉換(casting)上的操作:
List<Apple> box = ...;Apple apple = box.get( 0 );
上面的代碼自身已表達的很清楚:box是一個裝有Apple對象的List。get方法返回一個Apple對象執行個體,這個過程不需要進行類型轉換。沒有泛型,上面的代碼需要寫成這樣:
List box = ...;Apple apple = (Apple) box.get( 0 );
很明顯,泛型的主要好處就是讓編譯器保留參數的類型資訊,執行類型檢查,執行類型轉換操作:編譯器保證了這些類型轉換的絕對無誤。
相對於依賴程式員來記住物件類型、執行類型轉換——這會導致程式運行時的失敗,很難調試和解決,而編譯器能夠協助程式員在編譯時間強制進行大量的類型檢查,發現其中的錯誤。
泛型的構成
由泛型的構成引出了一個類型變數的概念。根據Java語言規範,類型變數是一種沒有限制的標誌符,產生於以下幾種情況:
◆ 泛型類聲明
◆ 泛型介面聲明
◆ 泛型方法聲明
◆ 泛型構造器(constructor)聲明
泛型類和介面
如果一個類或介面上有一個或多個類型變數,那它就是泛型。類型變數由角括弧界定,放在類或介面名的後面:
public interface List<T> extends Collection<T> {...}
簡單的說,類型變數扮演的角色就如同一個參數,它提供給編譯器用來類型檢查的資訊。
Java類庫裡的很多類,例如整個Collection架構都做了泛型化的修改。例如,我們在上面的第一段代碼裡用到的List介面就是一個泛型類。在那段代碼裡,box是一個List對象,它是一個帶有一個Apple類型變數的List介面的類實現的執行個體。編譯器使用這個類型變數參數在get方法被調用、返回一個Apple對象時自動對其進行類型轉換。
實際上,這新出現的泛型標記,或者說這個List介面裡的get方法是這樣的:
T get( int index);
get方法實際返回的是一個類型為T的對象,T是在List<T>聲明中的類型變數。
泛型方法和構造器(Constructor)
非常的相似,如果方法和構造器上聲明了一個或多個類型變數,它們也可以泛型化。
public static <t> T getFirst(List<T> list)
這個方法將會接受一個List<T>類型的參數,返回一個T類型的對象。
例子
你既可以使用Java類庫裡提供的泛型類,也可以使用自己的泛型類。
型別安全的寫入資料…
下面的這段代碼是個例子,我們建立了一個List<String>執行個體,然後裝入一些資料:
List<String> str = new ArrayList<String>();str.add( "Hello " );str.add( "World." );
如果我們試圖在List<String>裝入另外一種對象,編譯器就會提示錯誤:
str.add( 1 ); // 不能編譯
型別安全的讀取資料…
當我們在使用List<String>對象時,它總能保證我們得到的是一個String對象:
String myString = str.get( 0 );
遍曆
類庫中的很多類,諸如Iterator<T>,功能都有所增強,被泛型化。List<T>介面裡的iterator()方法現在返回的是Iterator<T>,由它的T next()方法返回的對象不需要再進行類型轉換,你直接得到正確的類型。
for (Iterator<String> iter = str.iterator(); iter.hasNext();) {String s = iter.next();System.out.print(s);}
使用foreach
“for each”文法同樣受益於泛型。前面的代碼可以寫出這樣:
for (String s: str) {System.out.print(s);}
這樣既容易閱讀也容易維護。
自動封裝(Autoboxing)和自動拆封(Autounboxing)
在使用Java泛型時,autoboxing/autounboxing這兩個特徵會被自動的用到,就像下面的這段代碼:
List<Integer> ints = new ArrayList<Integer>();ints.add( 0 );ints.add( 1 );int sum = 0 ;for ( int i : ints) {sum += i;}
然而,你要明白的一點是,封裝和解鎖會帶來效能上的損失,所有,通用要謹慎的使用。
子類型
在Java中,跟其它具有物件導向類型的語言一樣,類型的層級可以被設計成這樣:
在Java中,類型T的子類型既可以是類型T的一個擴充,也可以是類型T的一個直接或非直接實現(如果T是一個介面的話)。因為“成為某類型的子類型”是一個具有傳遞性質的關係,如果類型A是B的一個子類型,B是C的子類型,那麼A也是C的子類型。在上面的圖中:
◆ FujiApple(富士蘋果)是Apple的子類型
◆ Apple是Fruit(水果)的子類型
◆ FujiApple(富士蘋果)是Fruit(水果)的子類型
所有Java類型都是Object類型的子類型。
B類型的任何一個子類型A都可以被賦給一個類型B的聲明:
Apple a = ...;Fruit f = a;
泛型型別的子類型
如果一個Apple對象的執行個體可以被賦給一個Fruit對象的聲明,就像上面看到的,那麼,List<Apple> 和 a List<Fruit>之間又是個什麼關係呢?更通用些,如果類型A是類型B的子類型,那C<A> 和 C<B>之間是什麼關係?
答案會出乎你的意料:沒有任何關係。用更通俗的話,泛型型別跟其是否子類型沒有任何關係。
這意味著下面的這段代碼是無效的:
List<Apple> apples = ...;List<Fruit> fruits = apples;
下面的同樣也不允許:
List < Apple > apples;List < Fruit > fruits = ...;apples = fruits ;
為什嗎?一個蘋果是一個水果,為什麼一箱蘋果不能是一箱水果?
在某些事情上,這種說法可以成立,但在類型(類)封裝的狀態和操作上不成立。如果把一箱蘋果當成一箱水果會發生什麼情況?
List<Apple> apples = ...;List<Fruit> fruits = apples;fruits.add( new Strawberry());
如果可以這樣的話,我們就可以在list裡裝入各種不同的水果子類型,這是絕對不允許的。
另外一種方式會讓你有更直觀的理解:一箱水果不是一箱蘋果,因為它有可能是一箱另外一種水果,比如草莓(子類型)。
這是一個需要注意的問題嗎?
應該不是個大問題。而程式員對此感到意外的最大原因是數組和泛型型別上用法的不一致。對於泛型型別,它們和類型的子類型之間是沒什麼關係的。而對於數組,它們和子類型是相關的:如果類型A是類型B的子類型,那麼A[]是B[]的子類型:
Apple[] apples = ...;Fruit[] fruits = apples;
可是稍等一下!如果我們把前面的那個議論中暴露出的問題放在這裡,我們仍然能夠在一個apple類型的數組中加入strawberrie(草莓)對象:
Apple[] apples = new Apple[ 1 ];Fruit[] fruits = apples;fruits[ 0 ] = new Strawberry();
這樣寫真的可以編譯,但是在運行時拋出ArrayStoreException異常。因為數組的這特點,在儲存資料的操作上,Java運行時需要檢查類型的相容性。這種檢查,很顯然,會帶來一定的效能問題,你需要明白這一點。
重申一下,泛型使用起來更安全,能“糾正”Java數組中這種類型上的缺陷。
現在估計你會感到很奇怪,為什麼在數組上會有這種類型和子類型的關係,我來給你一個《Java Generics and Collections》這本書上給出的答案:如果它們不相關,你就沒有辦法把一個未知類型的對象數組傳入一個方法裡(不經過每次都封裝成 Object[]),就像下面的:
void sort(Object[] o);
泛型出現後,數組的這個個性已經不再有使用上的必要了(下面一部分我們會談到這個),實際上是應該避免使用