此系列文章譯自SUN的泛型編程指南, 看不懂譯文的請看原文
http://java./j2se/1.5/pdf/generics-tutorial.pdf 一,、緒言 JDK1.5對JAVA語言進(jìn)行了做了幾個(gè)擴(kuò)展,,其中一個(gè)就是泛型,。 本指南旨在介紹泛型。如果你熟悉其它語言的構(gòu)造類似的東西,,特別是C++的模 板(template),,你會很快發(fā)現(xiàn)它們之間的相同點(diǎn)及重要的不同點(diǎn);如果你在其他 地方?jīng)]看到過類似的東西,,那反而更好,,那樣你就可以開始全新的學(xué)習(xí),用不著去忘 掉那些(對JAVA泛型)容易產(chǎn)生誤解的東西,。 泛型允許你對類型進(jìn)行抽象,。最常見的例子是容器類型,比如那些在Collection 層次下的類型,。 下面是那類例子的典型用法: List myIntList = new LinkedList();//1 myIntList.add(new Integer(0));//2 Integer x = (Integer) myIntList.iterator().next();//3 第3行里的強(qiáng)制類型轉(zhuǎn)換有點(diǎn)煩人,,程序通常都知道一個(gè)特定的鏈表(list)里 存放的是何種類型的數(shù)據(jù),但卻一定要進(jìn)行類型轉(zhuǎn)換,。編譯器只能保證迭代器返回的 是一個(gè)對象,,要保證對Integer類型變量的賦值是類型安全的話,必須進(jìn)行類型轉(zhuǎn)換,。 類型轉(zhuǎn)換不但會引起程序的混亂,,還可能會導(dǎo)致運(yùn)行時(shí)錯(cuò)誤,因?yàn)槌绦騿T可能會 犯錯(cuò)誤,。 如果程序員可以如實(shí)地表達(dá)他們的意圖,,即標(biāo)記一個(gè)只能包含特定數(shù)據(jù)類型的鏈 表,那會怎么樣呢,?這就是泛型背后的核心思想,。下面是前面代碼的泛型寫法: List<Integer> myIntList = new LinkedList<Integer>();//1‘ myIntList.add(new Integer(0));//2‘ Integer x = myIntList.iterator().next();//3‘ 請注意變量myIntList的類型聲明,它指明了這不僅僅是一個(gè)任意的List,還 是一個(gè)Integer類型的List,,寫作List<Integer>,。我們說List是一個(gè)接受類型(在 這個(gè)例子是Integer)參數(shù)的泛華的接口,在創(chuàng)建鏈表對象的時(shí)候,,我們也指定了一個(gè) 類型參數(shù),。 另外要注意的是在第3‘行的類型轉(zhuǎn)換已經(jīng)不見了。 現(xiàn)在你可能會想,,我們所做的全部都是為了把混亂消除,。我們沒有在第3行把類 型轉(zhuǎn)換為Integer,而是在第1‘行加了Integer類型參數(shù),;非也非也,,這里面差別很 大,編譯器現(xiàn)在能夠在編譯期間檢測程序的類型正確性,。當(dāng)我們把myIntList聲明為 類型List<Integer>的后,,就意味著變量myIntList在何時(shí)何地的使用都是正確的, 編譯器保證了這一點(diǎn),。相反,,類型轉(zhuǎn)換只是告訴我們程序員認(rèn)為它在程序的某個(gè)地方 是正確的。 實(shí)際的結(jié)果是,,程序(特別是大型的程序)的可讀性和健壯性得到了提高,。 免費(fèi)linux公開課,,現(xiàn)在報(bào)名! 二,、定義簡單的泛型 下面是java.util包里的List和Iterator接口定義的一個(gè)小小的引用: public interface List<E>{ void add(E x); Iterator<E> iterator(); } public interface Iterator<E>{ E next(); boolean hasNext(); } 除了尖括號里的東西,,這里所有的都應(yīng)該很熟悉了。那是List和Iterator接口 的規(guī)范類型參數(shù)的聲明,。 類型參數(shù)可以用在任何的泛型聲明中,,就像使用普通的類型一樣(雖然有一些很 重要的限制;看第7部分),。 在緒言中,,我們看到了List泛型聲明的調(diào)用,比如List<Integer>,。在調(diào)用里面 ?。ㄍǔ7Q為參數(shù)化類型),所有出現(xiàn)規(guī)范類型參數(shù)(這里是E)的全部都用實(shí)際的類型 參數(shù)(這里是Integer)所代替,。 你可以想象成List<Integer>代表所有E都用Integer代替了的List: public interface IntegerList{ void add(Integer x) Iterator<Integer> iterator(); } 這種想法是有所幫助的,,但也會造成誤解。 它是有所幫助的,,是因?yàn)閰?shù)化類型List<integer>有看起來像這種擴(kuò)展的方法,。 它會造成誤解,,是因?yàn)榉盒偷穆暶鲗?shí)際上不會像那樣去擴(kuò)展;在源代碼中,、二進(jìn)制 文件中,、硬盤和內(nèi)在里,都沒有代碼的多個(gè)拷貝,。如果你是一個(gè)C++程序員,你會明白 這跟C++的模板(template)很不同,。 泛型聲明是一次編譯,,永遠(yuǎn)使用,它會變成一個(gè)單獨(dú)的class文件,,就像一個(gè)普通 的類或接口聲明,。 類型參數(shù)跟用在方法或構(gòu)造函數(shù)里的普通的參數(shù)類似,就像一個(gè)方法具有描述它運(yùn) 算用到的值的類型的規(guī)范值參一樣,,泛化聲明具有規(guī)范類型參數(shù),。當(dāng)一個(gè)方法被調(diào)用的 時(shí)候,實(shí)際的參數(shù)將會被規(guī)范參數(shù)所代替而對方法求值,。當(dāng)一個(gè)泛化聲明被調(diào)用的時(shí)候,, 實(shí)際類型參數(shù)將會代替規(guī)范類型參數(shù)。 命名慣例要注意的一個(gè)地方,。我們建議你用一些簡煉(如果可以的話只用一個(gè)字 符)但卻映眼的名字作為規(guī)范類型參數(shù)名,。在那些名字中最后避免小寫字母,這樣可 以很容易把規(guī)范類型參數(shù)和普通的類或接口區(qū)分開來,。就像前面的例子一樣,,很多容 器類型使用E。我們將會在后面的例子里看到其他的慣例,。 免費(fèi)linux公開課,,,現(xiàn)在報(bào)名! 三、泛型和子類化 http://xoj. 我們來測試一下對泛型的理解,,下面的代碼是否正確呢,? List<String> ls = new ArrayList<String>();//1 List<Object> lo = ls;//2 第1行肯定是正確的,問題的難點(diǎn)在于第2行,;這樣就歸結(jié)為這個(gè)問題:一個(gè)字符 串(String)鏈表(List)是不是一個(gè)對象鏈表,?大部分人的直覺是:“肯定了!” 那好,,看一下下面這兩行: lo.add(new Object());//3 String s = ls.get(0);//4:企圖把一個(gè)對象賦值給字符串,! 在這里我們把ls和lo搞混淆了。我們通過別名lo來訪問字符串鏈表ls,,插入不 確定對象,;結(jié)果就是ls不再存儲字符串,,當(dāng)我們嘗試從里面取出數(shù)據(jù)的時(shí)候就會出錯(cuò)。 Java編譯器當(dāng)然不允許這樣的事情發(fā)生了,,所以第2行肯定會編譯出錯(cuò),。 一般來說,如果Foo是Bar的子類型(子類或子接口),,而G又是某個(gè)泛型聲明的 話,,G<Foo>并不是G<Bar>的子類型。這可能是學(xué)習(xí)泛型的時(shí)候最難的地方,,因?yàn)樗?br> 與我們的深層直覺相違背,。 直覺出錯(cuò)的問題在于它把集合里的東西假想為不會改變的,我們的本能把這些東 西看作是不變的,。 舉個(gè)例子,,假設(shè)汽車公司為人口調(diào)查局提供一份駕駛員的列表,這看上去挺合理,。 假設(shè)Driver是Person的一個(gè)子類,,則我們認(rèn)為List<Driver>是一個(gè)List<Person>。 而實(shí)際上提交的是一份駕駛員登記表的一個(gè)副本,。否則的話,,人口調(diào)查局將可以駕駛員 的人加入到那份列表中去,汽車公司的紀(jì)錄受到破壞。 為了解決這類問題,我們需要考慮一些更靈活的泛型,,到現(xiàn)在為止碰到的規(guī)則太 受約束了,。 免費(fèi)linux公開課,,現(xiàn)在報(bào)名! 四、通配符 http://xoj. 考慮一下寫一個(gè)程序來打印一個(gè)集合對象(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)用。 那么,,什么才是所有集合類型的父類型呢,?這個(gè)東西寫作Collection<?>(讀 作“未知集合”),就是元素類型可以為任何類型的集合,。這就是它為什么被稱為“通 配符類型”的原因,。我們可以這樣寫: void printCollection(Collection<?> c){ for (Object e : c) { System.out.println(e); } } 現(xiàn)在,我們就可以以任何類型的集合對象作為參數(shù)來調(diào)用了,。注意,,在printCollection() 方法里面,,我們?nèi)匀豢梢詮腸對象中讀取元素并賦予Object類型;因?yàn)闊o論集合里 實(shí)際包含了什么類型,,它肯定是對象,,所以是類型安全的。但對它插入任意的對象 的話則是不安全的: Collection<?> c = new ArrayList<String>(); c.add(new Object());//編譯錯(cuò)誤 由于我們并不知道c的元素類型是什么,,因此我們不能對其插入對象,。add()方法 接受類型E,即集合的元素類型的參數(shù),。當(dāng)實(shí)際的類型參數(shù)是?的時(shí)候,,就代表是某未 知類型。任何傳遞給add方法的參數(shù),,其類型必須是該未知類型的子類型。因?yàn)槲覀儾?br> 不知道那是什么類型,,所以我們傳遞不了任何參數(shù),。唯一的例外就是null,因?yàn)樗侨?br> 何(對象)類型的成員,。 另外,,假設(shè)有一個(gè)List<?>,我們可以調(diào)用get()方法并使用其返回結(jié)果,。結(jié)果 類型是一個(gè)未知類型,,但我們都知道它是一個(gè)對象。因此把get()方法的返回結(jié)果賦 值給對象類型,,或者把它作為一個(gè)對象參數(shù)傳遞都是類型安全的,。 免費(fèi)linux公開課,,現(xiàn)在報(bào)名! 四,、1-有界通配符 http://xoj. 考慮一個(gè)簡單的畫圖程序,,它可以畫長方形和圓等形狀。為了表示這些形狀,, 你可能會定義這樣的一個(gè)類層次結(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); } } 任何的描畫通常都包括有幾種形狀,,假設(shè)它們用一個(gè)鏈表來表示,那么如果在 Canvas里面有一個(gè)方法來畫出所有的形狀的話,,那將會很方便: public void drawAll(List<Shape> shapes) { for (Shape s: shapes) { s.draw(this); } } 但是現(xiàn)在,,類型的規(guī)則說drawAll()方法只能對確切的Shape類型鏈表調(diào)用, 比如,,它不能對List<Circle>類型調(diào)用該方法,。那真是不幸,因?yàn)檫@個(gè)方法所要 做的就是從鏈表中讀取形狀對象,,從而對List<Circle>類型對象進(jìn)行調(diào)用,。我們 真正所想的是要讓這個(gè)方法能夠接受一個(gè)任何形狀的類型鏈表: public void drawAll(List<? extends Shape> shapes) { ... } 這里有一個(gè)很小但很重要的不同點(diǎn):我們把類型List<Shape>替換為List<? extends Shape>,。 現(xiàn)在drawAll()方法可以接受任何Shape子類的鏈表,我們就可以如愿的對List<Circle> 調(diào)用進(jìn)行啦,。 List<? extends Shape>是一個(gè)有界通配符的例子,。? 表示一個(gè)未知類型, 就像我們之前所看到的通配符一樣,。但是,,我們知道在這個(gè)例子里面這個(gè)未知類型 實(shí)際是Shape的子類型(注:它可以是Shape本身,或者是它的子類,,無須在字面上 表明它是繼承Shape類的),。我們說Shape是通配符的“上界”。 如往常一樣,,使用通配符帶來的靈活性得要付出一定的代價(jià),;代碼就是現(xiàn)在在 方法里面不能對Shape對象插入元素。例如,,下面的寫法是不允許的: public void addRectangle(List<? extends Shape> shapes) { shapes.add(0, new Rectangle()); //編譯錯(cuò)誤 } 你應(yīng)該可以指出為什么上面的代碼是不允許的,。shapes.add()方法的第二個(gè) 參數(shù)的類型是 ? 繼承Shape,也就是一個(gè)未知的Shape的子類型,。既然我們不知道 類型是什么,,那么我們就不知道它是否是Rectangle的父類型了;它可能是也可能 不是一個(gè)父類型,,因此在那里傳遞一個(gè)Rectangle的對象是不安全的,。 有界通配符正是需要用來處理汽車公司給人口調(diào)查局提交數(shù)據(jù)的例子方法。在 我們的例子里面,,我們假設(shè)數(shù)據(jù)表示為姓名(用字符串表示)對人(表示為引用類 型,,比如Person或它的子類型Driver等)的映射。Map<K, V>是有兩個(gè)類型參數(shù)的 一個(gè)泛型的例子,,表示鍵值映射,。 請?jiān)僖淮巫⒁庖?guī)范類型參數(shù)的命名慣例:K表示鍵,V表示值,。 public class Census { public static void addRegistry(Map<String, ? extends Person> registry){ ... } } ... Map<String, Driver> allDrivers = ...; Census.addRegistry(allDrivers); 免費(fèi)linux公開課,,,現(xiàn)在報(bào)名! 五、泛型方法 http://xoj. 考慮寫這樣一個(gè)方法,,它接收一個(gè)數(shù)組和一個(gè)集合(collection)作為參數(shù),, 并把數(shù)組里的所有對象放到集合里面。 先試試這樣: static void fromArrayToCollection(Object[] a, Collection<?> c){ for (Object o : a){ c.add(o);//編譯錯(cuò)誤 } } 到現(xiàn)在,,你應(yīng)該學(xué)會了避免把Collection<Object>作為集合參數(shù)的類型這種初學(xué) 者的錯(cuò)誤,;你可能或可能沒看出使用Collection<?>也是不行的,回想一下,,你是不能 把對象硬塞進(jìn)一個(gè)未知類型的集合里面的,。 解決這類問題的方法是使用泛型方法,。就像類型聲明一樣,方法也可以聲明為泛型 的,,就是說,,用一個(gè)或多個(gè)類型參數(shù)作為參數(shù)。 static <T> void fromArrayToCollection(T[]a, Collection<T> c){ for (T o : a){ c.add(o);//正確 } } 對于集合元素的類型是數(shù)組類型的父類型,,我們就可以調(diào)用這個(gè)方法,。 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);// 編譯錯(cuò)誤 請注意,我們并沒有把實(shí)際的類型實(shí)參傳遞給泛型方法,,因?yàn)榫幾g器會根據(jù) 實(shí)參的類型為我們推斷出類型實(shí)參,。一般地,編譯器推斷得到可以正確調(diào)用的最 接近的(the most specific)實(shí)參類型,。 現(xiàn)在有一個(gè)問題:我應(yīng)該什么時(shí)候使用泛型方法,,什么時(shí)候使用通配符類型 呢?為了明白這個(gè)問題的答案,,我們來看看Collection庫里的幾個(gè)方法: 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兩個(gè)方法里面都只是用了一次。返 回類型并不依賴于類型參數(shù)或其他傳遞給該方法的實(shí)參(這種是只有一個(gè)實(shí)參的簡單 情況),。這就告訴我們類型實(shí)參是用于多態(tài)的,,它的作用只是對不同的調(diào)用可以有一 系列的實(shí)際的實(shí)參類型。如果是那樣的話,,就應(yīng)該使用通配符,,通配符就是設(shè)計(jì)來支 持靈活的子類型的,這也是我們這里所要表述的東西,。 泛型方法允許類型參數(shù)用于表述一個(gè)或多個(gè)的實(shí)參類型對方法或及其返回類型的 依賴關(guān)系,。如果沒有那樣的一個(gè)依賴關(guān)系的話,泛型方法就不應(yīng)用使用,。 也有可能是一前一后一起使用泛型方法和通配符的情況,,下面是Collections.copy() 方法: class Collections { public static <T> void copy(List<T> dest, list< ? extends T> src) {...} } 請注意這里兩個(gè)參數(shù)類型的依賴關(guān)系,任何要從源鏈表src復(fù)制過來的對象都必 須是對目標(biāo)鏈表dst元素可賦值的,;所以我們可以不管src的元素類型是什么,,只要 它是T類型的子類型。copy方法的方法頭表示了使用一個(gè)類型參數(shù),,但是用通配符來 作為第二個(gè)參數(shù)的元素類型的依賴關(guān)系,。 我們是可以用另外一種不用通配符來寫這個(gè)方法頭的辦法。 class Collections { public static <T, S extends T> vod copy(List<T> dest, List<S> src) { ...} } 沒問題,,但是當(dāng)?shù)谝粋€(gè)類型參數(shù)用作dst的類型和批二個(gè)類型參數(shù)S的上界的 時(shí)候,,S它本身在src類型里只能使用一次,,沒有其他的東西依賴于它。這就意味 著我們可以用一個(gè)通配符來代替S了,。使用通配符比聲明顯式的類型參數(shù)要來得清 晰和簡單,,因此在可能的話都優(yōu)先使用通配符。 當(dāng)通配符用于方法頭外部,,作為成員變量,、局部變量和數(shù)組的類型的時(shí)候,同 樣也有優(yōu)勢,。請看下面的例子,。 看回我們之前畫圖的那個(gè)問題,現(xiàn)在我們想要保留一份畫圖請求的歷史記錄,。 我們可以這樣來維護(hù)這份歷史記錄,,在Shape類里用一個(gè)靜態(tài)的變量表示歷史記錄, 然后在drawAll()方法里面把傳遞的實(shí)參儲存到那歷史記錄變量里頭,。 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ù)的命名慣例。當(dāng)沒有更精確的類型來 區(qū)分的時(shí)候,,我們用T來表示類型,,這是通常是在泛型方法里面的情況。如果有多 個(gè)類型參數(shù),,我們可以用在字母表中與T相鄰的字母來表示,,比如S。如果一個(gè)泛 型方法出現(xiàn)在一個(gè)泛型類里面,,一個(gè)好的方法就是,,應(yīng)該避免對方法和類使用相 同的類型參數(shù)以免發(fā)生混淆。這在嵌套泛型類里也一樣,。 免費(fèi)linux公開課,,,現(xiàn)在報(bào)名! 六、與遺留代碼的交互 到現(xiàn)在為止,,我們所有的例子都是在一個(gè)假想的理想世界里面的,,就是所有的 人都在使用Java語言支持泛型的最新版本。 唉,,不過在現(xiàn)實(shí)中情況卻不是那樣,。千百萬行的代碼都是用早期版本的語言 來編寫的,不可能把它們?nèi)吭谝灰怪g就轉(zhuǎn)換過來,。 在后面的第10部分,,我們將會解決把遺留代碼轉(zhuǎn)為用泛型這個(gè)問題。在這部分 我們要看的是比較簡單的問題:遺留代碼與泛型代碼如何交互?這個(gè)問題分為兩個(gè) 部分:在泛型代碼中使用遺留代碼和在遺留代碼中使用泛型代碼,。 免費(fèi)linux公開課,,,現(xiàn)在報(bào)名! 六-1 在泛型代碼中使用遺留代碼 [url=http://xoj.][url] 當(dāng)你在享受在代碼中使用泛型帶來的好處的時(shí)候,你怎么樣使用遺留代碼呢,? 假設(shè)這樣一個(gè)例子,,你要使用com.Foodlibar.widgets這個(gè)包。Fooblibar.com 的人要銷售一個(gè)庫存控制系統(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ù)恰當(dāng) 的addAssembly()方法,,就是說傳遞的集合是一個(gè)Part類型的Collection對象,當(dāng) 然,,泛型是最適合做這個(gè): 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(); } } 當(dāng)我們調(diào)用addAssembly方法的時(shí)候,,它想要的第二個(gè)參數(shù)是Collection類型的, 實(shí)參是Collection<Part>類型,,但卻可以,,為什么呢?畢竟,,大多數(shù)集合存儲的都不是 Part對象,,所以總的來說,編譯器不會知道Collection存儲的是什么類型的集合,。 在正規(guī)的泛型代碼里面,,Collection都帶有類型參數(shù)。當(dāng)一個(gè)像Collection這樣 的泛型不帶類型參數(shù)使用的時(shí)候,,稱之為原生類型,。 很多人的第一直覺是Collection就是指Collection<Object>,,但從我們先前所 看到的可以知道,,當(dāng)需要的對象是Collection<Object>,,而傳遞的卻是Collection<Part> 對象的時(shí)候,,是類型不安全的,。確切點(diǎn)的說法是Collection類型表示一個(gè)未知類型的 集合,,就像Collection<?>,。 稍等一下,那樣做也是不正確的,!考慮一下調(diào)用getParts()方法,,它返回一個(gè) Collection對象,然后賦值給k,,而k是Collection<Part>類型的;如果調(diào)用的結(jié)果 是返回一個(gè)Collection<?>的對象,,這個(gè)賦值可能是錯(cuò)誤的,。 事實(shí)上,這個(gè)賦值是允許的,,只是它會產(chǎn)生一個(gè)未檢測警告,。警告是需要的,因?yàn)?br> 編譯器不能保證賦值的正確性,。我們沒有辦法通過檢測遺留代碼中的getAssembly()方法 來保證返回的集合的確是一個(gè)類型參數(shù)是Part的集合,。程序里面的類型是Collection, 我們可以合法的對此集合插入任何對象,。 所以,,這不應(yīng)該是錯(cuò)誤的嗎?理論上來說,,答案是:是,;但實(shí)際上如果是泛型代碼 調(diào)用遺留代碼的話,這又是允許的,。對這個(gè)賦值是否可接受,,得取決于程序員自己,在 這個(gè)例子中賦值是安全的,,因?yàn)間etAssembly()方法約定是返回以Part作為類型參數(shù)的 集合,盡管在類型標(biāo)記中沒有表明,。 所以原生類型很像通配符類型,,但它們沒有那么嚴(yán)格的類型檢測。這是有意設(shè)計(jì)成 這樣的,,從而可以允許泛型代碼可以與之前已有的遺留代碼交互,。 在泛型代碼中調(diào)用遺留代碼固然是危險(xiǎn)的,一旦把泛型代碼和非泛型代碼混合在一 起,,泛型系統(tǒng)所提供的全部安全保證就都變得無效了,。但這仍比根本不使用泛型要好, 最起碼你知道你的代碼是一致的,。 泛型代碼出現(xiàn)的今天,,仍然有很多非泛型代碼,二者混合同時(shí)使用是不可避免的。 如果一定要把遺留代碼與泛型代碼混合使用,,請小心留意那些未檢測警告,。仔細(xì)的 想想如何才能判定引發(fā)警告的代碼是安全的。 如果仍然出錯(cuò),,代碼引發(fā)的警告實(shí)際不是類型安全的,,那又怎么樣呢?我們會看 那樣的情況,,接下來,,我們將會部分的觀察編譯器的工作方式。 免費(fèi)linux公開課,,,現(xiàn)在報(bào)名! 六-2 擦除和翻譯 public String loophole(Integer x){ List<String> ys = new LinkedList<String>(); List xs = ys; xs.add(x);//編譯時(shí)未檢測警告 return ys.iterator().next(); } 在這里我們定義了一個(gè)字符串類型的鏈表和一個(gè)一般的老式鏈表,,我們先插入 一個(gè)Integer對象,然后試圖取出一個(gè)String對象,,很明顯這是錯(cuò)誤的,。如果我們 忽略警告繼續(xù)執(zhí)行代碼的話,程序?qū)谖覀兪褂缅e(cuò)誤類型的地方出錯(cuò),。在運(yùn)行時(shí),, 代碼執(zhí)行大致如下: public String loophole(Integer x) { List ys = new LinkedList; List xs = ys; xs.add(x); return (String)ys.iterator().next();//運(yùn)行時(shí)出錯(cuò) } 當(dāng)我們要從鏈表中取出一個(gè)元素,并把它當(dāng)作是一個(gè)字符串對象而把它轉(zhuǎn)換為 String類型的時(shí)候,,我們將會得到一個(gè)ClassCastException類型轉(zhuǎn)換異常,。在 泛型版本的loophole()方法里面發(fā)生的就是這種情況。 出現(xiàn)這種情況的原因是,,Java的泛型是通過一個(gè)前臺轉(zhuǎn)換“擦除”的編譯器實(shí)現(xiàn) 的,,你基本上可以認(rèn)為它是一個(gè)源碼對源碼的翻譯,這就是為何泛型版的loophole() 方法轉(zhuǎn)變?yōu)榉欠盒桶姹镜脑颉?br> 結(jié)果是,,Java虛擬機(jī)的類型安全性和完整性永遠(yuǎn)不會有問題,,就算出現(xiàn)未檢測 的警告。 基本上,,擦除會除去所有的泛型信息,。尖括號里面的所有類型信息都會去掉,比 如,,參數(shù)化類型的List<String>會轉(zhuǎn)換為List,。類型變量在之后使用時(shí)會被類型 變量的上界(通常是Object)所替換,。當(dāng)最后代碼不是類型正確的時(shí)候,,就會加入 一個(gè)適當(dāng)?shù)念愋娃D(zhuǎn)換,就像loophole()方法的最后一行,。 對“擦除”的完整描述不是本指南的范圍內(nèi)的內(nèi)容,,但前面我們所給的簡單描述 也差不多是那樣了。了解這點(diǎn)很有好處,特別是當(dāng)你想做諸如把現(xiàn)有API轉(zhuǎn)為使用 泛型(請看第10部分)這樣復(fù)雜的東西,,或者是想知道為什么它們會那樣的時(shí)候,。 免費(fèi)linux公開課,,現(xiàn)在報(bào)名! 六-3 在遺留代碼中使用泛型 現(xiàn)在我們來看看相反的情況,。假設(shè)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(); } } 客戶代碼是在引進(jìn)泛型之前寫下的,,但是它使用了com.Fooblibar.widgets包和集 合庫,,兩個(gè)現(xiàn)在都是在用泛型的。在客戶代碼里面使用的泛型全部都是原生類型,。 第1行產(chǎn)生一個(gè)未檢測警告,,因?yàn)榘岩粋€(gè)原生Collection傳遞給了一個(gè)需要Part類型的 Collection的地方,編譯器不能保證原生的Collection是一個(gè)Part類型的Collection,。 不這樣做的話,,你也可以在編譯客戶代碼的時(shí)候使用source 1.4這個(gè)標(biāo)記來保證不 會產(chǎn)生警告。但是這樣的話你就不能使用所有JDK 1.5引入的新的語言特性,。 免費(fèi)linux公開課,,,現(xiàn)在報(bào)名! 七、晦澀難懂的部分 七-1 泛型類為所有調(diào)用所共享 下面的代碼段會打印出什么呢,? List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass()); 你可能會說是false,,但是你錯(cuò)了,打印的是true,,因?yàn)樗蟹盒皖惖膶?shí)例它們 的運(yùn)行時(shí)的類(run-time class)都是一樣的,,不管它們實(shí)際類型參數(shù)如何。 泛型類之所以為泛型的,,是因?yàn)樗鼘λ锌赡艿念愋蛥?shù)都有相同的行為,,相同 的類可以看作是有很多不同的類型。 結(jié)果就是,,一個(gè)類的靜態(tài)的變量和方法也共享于所有的實(shí)例中,,這就是為什么不 允許在靜態(tài)方法或初始化部分、或者在靜態(tài)變量的聲明或初始化中引用類型參數(shù),。 免費(fèi)linux公開課,,,現(xiàn)在報(bào)名! 七-2 強(qiáng)制類型轉(zhuǎn)換和instanceof 泛型類在它所有的實(shí)例****享,就意味著判斷一個(gè)實(shí)例是否是一個(gè)特別調(diào)用的泛 型的實(shí)例是毫無意義的: Collection cs = new ArrayList<String>(); if (cs instanceof Collection<String>) {...}//非法 類似地,,像這樣的強(qiáng)制類型轉(zhuǎn)換: Collection<String> cstr = (Collection<String>) cs;//未檢測警告 給出了一個(gè)未檢測的警告,,因?yàn)檫@里系統(tǒng)在運(yùn)行時(shí)并不會檢測。 對于類型變量也一樣: <T> T BadCast(T t, Object o) { return (T) o;//未檢測警告 } 類型變量不存在于運(yùn)行時(shí),,這就是說它們對時(shí)間或空間的性能不會造成影響,。 但也因此而不能通過強(qiáng)制類型轉(zhuǎn)換可靠地使用它們了,。 免費(fèi)linux公開課,,現(xiàn)在報(bào)名! 七-3 數(shù)組 數(shù)組對象的組件類型可能不是一個(gè)類型變量或一個(gè)參數(shù)化類型,,除非它是一個(gè) ?。o界的)通配符類型。你可以聲明元素類型是類型變量和參數(shù)華類型的數(shù)組類型,, 但元素類型不能是數(shù)組對象,。 這自然有點(diǎn)郁悶,但這個(gè)限制對避免下面的情況是必要的: List<Strign>[] lsa = new List<String>[10];//實(shí)際上是不允許的 Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(8)); oa[1] = li;//不合理,,但可以通過運(yùn)行時(shí)的賦值檢測 String s = lsa[1].get(0);//運(yùn)行時(shí)出錯(cuò):ClassCastException異常 如果參數(shù)化類型的數(shù)組允許的話,,那么上面的例子編譯時(shí)就不會有未檢測的警告, 但在運(yùn)行時(shí)出錯(cuò),。對于泛型編程,,我們的主要設(shè)計(jì)目標(biāo)是類型安全,而特別的是這個(gè) 語言的設(shè)計(jì)保證了如果使用了javac -source 1.5來編譯整個(gè)程序而沒有未檢測的 警告的話,,它是類型安全的,。 但是你仍然會使用通配符數(shù)組,這與上面的代碼相比有兩個(gè)變化,。首先是不使用 數(shù)組對象或元素類型被參數(shù)化的數(shù)組類型,,這樣我們就需要在從數(shù)組中取出一個(gè)字符 串的時(shí)候進(jìn)行強(qiáng)制類型轉(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);//運(yùn)行時(shí)錯(cuò)誤,,顯式強(qiáng)制類型轉(zhuǎn)換 第二個(gè)變化是,,我們不創(chuàng)建元素類型被參數(shù)化的數(shù)組對象,但仍然使用參數(shù)化元素 類型的數(shù)組類型,,這是允許的,,但引起現(xiàn)未檢測警告。這樣的程序?qū)嶋H上是不安全的,, 甚至最終會出錯(cuò),。 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);//運(yùn)行出錯(cuò),,但之前已經(jīng)被警告 類似地,,想創(chuàng)建一個(gè)元素類型是類型變量的數(shù)組對象的話,將會編譯出錯(cuò),。 <T> T[] makeArray(T t){ return new T[100];//錯(cuò)誤 } 因?yàn)轭愋妥兞坎⒉淮嬖谟谶\(yùn)行時(shí),,所以沒有辦法知道實(shí)際的數(shù)組類型是什么。 要突破這類限制,,我們可以用第8部分說到的用類名作為運(yùn)行時(shí)標(biāo)記的方法,。 免費(fèi)linux公開課,,現(xiàn)在報(bào)名! 八,、 把類名作為運(yùn)行時(shí)的類型標(biāo)記 JDK1.5中的一個(gè)變化是java.lang.Class是泛化的,,一個(gè)有趣的例子是對 容器外的東西使用泛型。 現(xiàn)在Class類有一個(gè)類型參數(shù)T,,你可能會問,,T代表什么啊,?它就代表Class 對象所表示的類型,。 比如,String.class的類型是Class<String>,,Serializable.class的 類型是Class<Serializable>,,這可以提高你的反射代碼中的類型安全性。 特別地,,由于現(xiàn)在Class類中的newInstance()方法返回一個(gè)T對象,,因此 在通過反射創(chuàng)建對象的時(shí)候可以得到更精確的類型。 其中一個(gè)方法就是顯式傳入一個(gè)factory對象,,代碼如下: interface Factory<T> {T make();} public <T> Collection<T> select(Factory<T> factory, String statement){ Collection<T> result = new ArrayList<T>(); //用JDBC運(yùn)行SQL查詢 for(/*遍歷JDBC結(jié)果*/){ T item = factory.make(); /*通過SQL結(jié)果用反射和設(shè)置數(shù)據(jù)項(xiàng)*/ result.add(item); } return result; } 你可以這樣調(diào)用: select(new Factory<EmpInfo>(){ public EmpInfo make() { return new EmpInfo(); }} , "selection string"); 或者聲明一個(gè)EmpInfoFactory類來支持Factory接口: class EmpInfoFactory implements Factory<EmpInfo>{ ... public EmpInfo make() { return new EmpInfo();} } 然后這樣調(diào)用: select(getMyEmpInfoFactory(), "selection string"); 這種解決辦法需要下面的其中之一: · 在調(diào)用的地方使用詳細(xì)的匿名工廠類(verbose anonymous factory classes),,或者 · 為每個(gè)使用的類型聲明一個(gè)工廠類,并把工廠實(shí)例傳遞給調(diào)用的地方,,這樣有點(diǎn)不自然,。 使用類名作為一個(gè)工廠對象是非常自然的事,這樣的話還可以為反射所用?,F(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è)置數(shù)據(jù)項(xiàng)*/ 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è)置數(shù)據(jù)項(xiàng)*/ result.add(item); } return result; } 這樣就通過類型安全的方法來得到了精確的集合類型了,。 這種使用類名作為運(yùn)行時(shí)類型標(biāo)記的技術(shù)是一個(gè)很有用的技巧,是需要知道的,。 在處理注釋的新的API中也有很多類似的情況,。 免費(fèi)linux公開課,,現(xiàn)在報(bào)名! 九 通配符的其他作用 ?。╩ore fun with wildcards,,不知道如何譯才比較妥當(dāng),呵呵,。) 在這部分,,我們將會仔細(xì)看看通配符的幾個(gè)較為深入的用途。我們已經(jīng)從幾個(gè) 有界通配符的例子中看到,,它對從某一數(shù)據(jù)結(jié)構(gòu)中讀取數(shù)據(jù)是很有用的?,F(xiàn)在來看 看相反的情況,只對數(shù)據(jù)結(jié)構(gòu)進(jìn)行寫操作,。 下面的Sink接口就是這類情況的一個(gè)簡單的例子: interface Sink<T> { flush(T t); } 我們可以想象在下面的示范的例子中使用它,,writeAll()方法用于把coll集合 里的所有元素填充(flush)到Sink接口變量snk中,,并返回最后一個(gè)填充的元素。 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)用是非法的,,因?yàn)闊o有效的類型參數(shù) 可以引用;String和Object都不適合作為T的類型,,因?yàn)镃ollection和Sink的元素 必須是相同類型的,。 我們可以通過使用通配符來改寫writeAll()的方法頭來處理,如下: public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...} ... String str = writeAll(cs, s);//調(diào)用沒問題,,但返回類型錯(cuò)誤 現(xiàn)在調(diào)用是合法的了,,但由于T的類型跟元素類型是Object的s一樣,因?yàn)榉祷氐?br> 類型也是Object,,因此賦值是不正確的,。 解決辦法是使用我們之前從未見過的一種有界通配符形式:帶下界的通配符。 語法 ? super T 表示了是未知的T的父類型,,這與我們之前所使用的有界 ?。ǜ割愋停夯蛘逿類型本身,要記住的是,,你類型關(guān)系是自反的) 通配符是對偶有界通配符,,即用 ? extends T 表示未知的T的子類型。 public static<T> T writeAll(Collection<T> coll, Sink<? super T> snk) {...} ... String str = writeAll(cs, s);//正確,! 使用這個(gè)語法的調(diào)用是合法的,,指向的類型是所期望的String類型。 現(xiàn)在我們來看一個(gè)比較現(xiàn)實(shí)一點(diǎn)的例子,,java.util.TreeSet<E>表示元素類型 是E的樹形數(shù)據(jù)結(jié)構(gòu)里的元素是有序的,,創(chuàng)建一個(gè)TreeSet對象的一個(gè)方法是使用參數(shù) 是Comparator對象的構(gòu)造函數(shù),Comparator對象用于對TreeSet對象里的元素進(jìn)行 所期望的排序進(jìn)行分類,。 TreeSet(Comparator<E> c) Comparator接口是必要的: interface Comparator<T> { int compare(T fst, T snd); } 假設(shè)我們想要?jiǎng)?chuàng)建一個(gè)TreeSet<String>對象,,并傳入一下合適的Comparator 對象,我們傳遞的Comparator是能夠比較字符串的,。我們可以用Comparator<String>,, 但Comparator<Object>也是可以的。但是,,我們不能對Comparator<Object>對象 調(diào)用上面所給的構(gòu)造函數(shù),,我們可以用一個(gè)下界通配符來得到我們想要的靈活性: TreeSet(Comparator<? super E> c) 這樣就可以使用適合的Comparator對象啦。 最后一個(gè)下界通配符的例子,,我們來看看Collections.max()方法,,這個(gè)方法 返回作為參數(shù)傳遞的Collection對象中最大的元素。 現(xiàn)在,,為了max()方法能正常運(yùn)行,,傳遞的Collection對象中的所有元素都必 須是實(shí)現(xiàn)了Comparable接口的,,還有就是,它們之間必須是可比較的,。 先試一下泛化方法頭的寫法: public static <T extends Comparable<T>> T max(Collection<T> coll) 那樣,,方法就接受一個(gè)自身可比較的(comparable)某個(gè)T類型的Collection 對象,并返回T類型的一個(gè)元素,。這樣顯得太束縛了,。 來看看為什么,,假設(shè)一個(gè)類型可以與合意的對象進(jìn)行比較: class Foo implements Comparable<Object> {...} ... Collection<Foo> cf = ...; Collectins.max(cf);//應(yīng)該可以正常運(yùn)行 cf里的每個(gè)對象都可以和cf里的任意其他元素進(jìn)行比較,,因?yàn)槊總€(gè)元素都是Foo 的對象,而Foo對象可以與任意的對象進(jìn)行比較,,特別是同是Foo對象的,。但是,使用 上面的方法頭,,我們會發(fā)現(xiàn)這樣的調(diào)用是不被接受的,,指向的類型必須是Foo,但Foo 并沒有實(shí)現(xiàn)Comparable<Foo>,。 T對于自身的可比性不是必須的,,需要的是T與其父類型是可比的,就像下面: ?。▽?shí)際的Collections.max()方法頭在后面的第10部分將會講得更多) public static <T extends Comparable<? super T>> T max(Collection<T> coll) 這樣推理出來的結(jié)果基本上適用于想用Comparable來用于任意類型的用法: 就是你想這樣用Comparable<? super T>,。 總的來說,如果你有一個(gè)只能一個(gè)T類型參數(shù)作為實(shí)參的API的話,,你就應(yīng)該用 下界通配符類型(? suer T),;相反,如果API只返回T對象,,你就應(yīng)該用上界通 配符類型(? extends T),,以使得你的客戶的代碼有更大的靈活性。 |
|