<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    隨筆-128  評論-55  文章-5  trackbacks-0

    Java泛型編程指南

    此系列文章譯自SUN的泛型編程指南, 看不懂譯文的請看原文
    http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf

    一、緒言
    JDK1.5對JAVA語言進行了做了幾個擴展,其中一個就是泛型。
    本指南旨在介紹泛型。如果你熟悉其它語言的構(gòu)造類似的東西,特別是C++的模
    板(template),你會很快發(fā)現(xiàn)它們之間的相同點及重要的不同點;如果你在其他
    地方?jīng)]看到過類似的東西,那反而更好,那樣你就可以開始全新的學習,用不著去忘
    掉那些(對JAVA泛型)容易產(chǎn)生誤解的東西。
    泛型允許你對類型進行抽象。最常見的例子是容器類型,比如那些在Collection
    層次下的類型。
    下面是那類例子的典型用法:

    List myIntList = new LinkedList();//1
    myIntList.add(new Integer(0));//2
    Integer x = (Integer) myIntList.iterator().next();//3

    第3行里的強制類型轉(zhuǎn)換有點煩人,程序通常都知道一個特定的鏈表(list)里
    存放的是何種類型的數(shù)據(jù),但卻一定要進行類型轉(zhuǎn)換。編譯器只能保證迭代器返回的
    是一個對象,要保證對Integer類型變量的賦值是類型安全的話,必須進行類型轉(zhuǎn)換。
    類型轉(zhuǎn)換不但會引起程序的混亂,還可能會導致運行時錯誤,因為程序員可能會
    犯錯誤。
    如果程序員可以如實地表達他們的意圖,即標記一個只能包含特定數(shù)據(jù)類型的鏈
    表,那會怎么樣呢?這就是泛型背后的核心思想。下面是前面代碼的泛型寫法:
    List<Integer> myIntList = new LinkedList<Integer>();//1'
    myIntList.add(new Integer(0));//2'
    Integer x = myIntList.iterator().next();//3'

    請注意變量myIntList的類型聲明,它指明了這不僅僅是一個任意的List,還
    是一個Integer類型的List,寫作List<Integer>。我們說List是一個接受類型(在
    這個例子是Integer)參數(shù)的泛華的接口,在創(chuàng)建鏈表對象的時候,我們也指定了一個
    類型參數(shù)。
    另外要注意的是在第3'行的類型轉(zhuǎn)換已經(jīng)不見了。
    現(xiàn)在你可能會想,我們所做的全部都是為了把混亂消除。我們沒有在第3行把類
    型轉(zhuǎn)換為Integer,而是在第1'行加了Integer類型參數(shù);非也非也,這里面差別很
    大,編譯器現(xiàn)在能夠在編譯期間檢測程序的類型正確性。當我們把myIntList聲明為
    類型List<Integer>的后,就意味著變量myIntList在何時何地的使用都是正確的,
    編譯器保證了這一點。相反,類型轉(zhuǎn)換只是告訴我們程序員認為它在程序的某個地方
    是正確的。
    實際的結(jié)果是,程序(特別是大型的程序)的可讀性和健壯性得到了提高。
    免費linux公開課,,現(xiàn)在報名!
     
     

       二、定義簡單的泛型

    下面是java.util包里的List和Iterator接口定義的一個小小的引用:

    public interface List<E>{
    void add(E x);
    Iterator<E> iterator();
    }
    public interface Iterator<E>{
    E next();
    boolean hasNext();
    }

    除了尖括號里的東西,這里所有的都應該很熟悉了。那是List和Iterator接口
    的規(guī)范類型參數(shù)的聲明。
    類型參數(shù)可以用在任何的泛型聲明中,就像使用普通的類型一樣(雖然有一些很
    重要的限制;看第7部分)。
    在緒言中,我們看到了List泛型聲明的調(diào)用,比如List<Integer>。在調(diào)用里面
    (通常稱為參數(shù)化類型),所有出現(xiàn)規(guī)范類型參數(shù)(這里是E)的全部都用實際的類型
    參數(shù)(這里是Integer)所代替。
    你可以想象成List<Integer>代表所有E都用Integer代替了的List:

    public interface IntegerList{
    void add(Integer x)
    Iterator<Integer> iterator();
    }

    這種想法是有所幫助的,但也會造成誤解。
    它是有所幫助的,是因為參數(shù)化類型List<integer>有看起來像這種擴展的方法。
    它會造成誤解,是因為泛型的聲明實際上不會像那樣去擴展;在源代碼中、二進制
    文件中、硬盤和內(nèi)在里,都沒有代碼的多個拷貝。如果你是一個C++程序員,你會明白
    這跟C++的模板(template)很不同。
    泛型聲明是一次編譯,永遠使用,它會變成一個單獨的class文件,就像一個普通
    的類或接口聲明。
    類型參數(shù)跟用在方法或構(gòu)造函數(shù)里的普通的參數(shù)類似,就像一個方法具有描述它運
    算用到的值的類型的規(guī)范值參一樣,泛化聲明具有規(guī)范類型參數(shù)。當一個方法被調(diào)用的
    時候,實際的參數(shù)將會被規(guī)范參數(shù)所代替而對方法求值。當一個泛化聲明被調(diào)用的時候,
    實際類型參數(shù)將會代替規(guī)范類型參數(shù)。
    命名慣例要注意的一個地方。我們建議你用一些簡煉(如果可以的話只用一個字
    符)但卻映眼的名字作為規(guī)范類型參數(shù)名。在那些名字中最后避免小寫字母,這樣可
    以很容易把規(guī)范類型參數(shù)和普通的類或接口區(qū)分開來。就像前面的例子一樣,很多容
    器類型使用E。我們將會在后面的例子里看到其他的慣例。
    免費linux公開課,,現(xiàn)在報名!
     
     

       三、泛型和子類化

    http://xoj.blogone.net

    我們來測試一下對泛型的理解,下面的代碼是否正確呢?

    List<String> ls = new ArrayList<String>();//1
    List<Object> lo = ls;//2

    第1行肯定是正確的,問題的難點在于第2行;這樣就歸結(jié)為這個問題:一個字符
    串(String)鏈表(List)是不是一個對象鏈表?大部分人的直覺是:“肯定了!”
    那好,看一下下面這兩行:

    lo.add(new Object());//3
    String s = ls.get(0);//4:企圖把一個對象賦值給字符串!

    在這里我們把ls和lo搞混淆了。我們通過別名lo來訪問字符串鏈表ls,插入不
    確定對象;結(jié)果就是ls不再存儲字符串,當我們嘗試從里面取出數(shù)據(jù)的時候就會出錯。
    Java編譯器當然不允許這樣的事情發(fā)生了,所以第2行肯定會編譯出錯。
    一般來說,如果Foo是Bar的子類型(子類或子接口),而G又是某個泛型聲明的
    話,G<Foo>并不是G<Bar>的子類型。這可能是學習泛型的時候最難的地方,因為它
    與我們的深層直覺相違背。
    直覺出錯的問題在于它把集合里的東西假想為不會改變的,我們的本能把這些東
    西看作是不變的。
    舉個例子,假設汽車公司為人口調(diào)查局提供一份駕駛員的列表,這看上去挺合理。
    假設Driver是Person的一個子類,則我們認為List<Driver>是一個List<Person>。
    而實際上提交的是一份駕駛員登記表的一個副本。否則的話,人口調(diào)查局將可以駕駛員
    的人加入到那份列表中去,汽車公司的紀錄受到破壞。
    為了解決這類問題,我們需要考慮一些更靈活的泛型,到現(xiàn)在為止碰到的規(guī)則太
    受約束了。
    免費linux公開課,,現(xiàn)在報名!
     
     

       四、通配符

    http://xoj.blogone.net

    考慮一下寫一個程序來打印一個集合對象(collection)里的所有元素。
    在舊版的語言里面,你可以會像下面那樣寫:

    void printCollection(Collection c){
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++){
    System.out.println(i.next());
    }
    }

    下面嘗試著用泛型(和新的for循環(huán)語法)來寫:

    void printCollection(Collection<Object> c){
    for (Object e : c) {
    System.out.println(e);
    }
    }

    這樣的問題是新版本的代碼還沒舊版本的代碼好用。就像我們剛示范的一樣,
    Collection<Object>并不是所有類型的集合的父類型,所以它只能接受Collection<Object>
    對象,而舊版的代碼卻可以把任何類型的集合對象作為參數(shù)來調(diào)用。
    那么,什么才是所有集合類型的父類型呢?這個東西寫作Collection<?>(讀
    作“未知集合”),就是元素類型可以為任何類型的集合。這就是它為什么被稱為“通
    配符類型”的原因。我們可以這樣寫:

    void printCollection(Collection<?> c){
    for (Object e : c) {
    System.out.println(e);
    }
    }

    現(xiàn)在,我們就可以以任何類型的集合對象作為參數(shù)來調(diào)用了。注意,在printCollection()
    方法里面,我們?nèi)匀豢梢詮腸對象中讀取元素并賦予Object類型;因為無論集合里
    實際包含了什么類型,它肯定是對象,所以是類型安全的。但對它插入任意的對象
    的話則是不安全的:

    Collection<?> c = new ArrayList<String>();
    c.add(new Object());//編譯錯誤

    由于我們并不知道c的元素類型是什么,因此我們不能對其插入對象。add()方法
    接受類型E,即集合的元素類型的參數(shù)。當實際的類型參數(shù)是?的時候,就代表是某未
    知類型。任何傳遞給add方法的參數(shù),其類型必須是該未知類型的子類型。因為我們并
    不知道那是什么類型,所以我們傳遞不了任何參數(shù)。唯一的例外就是null,因為它是任
    何(對象)類型的成員。
    另外,假設有一個List<?>,我們可以調(diào)用get()方法并使用其返回結(jié)果。結(jié)果
    類型是一個未知類型,但我們都知道它是一個對象。因此把get()方法的返回結(jié)果賦
    值給對象類型,或者把它作為一個對象參數(shù)傳遞都是類型安全的。
    免費linux公開課,,現(xiàn)在報名!
     
     
         

       四、1-有界通配符

    http://xoj.blogone.net

    考慮一個簡單的畫圖程序,它可以畫長方形和圓等形狀。為了表示這些形狀,
    你可能會定義這樣的一個類層次結(jié)構(gòu):

    public abstract class Shape{
    public abstract void draw(Canvas c);
    }
    public class Circle extends Shape{
    private int x, y, radius;
    public void draw(Canvas c) { ... }
    public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) { ... }
    }

    這些類可以在canvas上描畫:

    public class Canvas {
    public void draw(Shape s) {
    s.draw(this);
    }
    }

    任何的描畫通常都包括有幾種形狀,假設它們用一個鏈表來表示,那么如果在
    Canvas里面有一個方法來畫出所有的形狀的話,那將會很方便:

    public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
    s.draw(this);
    }
    }

    但是現(xiàn)在,類型的規(guī)則說drawAll()方法只能對確切的Shape類型鏈表調(diào)用,
    比如,它不能對List<Circle>類型調(diào)用該方法。那真是不幸,因為這個方法所要
    做的就是從鏈表中讀取形狀對象,從而對List<Circle>類型對象進行調(diào)用。我們
    真正所想的是要讓這個方法能夠接受一個任何形狀的類型鏈表:

    public void drawAll(List<? extends Shape> shapes) { ... }

    這里有一個很小但很重要的不同點:我們把類型List<Shape>替換為List<? extends Shape>。
    現(xiàn)在drawAll()方法可以接受任何Shape子類的鏈表,我們就可以如愿的對List<Circle>
    調(diào)用進行啦。
    List<? extends Shape>是一個有界通配符的例子。? 表示一個未知類型,
    就像我們之前所看到的通配符一樣。但是,我們知道在這個例子里面這個未知類型
    實際是Shape的子類型(注:它可以是Shape本身,或者是它的子類,無須在字面上
    表明它是繼承Shape類的)。我們說Shape是通配符的“上界”。
    如往常一樣,使用通配符帶來的靈活性得要付出一定的代價;代碼就是現(xiàn)在在
    方法里面不能對Shape對象插入元素。例如,下面的寫法是不允許的:

    public void addRectangle(List<? extends Shape> shapes) {
    shapes.add(0, new Rectangle()); //編譯錯誤
    }

    你應該可以指出為什么上面的代碼是不允許的。shapes.add()方法的第二個
    參數(shù)的類型是 ? 繼承Shape,也就是一個未知的Shape的子類型。既然我們不知道
    類型是什么,那么我們就不知道它是否是Rectangle的父類型了;它可能是也可能
    不是一個父類型,因此在那里傳遞一個Rectangle的對象是不安全的。
    有界通配符正是需要用來處理汽車公司給人口調(diào)查局提交數(shù)據(jù)的例子方法。在
    我們的例子里面,我們假設數(shù)據(jù)表示為姓名(用字符串表示)對人(表示為引用類
    型,比如Person或它的子類型Driver等)的映射。Map<K, V>是有兩個類型參數(shù)的
    一個泛型的例子,表示鍵值映射。
    請再一次注意規(guī)范類型參數(shù)的命名慣例:K表示鍵,V表示值。

    public class Census {
    public static void
    addRegistry(Map<String, ? extends Person> registry){ ... }
    }
    ...
    Map<String, Driver> allDrivers = ...;
    Census.addRegistry(allDrivers);
    免費linux公開課,,現(xiàn)在報名!
     
       五、泛型方法

    http://xoj.blogone.net

    考慮寫這樣一個方法,它接收一個數(shù)組和一個集合(collection)作為參數(shù),
    并把數(shù)組里的所有對象放到集合里面。
    先試試這樣:

    static void fromArrayToCollection(Object[] a, Collection<?> c){
    for (Object o : a){
    c.add(o);//編譯錯誤
    }
    }

    到現(xiàn)在,你應該學會了避免把Collection<Object>作為集合參數(shù)的類型這種初學
    者的錯誤;你可能或可能沒看出使用Collection<?>也是不行的,回想一下,你是不能
    把對象硬塞進一個未知類型的集合里面的。
    解決這類問題的方法是使用泛型方法。就像類型聲明一樣,方法也可以聲明為泛型
    的,就是說,用一個或多個類型參數(shù)作為參數(shù)。

    static <T> void fromArrayToCollection(T[]a, Collection<T> c){
    for (T o : a){
    c.add(o);//正確
    }
    }

    對于集合元素的類型是數(shù)組類型的父類型,我們就可以調(diào)用這個方法。

    Object[] oa = new Object[100];
    Collection<Object> co = new ArrayList<Object>();
    fromArrayToCollection(oa, co);// T是對象類型
    String[] sa = new String[100];
    Collection<String> cs = new ArrayList<String>();
    fromArrayToCollection(sa, cs);// T是字符串類型(String)
    fromArrayToCollection(sa, co);// T對象類型
    Integer[] ia = new Integer[100];
    Float[] fa = new Float[100];
    Number[] na = new Number[100];
    Collection<Number> cn = new ArrayList<Number>();
    fromArrayToCollection(ia, cn);// T是Number類型
    fromArrayToCollection(fa, cn);// T是Number類型
    fromArrayToCollection(na, cn);// T是Number類型
    fromArrayToCollection(na, co);// T是Number類型
    fromArrayToCollection(na, cs);// 編譯錯誤

    請注意,我們并沒有把實際的類型實參傳遞給泛型方法,因為編譯器會根據(jù)
    實參的類型為我們推斷出類型實參。一般地,編譯器推斷得到可以正確調(diào)用的最
    接近的(the most specific)實參類型。
    現(xiàn)在有一個問題:我應該什么時候使用泛型方法,什么時候使用通配符類型
    呢?為了明白這個問題的答案,我們來看看Collection庫里的幾個方法:

    interface Collection<E>{
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
    }

    在這里我們也可以用泛型方法:

    interface Collection<E>{
    public <T> boolean containsAll(Collection<T> c);
    public <? extends E>boolean addAll(Collection<T> c);
    //哈哈,類型變量也可以有界!
    }

    但是,類型參數(shù)T在containsAll和addAll兩個方法里面都只是用了一次。返
    回類型并不依賴于類型參數(shù)或其他傳遞給該方法的實參(這種是只有一個實參的簡單
    情況)。這就告訴我們類型實參是用于多態(tài)的,它的作用只是對不同的調(diào)用可以有一
    系列的實際的實參類型。如果是那樣的話,就應該使用通配符,通配符就是設計來支
    持靈活的子類型的,這也是我們這里所要表述的東西。
    泛型方法允許類型參數(shù)用于表述一個或多個的實參類型對方法或及其返回類型的
    依賴關系。如果沒有那樣的一個依賴關系的話,泛型方法就不應用使用。
    也有可能是一前一后一起使用泛型方法和通配符的情況,下面是Collections.copy()
    方法:

    class Collections {
    public static <T> void copy(List<T> dest, list< ? extends T> src) {...}
    }

    請注意這里兩個參數(shù)類型的依賴關系,任何要從源鏈表src復制過來的對象都必
    須是對目標鏈表dst元素可賦值的;所以我們可以不管src的元素類型是什么,只要
    它是T類型的子類型。copy方法的方法頭表示了使用一個類型參數(shù),但是用通配符來
    作為第二個參數(shù)的元素類型的依賴關系。
    我們是可以用另外一種不用通配符來寫這個方法頭的辦法。

    class Collections {
    public static <T, S extends T>
    vod copy(List<T> dest, List<S> src) { ...}
    }

    沒問題,但是當?shù)谝粋€類型參數(shù)用作dst的類型和批二個類型參數(shù)S的上界的
    時候,S它本身在src類型里只能使用一次,沒有其他的東西依賴于它。這就意味
    著我們可以用一個通配符來代替S了。使用通配符比聲明顯式的類型參數(shù)要來得清
    晰和簡單,因此在可能的話都優(yōu)先使用通配符。
    當通配符用于方法頭外部,作為成員變量、局部變量和數(shù)組的類型的時候,同
    樣也有優(yōu)勢。請看下面的例子。
    看回我們之前畫圖的那個問題,現(xiàn)在我們想要保留一份畫圖請求的歷史記錄。
    我們可以這樣來維護這份歷史記錄,在Shape類里用一個靜態(tài)的變量表示歷史記錄,
    然后在drawAll()方法里面把傳遞的實參儲存到那歷史記錄變量里頭。

    static List<List<? extends Shape>> history =
    new ArrayList<List<? extends Shape>>();
    public void drawAll(List<? extends Shape> shapes){
    history.addLast(shapes);
    for (Shape s: shapes) {
    s.draw(this);
    }
    }

    最后,我們再次留意一下使用類型參數(shù)的命名慣例。當沒有更精確的類型來
    區(qū)分的時候,我們用T來表示類型,這是通常是在泛型方法里面的情況。如果有多
    個類型參數(shù),我們可以用在字母表中與T相鄰的字母來表示,比如S。如果一個泛
    型方法出現(xiàn)在一個泛型類里面,一個好的方法就是,應該避免對方法和類使用相
    同的類型參數(shù)以免發(fā)生混淆。這在嵌套泛型類里也一樣。
    免費linux公開課,,現(xiàn)在報名!
     
     

       六、與遺留代碼的交互


    到現(xiàn)在為止,我們所有的例子都是在一個假想的理想世界里面的,就是所有的
    人都在使用Java語言支持泛型的最新版本。
    唉,不過在現(xiàn)實中情況卻不是那樣。千百萬行的代碼都是用早期版本的語言
    來編寫的,不可能把它們?nèi)吭谝灰怪g就轉(zhuǎn)換過來。
    在后面的第10部分,我們將會解決把遺留代碼轉(zhuǎn)為用泛型這個問題。在這部分
    我們要看的是比較簡單的問題:遺留代碼與泛型代碼如何交互?這個問題分為兩個
    部分:在泛型代碼中使用遺留代碼和在遺留代碼中使用泛型代碼。
    免費linux公開課,,現(xiàn)在報名!
     


       六-1 在泛型代碼中使用遺留代碼

    [url=http://xoj.blogone.net][url]
    當你在享受在代碼中使用泛型帶來的好處的時候,你怎么樣使用遺留代碼呢?
    假設這樣一個例子,你要使用com.Foodlibar.widgets這個包。Fooblibar.com
    的人要銷售一個庫存控制系統(tǒng),主要部分如下:

    package com.Fooblibar.widgets;
    public interface Part { ... }
    public class Inventory {
    /**
    *Adds a new Assembly to the inventory databse.
    *The assembly is given the name name, and consists of a set
    *parts specified by parts. All elements of the collection parts
    *must support the Part interface.
    **/
    public static void addAssembly(String name, Collection parts) {...}
    public static Assembly getAssembly(String name) {...}
    }
    public interface Assembly{
    Collection getParts();//Returns a collection of Parts
    }

    現(xiàn)在,你可以用上面的API來增加新的代碼,它可以很好的保證你調(diào)用參數(shù)恰當
    的addAssembly()方法,就是說傳遞的集合是一個Part類型的Collection對象,當
    然,泛型是最適合做這個:

    package com.mycompany.inventory;
    import com.Fooblibar.widgets.*;
    public class Blade implements Part{
    ...
    }
    public class Guillotine implements Part {
    }
    public class Main {
    public static void main(Sring[] args) {
    Collection<Part> c = new ArrayList<Part>();
    c.add(new Guillotine());
    c.add(new Blade());
    Inventory.addAssembly("thingee", c);
    Collection<Part> k = Inventory.getAssembly("thingee").getParts();
    }
    }

    當我們調(diào)用addAssembly方法的時候,它想要的第二個參數(shù)是Collection類型的,
    實參是Collection<Part>類型,但卻可以,為什么呢?畢竟,大多數(shù)集合存儲的都不是
    Part對象,所以總的來說,編譯器不會知道Collection存儲的是什么類型的集合。
    在正規(guī)的泛型代碼里面,Collection都帶有類型參數(shù)。當一個像Collection這樣
    的泛型不帶類型參數(shù)使用的時候,稱之為原生類型。
    很多人的第一直覺是Collection就是指Collection<Object>,但從我們先前所
    看到的可以知道,當需要的對象是Collection<Object>,而傳遞的卻是Collection<Part>
    對象的時候,是類型不安全的。確切點的說法是Collection類型表示一個未知類型的
    集合,就像Collection<?>。
    稍等一下,那樣做也是不正確的!考慮一下調(diào)用getParts()方法,它返回一個
    Collection對象,然后賦值給k,而k是Collection<Part>類型的;如果調(diào)用的結(jié)果
    是返回一個Collection<?>的對象,這個賦值可能是錯誤的。
    事實上,這個賦值是允許的,只是它會產(chǎn)生一個未檢測警告。警告是需要的,因為
    編譯器不能保證賦值的正確性。我們沒有辦法通過檢測遺留代碼中的getAssembly()方法
    來保證返回的集合的確是一個類型參數(shù)是Part的集合。程序里面的類型是Collection,
    我們可以合法的對此集合插入任何對象。
    所以,這不應該是錯誤的嗎?理論上來說,答案是:是;但實際上如果是泛型代碼
    調(diào)用遺留代碼的話,這又是允許的。對這個賦值是否可接受,得取決于程序員自己,在
    這個例子中賦值是安全的,因為getAssembly()方法約定是返回以Part作為類型參數(shù)的
    集合,盡管在類型標記中沒有表明。
    所以原生類型很像通配符類型,但它們沒有那么嚴格的類型檢測。這是有意設計成
    這樣的,從而可以允許泛型代碼可以與之前已有的遺留代碼交互。
    在泛型代碼中調(diào)用遺留代碼固然是危險的,一旦把泛型代碼和非泛型代碼混合在一
    起,泛型系統(tǒng)所提供的全部安全保證就都變得無效了。但這仍比根本不使用泛型要好,
    最起碼你知道你的代碼是一致的。
    泛型代碼出現(xiàn)的今天,仍然有很多非泛型代碼,二者混合同時使用是不可避免的。
    如果一定要把遺留代碼與泛型代碼混合使用,請小心留意那些未檢測警告。仔細的
    想想如何才能判定引發(fā)警告的代碼是安全的。
    如果仍然出錯,代碼引發(fā)的警告實際不是類型安全的,那又怎么樣呢?我們會看
    那樣的情況,接下來,我們將會部分的觀察編譯器的工作方式。
    免費linux公開課,,現(xiàn)在報名!
     
     
         

       六-2 擦除和翻譯


    public String loophole(Integer x){
    List<String> ys = new LinkedList<String>();
    List xs = ys;
    xs.add(x);//編譯時未檢測警告
    return ys.iterator().next();
    }

    在這里我們定義了一個字符串類型的鏈表和一個一般的老式鏈表,我們先插入
    一個Integer對象,然后試圖取出一個String對象,很明顯這是錯誤的。如果我們
    忽略警告繼續(xù)執(zhí)行代碼的話,程序?qū)谖覀兪褂缅e誤類型的地方出錯。在運行時,
    代碼執(zhí)行大致如下:

    public String loophole(Integer x) {
    List ys = new LinkedList;
    List xs = ys;
    xs.add(x);
    return (String)ys.iterator().next();//運行時出錯
    }

    當我們要從鏈表中取出一個元素,并把它當作是一個字符串對象而把它轉(zhuǎn)換為
    String類型的時候,我們將會得到一個ClassCastException類型轉(zhuǎn)換異常。在
    泛型版本的loophole()方法里面發(fā)生的就是這種情況。
    出現(xiàn)這種情況的原因是,Java的泛型是通過一個前臺轉(zhuǎn)換“擦除”的編譯器實現(xiàn)
    的,你基本上可以認為它是一個源碼對源碼的翻譯,這就是為何泛型版的loophole()
    方法轉(zhuǎn)變?yōu)榉欠盒桶姹镜脑颉?br /> 結(jié)果是,Java虛擬機的類型安全性和完整性永遠不會有問題,就算出現(xiàn)未檢測
    的警告。
    基本上,擦除會除去所有的泛型信息。尖括號里面的所有類型信息都會去掉,比
    如,參數(shù)化類型的List<String>會轉(zhuǎn)換為List。類型變量在之后使用時會被類型
    變量的上界(通常是Object)所替換。當最后代碼不是類型正確的時候,就會加入
    一個適當?shù)念愋娃D(zhuǎn)換,就像loophole()方法的最后一行。
    對“擦除”的完整描述不是本指南的范圍內(nèi)的內(nèi)容,但前面我們所給的簡單描述
    也差不多是那樣了。了解這點很有好處,特別是當你想做諸如把現(xiàn)有API轉(zhuǎn)為使用
    泛型(請看第10部分)這樣復雜的東西,或者是想知道為什么它們會那樣的時候。
    免費linux公開課,,現(xiàn)在報名!
     
     
         
       六-3 在遺留代碼中使用泛型

     

    現(xiàn)在我們來看看相反的情況。假設Fooblibar.com把他們的API轉(zhuǎn)換為泛型的,
    但有些客戶還沒有轉(zhuǎn)換。代碼就會像下面的:

    package com.Fooblibar.widgets;
    public interface Part { ... }
    publlic class Inventory {
    /**
    *Adds a new Assembly to the inventory database.
    *The assembly is given the name name, and consists of a set
    *parts specified by parts. All elements of the collection parts
    *must support the Part interface.
    **/
    public static void addAssembly(String name, Collection<Part> parts) {...}
    public static Assembly getAssembly(String name){ ... }
    }
    public interface Assembly {
    Collection<Part> getParts();//Return a collection of Parts
    }

    客戶代碼如下:

    package com.mycompany.inventory;
    import com.Fooblibar.widgets.*;
    public class Blade implements Part {
    ...
    }
    public class Guillotine implements Part {
    ...
    }
    public class Main {
    public static void main(String[] args){
    Collection c = new ArrayList();
    c.add(new Guillotine());
    c.add(new Blade());
    Inventory.addAssembly("thingee", c);//1: unchecked warning
    Collection k = Inventory.getAssembly("thingee").getParts();
    }
    }

    客戶代碼是在引進泛型之前寫下的,但是它使用了com.Fooblibar.widgets包和集
    合庫,兩個現(xiàn)在都是在用泛型的。在客戶代碼里面使用的泛型全部都是原生類型。
    第1行產(chǎn)生一個未檢測警告,因為把一個原生Collection傳遞給了一個需要Part類型的
    Collection的地方,編譯器不能保證原生的Collection是一個Part類型的Collection。
    不這樣做的話,你也可以在編譯客戶代碼的時候使用source 1.4這個標記來保證不
    會產(chǎn)生警告。但是這樣的話你就不能使用所有JDK 1.5引入的新的語言特性。
    免費linux公開課,,現(xiàn)在報名!
     
     
         


       七、晦澀難懂的部分

    七-1 泛型類為所有調(diào)用所共享
    下面的代碼段會打印出什么呢?

    List<String> l1 = new ArrayList<String>();
    List<Integer> l2 = new ArrayList<Integer>();
    System.out.println(l1.getClass() == l2.getClass());

    你可能會說是false,但是你錯了,打印的是true,因為所有泛型類的實例它們
    的運行時的類(run-time class)都是一樣的,不管它們實際類型參數(shù)如何。
    泛型類之所以為泛型的,是因為它對所有可能的類型參數(shù)都有相同的行為,相同
    的類可以看作是有很多不同的類型。
    結(jié)果就是,一個類的靜態(tài)的變量和方法也共享于所有的實例中,這就是為什么不
    允許在靜態(tài)方法或初始化部分、或者在靜態(tài)變量的聲明或初始化中引用類型參數(shù)。
    免費linux公開課,,現(xiàn)在報名!
     
     
     
     

       七-2 強制類型轉(zhuǎn)換和instanceof

    泛型類在它所有的實例****享,就意味著判斷一個實例是否是一個特別調(diào)用的泛
    型的實例是毫無意義的:

    Collection cs = new ArrayList<String>();
    if (cs instanceof Collection<String>) {...}//非法

    類似地,像這樣的強制類型轉(zhuǎn)換:

    Collection<String> cstr = (Collection<String>) cs;//未檢測警告

    給出了一個未檢測的警告,因為這里系統(tǒng)在運行時并不會檢測。
    對于類型變量也一樣:

    <T> T BadCast(T t, Object o) {
    return (T) o;//未檢測警告
    }

    類型變量不存在于運行時,這就是說它們對時間或空間的性能不會造成影響。
    但也因此而不能通過強制類型轉(zhuǎn)換可靠地使用它們了。
    免費linux公開課,,現(xiàn)在報名!
     
     
         
       七-3 數(shù)組

    數(shù)組對象的組件類型可能不是一個類型變量或一個參數(shù)化類型,除非它是一個
    (無界的)通配符類型。你可以聲明元素類型是類型變量和參數(shù)華類型的數(shù)組類型,
    但元素類型不能是數(shù)組對象。
    這自然有點郁悶,但這個限制對避免下面的情況是必要的:

    List<Strign>[] lsa = new List<String>[10];//實際上是不允許的
    Object o = lsa;
    Object[] oa = (Object[]) o;
    List<Integer> li = new ArrayList<Integer>();
    li.add(new Integer(8));
    oa[1] = li;//不合理,但可以通過運行時的賦值檢測
    String s = lsa[1].get(0);//運行時出錯:ClassCastException異常

    如果參數(shù)化類型的數(shù)組允許的話,那么上面的例子編譯時就不會有未檢測的警告,
    但在運行時出錯。對于泛型編程,我們的主要設計目標是類型安全,而特別的是這個
    語言的設計保證了如果使用了javac -source 1.5來編譯整個程序而沒有未檢測的
    警告的話,它是類型安全的。
    但是你仍然會使用通配符數(shù)組,這與上面的代碼相比有兩個變化。首先是不使用
    數(shù)組對象或元素類型被參數(shù)化的數(shù)組類型,這樣我們就需要在從數(shù)組中取出一個字符
    串的時候進行強制類型轉(zhuǎn)換:

    List<?>[] lsa = new List<?>[10];//沒問題,無界通配符類型數(shù)組
    Object o = lsa;
    Object[] oa = (Object[]) o;
    List<Integer> li = new ArrayList<Integer>();
    li.add(new Integer(3));
    oa[1] = li;//正確
    String s = (String) lsa[1].get(0);//運行時錯誤,顯式強制類型轉(zhuǎn)換

    第二個變化是,我們不創(chuàng)建元素類型被參數(shù)化的數(shù)組對象,但仍然使用參數(shù)化元素
    類型的數(shù)組類型,這是允許的,但引起現(xiàn)未檢測警告。這樣的程序?qū)嶋H上是不安全的,
    甚至最終會出錯。

    List<String>[] lsa = new List<?>[10];//未檢測警告-這是不安全的!
    Object o = lsa;
    Object[] oa = (Object[]) o;
    List<Integer> li = new ArrayList<integer>();
    li.add(new Integer(3));
    oa[1]=li;//正確
    String s = lsa[1].get(0);//運行出錯,但之前已經(jīng)被警告

    類似地,想創(chuàng)建一個元素類型是類型變量的數(shù)組對象的話,將會編譯出錯。

    <T> T[] makeArray(T t){
    return new T[100];//錯誤
    }

    因為類型變量并不存在于運行時,所以沒有辦法知道實際的數(shù)組類型是什么。
    要突破這類限制,我們可以用第8部分說到的用類名作為運行時標記的方法。
    免費linux公開課,,現(xiàn)在報名!
     
     
         
       八、 把類名作為運行時的類型標記

    JDK1.5中的一個變化是java.lang.Class是泛化的,一個有趣的例子是對
    容器外的東西使用泛型。
    現(xiàn)在Class類有一個類型參數(shù)T,你可能會問,T代表什么啊?它就代表Class
    對象所表示的類型。
    比如,String.class的類型是Class<String>,Serializable.class的
    類型是Class<Serializable>,這可以提高你的反射代碼中的類型安全性。
    特別地,由于現(xiàn)在Class類中的newInstance()方法返回一個T對象,因此
    在通過反射創(chuàng)建對象的時候可以得到更精確的類型。
    其中一個方法就是顯式傳入一個factory對象,代碼如下:

    interface Factory<T> {T make();}
    public <T> Collection<T> select(Factory<T> factory, String statement){
    Collection<T> result = new ArrayList<T>();
    //用JDBC運行SQL查詢
    for(/*遍歷JDBC結(jié)果*/){
    T item = factory.make();
    /*通過SQL結(jié)果用反射和設置數(shù)據(jù)項*/
    result.add(item);
    }
    return result;
    }

    你可以這樣調(diào)用:

    select(new Factory<EmpInfo>(){ public EmpInfo make() {
    return new EmpInfo();
    }}
    , "selection string");

    或者聲明一個EmpInfoFactory類來支持Factory接口:

    class EmpInfoFactory implements Factory<EmpInfo>{
    ...
    public EmpInfo make() { return new EmpInfo();}
    }

    然后這樣調(diào)用:

    select(getMyEmpInfoFactory(), "selection string");

    這種解決辦法需要下面的其中之一:
    · 在調(diào)用的地方使用詳細的匿名工廠類(verbose anonymous factory classes),或者
    · 為每個使用的類型聲明一個工廠類,并把工廠實例傳遞給調(diào)用的地方,這樣有點不自然。

    使用類名作為一個工廠對象是非常自然的事,這樣的話還可以為反射所用。現(xiàn)在
    沒有泛型的代碼可能寫作如下:

    Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
    ...
    public static Collection select(Class c, String sqlStatement) {
    Collection result = new ArrayList();
    /*用JDBC執(zhí)行SQL查詢*/
    for(/*遍歷JDBC產(chǎn)生的結(jié)果*/){
    Object item = c.newInstance();
    /*通過SQL結(jié)果用反射和設置數(shù)據(jù)項*/
    result.add(item);
    }
    return result;
    }

    但是,這樣并不能得到我們所希望的更精確的集合類型,現(xiàn)在Class是泛化的,
    我們可以這樣寫:

    Collection<EmpInfo> emps =
    sqlUtility.select(EmpInfo.class, "select * from emps");
    ...
    public static <T> Collection<T> select(Class<T> c, String sqlStatement) {
    Collection<T> result = new ArrayList<T>();
    /*用JDBC執(zhí)行SQL查詢*/
    for(/*遍歷JDBC產(chǎn)生的結(jié)果*/){
    T item = c.newInstance();
    /*通過SQL結(jié)果用反射和設置數(shù)據(jù)項*/
    result.add(item);
    }
    return result;
    }

    這樣就通過類型安全的方法來得到了精確的集合類型了。
    這種使用類名作為運行時類型標記的技術(shù)是一個很有用的技巧,是需要知道的。
    在處理注釋的新的API中也有很多類似的情況。
    免費linux公開課,,現(xiàn)在報名!
     
     
         

       九 通配符的其他作用

    (more fun with wildcards,不知道如何譯才比較妥當,呵呵。)

    在這部分,我們將會仔細看看通配符的幾個較為深入的用途。我們已經(jīng)從幾個
    有界通配符的例子中看到,它對從某一數(shù)據(jù)結(jié)構(gòu)中讀取數(shù)據(jù)是很有用的。現(xiàn)在來看
    看相反的情況,只對數(shù)據(jù)結(jié)構(gòu)進行寫操作。
    下面的Sink接口就是這類情況的一個簡單的例子:

    interface Sink<T> {
    flush(T t);
    }

    我們可以想象在下面的示范的例子中使用它,writeAll()方法用于把coll集合
    里的所有元素填充(flush)到Sink接口變量snk中,并返回最后一個填充的元素。

    public static <T> T writeAll(Collection<T> coll, Sink<T> snk){
    T last;
    for (T t: coll){
    last = t;
    snk.flush(last);
    }
    return last;
    }
    ...
    Sink<Object> s;
    Collection<String> cs;
    String str = writeAll(cs, s);//非法調(diào)用

    如注釋所注,這里對writeAll()方法的調(diào)用是非法的,因為無有效的類型參數(shù)
    可以引用;String和Object都不適合作為T的類型,因為Collection和Sink的元素
    必須是相同類型的。
    我們可以通過使用通配符來改寫writeAll()的方法頭來處理,如下:

    public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
    ...
    String str = writeAll(cs, s);//調(diào)用沒問題,但返回類型錯誤

    現(xiàn)在調(diào)用是合法的了,但由于T的類型跟元素類型是Object的s一樣,因為返回的
    類型也是Object,因此賦值是不正確的。
    解決辦法是使用我們之前從未見過的一種有界通配符形式:帶下界的通配符。
    語法 ? super T 表示了是未知的T的父類型,這與我們之前所使用的有界
    (父類型:或者T類型本身,要記住的是,你類型關系是自反的)
    通配符是對偶有界通配符,即用 ? extends T 表示未知的T的子類型。

    public static<T> T writeAll(Collection<T> coll, Sink<? super T> snk) {...}
    ...
    String str = writeAll(cs, s);//正確!

    使用這個語法的調(diào)用是合法的,指向的類型是所期望的String類型。

    現(xiàn)在我們來看一個比較現(xiàn)實一點的例子,java.util.TreeSet<E>表示元素類型
    是E的樹形數(shù)據(jù)結(jié)構(gòu)里的元素是有序的,創(chuàng)建一個TreeSet對象的一個方法是使用參數(shù)
    是Comparator對象的構(gòu)造函數(shù),Comparator對象用于對TreeSet對象里的元素進行
    所期望的排序進行分類。

    TreeSet(Comparator<E> c)

    Comparator接口是必要的:

    interface Comparator<T> {
    int compare(T fst, T snd);
    }

    假設我們想要創(chuàng)建一個TreeSet<String>對象,并傳入一下合適的Comparator
    對象,我們傳遞的Comparator是能夠比較字符串的。我們可以用Comparator<String>,
    但Comparator<Object>也是可以的。但是,我們不能對Comparator<Object>對象
    調(diào)用上面所給的構(gòu)造函數(shù),我們可以用一個下界通配符來得到我們想要的靈活性:

    TreeSet(Comparator<? super E> c)

    這樣就可以使用適合的Comparator對象啦。
    最后一個下界通配符的例子,我們來看看Collections.max()方法,這個方法
    返回作為參數(shù)傳遞的Collection對象中最大的元素。
    現(xiàn)在,為了max()方法能正常運行,傳遞的Collection對象中的所有元素都必
    須是實現(xiàn)了Comparable接口的,還有就是,它們之間必須是可比較的。
    先試一下泛化方法頭的寫法:

    public static <T extends Comparable<T>>
    T max(Collection<T> coll)

    那樣,方法就接受一個自身可比較的(comparable)某個T類型的Collection
    對象,并返回T類型的一個元素。這樣顯得太束縛了。
    來看看為什么,假設一個類型可以與合意的對象進行比較:

    class Foo implements Comparable<Object> {...}
    ...
    Collection<Foo> cf = ...;
    Collectins.max(cf);//應該可以正常運行

    cf里的每個對象都可以和cf里的任意其他元素進行比較,因為每個元素都是Foo
    的對象,而Foo對象可以與任意的對象進行比較,特別是同是Foo對象的。但是,使用
    上面的方法頭,我們會發(fā)現(xiàn)這樣的調(diào)用是不被接受的,指向的類型必須是Foo,但Foo
    并沒有實現(xiàn)Comparable<Foo>。
    T對于自身的可比性不是必須的,需要的是T與其父類型是可比的,就像下面:
    (實際的Collections.max()方法頭在后面的第10部分將會講得更多)

    public static <T extends Comparable<? super T>>
    T max(Collection<T> coll)

    這樣推理出來的結(jié)果基本上適用于想用Comparable來用于任意類型的用法:
    就是你想這樣用Comparable<? super T>。
    總的來說,如果你有一個只能一個T類型參數(shù)作為實參的API的話,你就應該用
    下界



    Author: orangelizq
    email: orangelizq@163.com

    歡迎大家訪問我的個人網(wǎng)站 萌萌的IT人
    posted on 2008-09-16 14:34 桔子汁 閱讀(1137) 評論(0)  編輯  收藏 所屬分類: J2SE
    主站蜘蛛池模板: 精品亚洲视频在线观看| 九九全国免费视频| 国产亚洲精久久久久久无码77777| 国产亚洲一卡2卡3卡4卡新区| 韩国免费三片在线视频| 免费A级毛片无码视频| 无套内谢孕妇毛片免费看看| 亚洲乱码一二三四区乱码| 亚洲电影中文字幕| 日本h在线精品免费观看| 国产成人精品日本亚洲专区6| 国产成人免费a在线视频色戒| 好吊色永久免费视频大全 | 免费h黄肉动漫在线观看| 在线观看免费视频资源| 99久久国产精品免费一区二区| 亚洲最大在线观看| 亚洲av成人无码久久精品| 久久久久亚洲精品男人的天堂| 84pao强力永久免费高清| 在线观看片免费人成视频无码| 亚洲一区二区三区乱码在线欧洲| 免费一级毛片不卡不收费| 免费无码又爽又刺激高潮| 搡女人真爽免费视频大全| 成年黄网站色大免费全看| 国产精彩免费视频| 一级毛片试看60分钟免费播放| 一区二区三区亚洲| 亚洲av无码精品网站| 午夜视频免费成人| 女人18毛片水真多免费播放| 大地资源在线观看免费高清| 美女视频黄的全免费视频网站| aa级毛片毛片免费观看久| 亚洲xxxx视频| 亚洲成A人片在线播放器| 亚洲精品蜜夜内射| 亚洲欧美成人av在线观看| 亚洲6080yy久久无码产自国产| 亚洲视频.com|