John Jiang

          a cup of Java, cheers!
          https://github.com/johnshajiang/blog

             :: 首頁(yè) ::  :: 聯(lián)系 :: 聚合  :: 管理 ::
            131 隨筆 :: 1 文章 :: 530 評(píng)論 :: 0 Trackbacks
          Java Tutorials -- Generics
          Java Generics伴隨JDK 5.0發(fā)布到現(xiàn)在已經(jīng)超過(guò)2年半了,但目前還沒(méi)有被"非常廣泛"地應(yīng)用,我也一直沒(méi)有進(jìn)行過(guò)系統(tǒng)的學(xué)習(xí)。最近使用Thinking in Java(4th)和Java Tutorials對(duì)泛型進(jìn)行了專門的學(xué)習(xí)。本文是對(duì)Java Tutorials中Generics一章的翻譯。其實(shí)關(guān)于Java Generics的文章已是汗牛充棟,之所以將這篇譯文放在此處,也算是對(duì)自己學(xué)習(xí)的一種鼓勵(lì)吧。該文的讀者應(yīng)該只有我一人,但仍然希望對(duì)其他朋友有所助益。(2007.07.10最后更新)

          1 介紹
              JDK 5.0引進(jìn)了幾種Java程序設(shè)計(jì)語(yǔ)言的新擴(kuò)展。其中之一,就是對(duì)泛型的引入。
              本次體驗(yàn)只是對(duì)泛型的介紹。你可能通過(guò)其它的語(yǔ)言,特別是C++ Template,已經(jīng)對(duì)泛型的結(jié)構(gòu)有些熟悉了。如果是這樣的話,你將看到它們的相似點(diǎn)和重要的不同點(diǎn)。如果你對(duì)從別處看到的這種似曾相識(shí)的結(jié)構(gòu)不熟悉的話,那就更好了,你可以從頭開(kāi)始,以避免不得不忘卻一些誤解。
              泛型允許你抽象出類型。最普通的例子就是容器類型,如集合框架(Collection)中的那些類。
              下面是一個(gè)此類特性的典型使用:
              List myIntList = new LinkedList(); // 1
              myIntList.add(new Integer(0)); // 2
              Integer x = (Integer) myIntList.iterator().next();  // 3        
              第三行的強(qiáng)制類型轉(zhuǎn)換有點(diǎn)煩人。基本上,程序員知道到底是什么類型的數(shù)據(jù)被放到這個(gè)特定的List中了。然而,這個(gè)強(qiáng)制類型轉(zhuǎn)換是必需的。編譯器只能保證迭代器將返回的是一個(gè)對(duì)象。為了確保一個(gè)類型為Integer的變量x是類型安全的,這個(gè)強(qiáng)制類型轉(zhuǎn)換是需要的。
              當(dāng)然,這個(gè)強(qiáng)制類型轉(zhuǎn)換并不會(huì)造成混亂。它仍然可能會(huì)造成一個(gè)運(yùn)行時(shí)錯(cuò)誤,可能是由程序員的失誤而產(chǎn)生的。
              那么程序員如何才能準(zhǔn)確地表達(dá)他們的本意,使得一個(gè)List被限制為只能包含某個(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對(duì)象的List,寫作List<Integer>。我們說(shuō),List是一個(gè)擁有類型參數(shù),在此處就是Integer,的泛型接口。當(dāng)創(chuàng)建這個(gè)List對(duì)象時(shí),我們也指定了一個(gè)類型參數(shù)。
              再次注意,原來(lái)行3的的強(qiáng)制類型轉(zhuǎn)換已經(jīng)不需要了。
              現(xiàn)在你可能會(huì)想我們所已經(jīng)完成的就是移除了那個(gè)混亂(強(qiáng)制類型轉(zhuǎn)換)。我們?cè)谛?處就使Integer成為一個(gè)類型參數(shù),而不是在行3處進(jìn)行強(qiáng)制類型轉(zhuǎn)換。這兒就有一個(gè)很大的不同。在編譯時(shí),編譯器就能夠檢查程序中的類型是否正確。當(dāng)我們說(shuō)myIntList在聲明時(shí)使用了類型List<Integer>,那么就是告訴我們myIntList變量在任何時(shí)間和任何地點(diǎn)所包含的類型必須是Integer,并且編譯器會(huì)確保這一點(diǎn)。相反地,強(qiáng)制類型轉(zhuǎn)換只是告訴我們?cè)诖a中的某個(gè)獨(dú)立的地方程序員所期望的情況而以。
              在實(shí)際情況下,特別是在大型應(yīng)用中,泛型可以提高程序的可讀性和魯棒性。

          2 定義簡(jiǎn)單的泛型
              下面是java.util包中List和Iterator接口定義的簡(jiǎn)短摘要:
              public interface List <E>{
                  void add(E x);
                  Iterator<E> iterator();
              }

              public interface Iterator<E>{
                  E next();
                  boolean hasNext();
              }
          除了角括號(hào)中的內(nèi)容,我們對(duì)這段代碼應(yīng)該比較熟悉了。這些是List和Iterator接口的形式類型參數(shù)的聲明。
              類型參數(shù)的使用可以貫穿于整個(gè)泛型聲明,用在那些你以后想使用普通類型的地方(但有一些重要的約束,詳見(jiàn)"良好的打印"一節(jié))。
              在"介紹"一節(jié)中,我們知道了使用了泛型類型聲明的List接口的調(diào)用方法,如List<Integer>。在這個(gè)調(diào)用(一般就是調(diào)用一個(gè)參數(shù)化的類型)中,所有形式類型參數(shù)(即此處的E)出現(xiàn)的地方都被實(shí)際的類型參數(shù)(即此處的Integer)替換了。
              你可能會(huì)想像List<Integer>表示一種由Integer統(tǒng)一地代替E之后的新的List版本:
              public interface IntegerList {
                  void add(Integer x);
                  Iterator<Integer> iterator();
              }
          這種直覺(jué)是有助益的,但那也是誤解。
              說(shuō)它是有助益的,是因?yàn)閰?shù)類型List<Integer>實(shí)際上所使用的方法看起來(lái)就是像那種擴(kuò)展。
              說(shuō)它是誤解,是因?yàn)榉盒偷穆暶鞔_實(shí)沒(méi)有用那種方式進(jìn)行擴(kuò)展。并不存在那些代碼的多個(gè)復(fù)本,在源文件中、二進(jìn)制文件中、硬盤中、內(nèi)存中都沒(méi)有這些復(fù)本。如果你是C++程序員,你將會(huì)發(fā)現(xiàn)這與C++ Template非常的不同。
              泛型類型的聲明絕對(duì)只會(huì)被編譯一次,然后進(jìn)入一個(gè)class文件中,就像一個(gè)普通的類或接口聲明一樣。
              類型參數(shù)類似于方法或構(gòu)造器中的普通參數(shù)。它非常像一個(gè)方法擁有一個(gè)形式值參數(shù),這個(gè)參數(shù)描述了可以出現(xiàn)在該處的值的類型,泛型聲明也有一個(gè)形式類型參數(shù)。當(dāng)一個(gè)方法被調(diào)用時(shí),一個(gè)真實(shí)的的參數(shù)會(huì)替換形式參數(shù),然后這個(gè)方法會(huì)進(jìn)行評(píng)估。當(dāng)一個(gè)泛型聲明被調(diào)用時(shí),一個(gè)真實(shí)的類型參數(shù)也會(huì)替代形式類型參數(shù)。
              需要注重一個(gè)命名規(guī)范。我們推薦你使用叫起來(lái)盡量精簡(jiǎn)的(如果可能的話,最好是單個(gè)字母)的名字作為形式類型參數(shù)。最好避免使用小寫字母,這樣就可以很容易地從普通的類和接口中區(qū)分出形式類型參數(shù)。如上述例子中,很多容器類型使用E代表容器中的元素(element)。

          3 泛型與子類
              讓我們測(cè)試一下你對(duì)泛型的理解。下面的代碼片斷是合法的嗎?
              List<String> ls = new ArrayList<String>(); // 1
              List<Object> lo = ls; // 2
              第一行肯定是合法的。這個(gè)問(wèn)題狡猾的部分是在第二行。這個(gè)問(wèn)題可歸結(jié)為:一個(gè)String對(duì)象的List也是Object對(duì)象的List嗎?大部分人都會(huì)本能的回答到,是的!
              那好,來(lái)看看下面幾行:
              lo.add(new Object()); // 3
              String s = ls.get(0); // 4: 試圖將一個(gè)Object對(duì)象賦值給一個(gè)String變量!
              此處我們已經(jīng)別名化了ls和lo。通過(guò)別名lo訪問(wèn)ls,一個(gè)String對(duì)象的List,我們可以向其中插入任意對(duì)象。但ls不能包含除String對(duì)象外的其它對(duì)象,則當(dāng)我們?cè)噲D從中獲得些什么(Object對(duì)象)時(shí),我們會(huì)感到非常的驚訝。
              譯者:上面這段話的意思是說(shuō),如果上述4行代碼都成立的話,那么就會(huì)使我們感到很驚訝、很困惑。lo的類型是List<Object>,那么可以放入任意的Object到這個(gè)List中;而ls的類型是List<String>,即只能放入String對(duì)象。但lo引用的對(duì)象實(shí)際上是ArrayList<String>的對(duì)象,即只能存放String對(duì)象,所以上面的例子會(huì)使人感到很困惑。
              當(dāng)然,Java編譯器會(huì)阻止這一切的發(fā)生--第二行將會(huì)導(dǎo)致一個(gè)編譯時(shí)錯(cuò)誤。
              一般地,如果Foo是Bar的子類型(子類或子接口),且G是某個(gè)泛型類型聲明,那么G<Foo>并不是G<Bar>的子類型。這可能是當(dāng)你學(xué)習(xí)泛型時(shí)所遇到的最困難的問(wèn)題,因?yàn)檫@違反了我們根深蒂固的直覺(jué)。
              我們不能假設(shè)集成對(duì)象們不會(huì)改變。我們的直覺(jué)可能會(huì)導(dǎo)致我們靜態(tài)地思考這些問(wèn)題。
              例如,如果機(jī)動(dòng)車管理部(Department of Motor Vehicles, DMV)向人口調(diào)查局(Census Bureau)提交了一組司機(jī)的名單,這會(huì)被看成是合理的,因?yàn)槲覀冋J(rèn)為L(zhǎng)ist<Driver>是List<Person>的子類型(假設(shè)Driver是Person的子類型)。實(shí)際上被提交的只是司機(jī)注冊(cè)表的副本。否則,人口調(diào)查局也可以把那些不是司機(jī)的人也加入到這個(gè)名單 (List)中,這就會(huì)破壞DMV的記錄。
              為了應(yīng)對(duì)這種情況,有必要考慮更為彈性的泛型類型。我們到目前為止所看到的規(guī)則實(shí)在是太具限制性了。

          4 通配符
              考慮這樣一個(gè)問(wèn)題,寫一個(gè)程序打印出一個(gè)集合對(duì)象中的所有元素。下面的程序可能是你用老版Java語(yǔ)言所寫的:
              void printCollection(Collection c) {
                  Iterator i = c.iterator();
                  for (k = 0; k < c.size(); k++) {
                      System.out.println(i.next());
                  }
              }
              這兒有一個(gè)不成熟的對(duì)泛型應(yīng)用的嘗試(并且使用了新的foreach循環(huán)語(yǔ)法):
              void printCollection(Collection<Object> c) {
                  for (Object e : c) {
                      System.out.println(e);
                  }
              }
              這個(gè)問(wèn)題就是新版的程序并不比舊版的程序更有用。反之,舊版的程序能夠作為參數(shù)被任何類型的集合對(duì)象調(diào)用,新版的程序只能用于Collection<Object>,而這種情況已經(jīng)被我們證明了,它并不是所有集合類型的超類。
              那么什么才是所有集合對(duì)象的超類呢?它應(yīng)該寫作Collection<?>(叫作"collection of unknow,未知的集合"),這種集合類型的元素才可能配置任何類型。很明顯,它被稱作通配符類型。我們可以這樣寫:
              void printCollection(Collection<?> c) {
                  for (Object e : c) {
                      System.out.println(e);
                  }
              }
              然后我們就可以用任何集合類型來(lái)調(diào)用這個(gè)方法了。注意printCollection方法的內(nèi)部,我們?nèi)匀豢梢詮腸中讀取它的元素,并可將這些元素賦值給Object類型的變量。
                  Collection<?> c = new ArrayList<String>();
                  c.add(new Object()); // Compile time error
              由于不知道c中元素的類型是什么,我們不能向它里面添加元素。add方法接受類型E的參數(shù),即這個(gè)集合對(duì)象元素的類型。當(dāng)然實(shí)際的類型參數(shù)是"?"時(shí),它表示某個(gè)未知的類型。任何我們要添加入的參數(shù)都將不得不是未知類型的子類型。由于我們不知道這個(gè)類型是什么,所以我們不能傳入任何類型。唯一的例外是 "null",null是每個(gè)類型的成員(譯者:null是每種類型的子類型。)。
              另一方面,給出一個(gè)List<?>,我們就能調(diào)用get方法并使用得到的結(jié)果。所得結(jié)果的類型是未知的,但我們總可以知道它是一個(gè) Object對(duì)象。因此將由get方法得到的結(jié)果賦予一個(gè)Object類型的變量,或是將它作為一個(gè)參數(shù)傳入一個(gè)期望獲得Object類型對(duì)象的地方,都是完全的。
              有邊界的通配符
              考慮這樣的一個(gè)簡(jiǎn)單的繪圖程序,它可以繪制諸如矩形和環(huán)形之類的形狀。為了使用程序來(lái)描述這些形狀,你可能是會(huì)下面那樣定義一組類:
              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) {
                      ...
                  }
              }
              這些類可以被繪在一個(gè)畫(huà)布(canvas)上:
              public class Canvas {
                  public void draw(Shape s) {
                      s.draw(this);
                  }
              }
              任何繪制動(dòng)作通常都會(huì)包含一組形狀。假設(shè)使用List來(lái)表示它們,那么為方便起見(jiàn),Canvas需要有一個(gè)方法去繪制所有的形狀:
              public void drawAll(List<Shape> shapes) {
                  for (Shape s: shapes) {
                      s.draw(this);
                  }
              }
              現(xiàn)在,規(guī)則要求drawAll方法只能用于僅包含Shape對(duì)象的List,例如它不能用于List<Circle>。但不幸的是,由于所有的方法所做的只是從List中讀取Shape對(duì)象,所以它也需要能用于List<Circle>。我們所想要的就是這個(gè)方法能夠接受所有的 Shape類型。
              public void drawAll(List<? extends Shape> shapes) {
                  ...
              }
              這兒是一個(gè)很小但很重要的區(qū)別:我們已經(jīng)用List<? extends Shape>代替了List<Shape>?,F(xiàn)在,drawAll方法就可以接受Shape的任何子類對(duì)象的List了。
              List<? extends Shape>就是有邊界的通配符的一個(gè)例子。問(wèn)號(hào)(?)代表未知類型,就如我們之前所看到的這個(gè)通配符一樣。然而,在這個(gè)例子中,我們這個(gè)未知類型實(shí)際上是Shape類的子類。(注:它可以是Shape類型本身;無(wú)需按字面上的意義一定說(shuō)是Shape子類)。
              一般地,在使用通配符時(shí)要付出一些彈性方面的代價(jià)。這個(gè)代價(jià)就是,馬上向該方法體中寫入Shape類型的對(duì)象是非法的。例如,下面的代碼是不被允許的:
              public void addRectangle(List<? extends Shape> shapes) {
                  shapes.add(0, new Rectangle()); // Compile-time error!
              }
              你應(yīng)該會(huì)指出為什么上面的代碼是不能被接受的。shaps.add的第二個(gè)參數(shù)是"? extends Shape"--一個(gè)未知的Shape子類,由于我們不知道它會(huì)是哪個(gè)Shape類型,不知道它的超類是否就是Rectangle;它可能是,也可能不是 Rectangle的超類,所以當(dāng)傳遞一個(gè)Rectangle對(duì)象,并不安全。
             
          有邊界的通配符正是上一節(jié)中DMV向人口調(diào)查局提交數(shù)據(jù)的例子所需要的。我們的例子假設(shè)那些數(shù)據(jù)是由姓名(用字符串表示)到人(用Person或其子類,如 Driver,的引用類型表示)的映射表示。Map<K, V>是包含兩個(gè)類型參數(shù)的例子,這兩個(gè)類型參數(shù)分別表示映射中的鍵與值。
              再次注意形式類型參數(shù)的命名規(guī)范--K代表鍵,V代表值。
              public class Census {
                  public static void addRegistry(Map<String, ? extends Person> registry) {
              }
              ...

              Map<String, Driver> allDrivers = ... ;
              Census.addRegistry(allDrivers);

          5 泛型方法
              考慮寫一個(gè)方法,它包含一個(gè)Object數(shù)據(jù)和一個(gè)集合對(duì)象,它的作用是將數(shù)組中的對(duì)象全部插入到集合對(duì)象中。下面是第一次嘗試:
              static void fromArrayToCollection(Object[] a, Collection<?> c) {
                  for (Object o : a) {
                      c.add(o); // Compile time error
                  }
              }
              到現(xiàn)在為此,你要學(xué)會(huì)避免新手所犯的錯(cuò)誤--嘗試將Collection<Object>作為這個(gè)集合的類型參數(shù)。你可能認(rèn)識(shí)或沒(méi)認(rèn)識(shí)到使用 Collection<?>也不能完成工作。回憶一下,你不能將對(duì)象擠入一個(gè)未知類型的集合對(duì)象中。
              處理這些問(wèn)題的方法是使用泛型方法。就像類型的聲明一樣,方法的聲明也可以泛型化--即,用一個(gè)或多個(gè)參數(shù)去參數(shù)化這個(gè)方法。
              static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
                  for (T o : a) {
                      c.add(o); // Correct
                  }
              }
              我們能夠調(diào)用任意類型的集合對(duì)象中的方法,只要這個(gè)集合對(duì)象中的元素是數(shù)組類型中元素的超類型。
              Object[] oa = new Object[100];
              Collection<Object> co = new ArrayList<Object>();
              fromArrayToCollection(oa, co); // T inferred to be Object

              String[] sa = new String[100];
              Collection<String> cs = new ArrayList<String>();
              fromArrayToCollection(sa, cs); // T inferred to be String
              fromArrayToCollection(sa, co); // T inferred to be Object

              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 inferred to be Number
              fromArrayToCollection(fa, cn); // T inferred to be Number
              fromArrayToCollection(na, cn); // T inferred to be Number
              fromArrayToCollection(na, co); // T inferred to be Object

              fromArrayToCollection(na, cs); // compile-time error
              注意我們并不需要傳遞一個(gè)確切的類型給泛型方法。編譯器會(huì)根據(jù)準(zhǔn)確的參數(shù)的類型幫我們推斷出實(shí)際類型參數(shù)。編譯器通常會(huì)推斷出大部分的特定類型參數(shù),這就使得對(duì)方法的調(diào)用是類型正確的。
              產(chǎn)生了一個(gè)問(wèn)題:什么時(shí)候我應(yīng)該使用泛型方法,什么時(shí)候我應(yīng)用使用通配符類型?為了理解答案,讓我們測(cè)試一些集合框架類庫(kù)中的方法:
              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 <T extends E> boolean addAll(Collection<T> c);
                  // Hey, type variables can have bounds too!
              }
              然而,在兩個(gè)containAll和addAll方法中,類型參數(shù)T只被使用了一次。返回類型既不依賴類型參數(shù),也不需要傳遞其它的參數(shù)給這個(gè)方法(在本例中,只不過(guò)是一個(gè)實(shí)參罷了)。這就告訴我們?cè)搶?shí)參將用于多態(tài);它的僅有的作用就是允許該方法的多種不同的實(shí)參能夠應(yīng)用于不同的調(diào)用點(diǎn)。
              泛型方法允許類型參數(shù)用于描述一個(gè)或多個(gè)實(shí)參的類型對(duì)于該方法和/或它的返回值之間依賴關(guān)系。如果沒(méi)有這種依賴關(guān)系,那么就不應(yīng)該使用泛型方法。
              一前一后的使用泛型方法和通配符是可能的,下面的方法Collections.copy()就表現(xiàn)了這一點(diǎn):    class Collections {
                  public static <T> void copy(List<T> dest, List<? extends T> src) {
                  ...
              }
              注意這兩個(gè)參數(shù)的類型之間的依賴關(guān)系。任何復(fù)制于源表scr的對(duì)象對(duì)于目標(biāo)表dest中元素的類型T都必須是可賦值的。所以src元素的類型肯定是T的任何子類型--我們不用關(guān)心這些。復(fù)制方法的簽名使用一個(gè)類型參數(shù)描述了這種依賴關(guān)系,但將通配符用于第二個(gè)參數(shù)中元素的類型。
              我們也可以使用另一種方法來(lái)書(shū)寫這個(gè)方法的簽名,這種方法完全不需要使用通配符:
              class Collections {
                  public static <T, S extends T>
                         void copy(List<T> dest, List<S> src) {
                  ...
              }
              這很好,但當(dāng)?shù)谝粋€(gè)類型參數(shù)在類型dest和第二個(gè)類型的限度中都使用了時(shí),S它那本身只被使用了一次,就是在src的類型中--沒(méi)任何其它的東西再依賴于它了。這就是一個(gè)我們要以使用通配符替換S的一個(gè)信號(hào)。使用通配符比顯示的聲明類型變量更加清晰、更加精確,所以在任何可能的時(shí)候通配符是首選。
              通配符也有它的優(yōu)點(diǎn),它可以被用于方法簽名的外面,以作為字段的類型,局部變量或數(shù)組。下面就是這樣的一個(gè)例子。
              回到我們繪制形狀的那個(gè)例子,假設(shè)我們想維護(hù)一個(gè)繪制形狀請(qǐng)求的歷史記錄。我們可以將這個(gè)歷史記錄維護(hù)在類Shape內(nèi)部的一個(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);
                  }
              }
              最后,仍然讓我們?cè)俅巫⒁忸愋妥兞康拿?guī)范。我們一般使用T表示類型,只要無(wú)需再區(qū)別任何其它的特定類型。這種情況經(jīng)常用于泛型方法中。如果有多個(gè)類型參數(shù),我可以使字母表中鄰近T的其它字母,例如S。如果在一個(gè)泛型類中有一個(gè)泛型方法,那么為了避免混淆,一個(gè)好的習(xí)慣是不要使泛型類和泛型方法有相同名字的類型參數(shù)。這也適用于嵌套泛型類。

          6 與遺留代碼交互
              到現(xiàn)在為止,我們的例子是假設(shè)處于一種理想的狀況,即每個(gè)人都在使用Java程序設(shè)計(jì)語(yǔ)言的支持泛型的最新版。
              唉,但現(xiàn)實(shí)并非如此。數(shù)以百萬(wàn)行計(jì)的代碼是用Java語(yǔ)言的早期版本寫的,而且也不可能在一夜之間就將它們轉(zhuǎn)換到新版中。
              稍后,在"使用泛型轉(zhuǎn)化遺留代碼"這一節(jié)中,我們將解決將你的舊代碼轉(zhuǎn)換到使用泛型這個(gè)問(wèn)題。在本節(jié),我們將關(guān)注一個(gè)簡(jiǎn)單的問(wèn)題:遺留代碼與泛型代碼之間如何交互?這個(gè)問(wèn)題含有兩個(gè)部分:在泛型代碼內(nèi)部使用遺留代碼;在遺留代碼內(nèi)部使用泛型代碼。
              作為一個(gè)例子,假設(shè)你想使用包c(diǎn)om.Fooblibar.widgets。分支Fooblibar.com*商用在一個(gè)資產(chǎn)管理系統(tǒng)中,這個(gè)系統(tǒng)的精華如下所示:
              package com.Fooblibar.widgets;

              public interface Part { ...}

              public 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 parts) {...}
                  public static Assembly getAssembly(String name) {...}
              }

              public interface Assembly {
                  Collection getParts(); // Returns a collection of Parts
              }
          現(xiàn)在,你要添加一些新的代碼并使用上述API。比較好的是,要確保你一直能夠使用適當(dāng)?shù)膶?shí)參去調(diào)用addAssembly方法--即,你傳入的集合對(duì)象必須是裝有Part對(duì)象的集合對(duì)象。當(dāng)然,泛型最適合做這些了:
              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<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ù)的實(shí)際類型是Collection< Part>。這是正確的,但是什么呢?畢竟,大部分的Collection是不能包含Part對(duì)象的,因?yàn)橐话銇?lái)說(shuō),編譯器無(wú)法知道該 Collection所表示的是哪種對(duì)象的集合對(duì)象。
              在合適的泛型代碼中,Collection將一直跟隨著一個(gè)類型參數(shù)。當(dāng)一個(gè)像Collection這樣的泛型類型在被使用時(shí)沒(méi)有提供類型參數(shù),就被稱之為原生類型(Raw Type)。
              大多數(shù)人的每一直覺(jué)認(rèn)為Collection就是Collection<Object>。然而,按我們之前所說(shuō)的,在需要Collection<Object>的地方使用Collection<Part>并不是安全的。
              但請(qǐng)等等,那也不對(duì)!想想對(duì)getParts對(duì)象的調(diào)用,它要返回一個(gè)Collection對(duì)象(實(shí)際上是一個(gè)引用變量)。然后這個(gè)對(duì)象被賦于變量k,k是 Collection<Part>類型。如果調(diào)用該方法而返回的結(jié)果是一個(gè)Collection<?>對(duì)象,該賦值操作也將產(chǎn)生錯(cuò)誤。
              事實(shí)上,該賦值操作是合法的,它會(huì)生產(chǎn)一個(gè)未檢查的警告。這個(gè)警告是必要的,因?yàn)槭聦?shí)上編譯器并不能保證它的正確性。我們沒(méi)辦法檢查 getAssembly方法中的遺留代碼以保證返回的集合對(duì)象Part對(duì)象的集合。被用于該代碼的類型是Collection,能夠合法的向這種 Collection中插入任何類型的對(duì)象。
              那么這還應(yīng)該是一個(gè)錯(cuò)誤嗎?就理論上而言,是的;但就實(shí)際上而言,如果泛型代碼是為了調(diào)用遺留代碼,那么就不得不允許了。對(duì)于你,一個(gè)程序員,會(huì)對(duì)這種情況感到滿意的,賦值是安全的,因?yàn)間etAssermbly方法的規(guī)則告訴我們它返回返回的是 Part對(duì)象的Collection,即使該方法的簽名并沒(méi)有表明這一點(diǎn)。
              所以原生類型非常像通配符類型,但它們不會(huì)被做嚴(yán)格的類型檢查。這是經(jīng)過(guò)深思熟慮之后的結(jié)果,是為了允許泛型代碼能夠與之前已存在的代碼交互使用。
              用泛型代碼調(diào)用遺留代碼是天生危險(xiǎn)的;一旦你在泛型代碼中混合了非泛型的遺留代碼,那么泛型類型系統(tǒng)通常都無(wú)法提供完全的保證。然而,這仍然比你不使用泛型要好些。至少你知道最終這些代碼是一致的。
              碰到那兒已經(jīng)有了很多的非泛型代碼,然后又有了泛型代碼的時(shí)候,那么無(wú)法避免的情況就是不得不混合它們。
              如果你發(fā)現(xiàn)你必須混合使用遺留代碼和泛型代碼,請(qǐng)密切注意未檢查的警告。要謹(jǐn)慎地思考你如何再才能證明那些被給出了危險(xiǎn)警告的代碼是安全的。
              當(dāng)你繼續(xù)犯錯(cuò)誤,且代碼造成的警告確實(shí)不是類型安全的,什么事情將發(fā)生呢?讓我們看看這樣的一種情況。在這個(gè)處理過(guò)程中,我們將觀察編譯器所做的事情。

          擦除和翻譯
              public String loophole(Integer x) {
                  List<String> ys = new LinkedList<String>();
                  List xs = ys;
                  xs.add(x); // Compile-time unchecked warning
                  return ys.iterator().next();
              }
          此處,我們已經(jīng)別名化了String的List和一個(gè)普通的老版的List。我們向這個(gè)List xs插入一個(gè)Integer對(duì)象,并試圖抽取一個(gè)String對(duì)象。這顯然是錯(cuò)的。如果我們忽略警告并嘗試執(zhí)行這段代碼,它將在我們?cè)噲D使用錯(cuò)誤類型的地方上失敗。
              public String loophole(Integer x) {
                  List ys = new LinkedList;
                  List xs = ys;
                  xs.add(x);
                  return(String) ys.iterator().next(); // run time error
              }
          當(dāng)我們從這個(gè)List中抽取一個(gè)元素,并試圖將它當(dāng)作String對(duì)象而把它轉(zhuǎn)換成String時(shí),我們將得到一個(gè)ClassCastException的異常。完全相同的情況也發(fā)生在了loophole方法的泛型版中。
              這種情況的原因就是泛型是由Java編譯器作為一種叫做"擦除(Erasure)"的最前到后的機(jī)制實(shí)現(xiàn)的。你(幾乎)可以把它想像為一種"源代碼對(duì)源代碼"(source-to-source)的翻譯,這就是為何loophole的泛型版被轉(zhuǎn)換成了非泛型版了。
              結(jié)果,Java虛擬機(jī)的類型安全和完整性再也不處于危險(xiǎn)中了,甚至在遇到到未檢查的警告時(shí)也一樣。
              基本地,Erasure去除(或者說(shuō)"擦除")了所有的泛型信息。所有的在角括號(hào)中的類型信息都被拋棄了,所以,如像List<String> 這樣的參數(shù)化類型被轉(zhuǎn)化成了List。所有保持對(duì)類型變量使用的地方都被類型變量的高層限度類型(一般就是Object)替換了。并且,無(wú)論何時(shí)產(chǎn)生的結(jié)果都不是類型正確的,一個(gè)向適當(dāng)?shù)念愋偷膹?qiáng)制類型轉(zhuǎn)換被插入了其中。
              對(duì)Erasure的全部細(xì)節(jié)的描述超出了本教程的范疇,但我們給出的簡(jiǎn)單描述離真實(shí)情況并不太遠(yuǎn)。了解一些這方面的知識(shí)是有益的,特別是如果你想做一些更加老練的泛型應(yīng)用,如把已有的API轉(zhuǎn)換到使用泛型時(shí)(詳見(jiàn)"使用泛型轉(zhuǎn)化遺留代碼"),或者只是想理解為什么它們會(huì)是這種情況。

              在遺留代碼中使用泛型代碼
              現(xiàn)在讓我們思考一個(gè)顛倒的例子。想像Foolibar.com選擇泛型去轉(zhuǎn)化了它們的API,但他們的一些客戶端程序還沒(méi)有轉(zhuǎn)化。所以這些代碼看起來(lái)像:
              package com.Fooblibar.widgets;

              public interface Part {
                  ...
              }

              public 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(); // Returns a collection of Parts
              }
          客戶端程序看起來(lái)像:
              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();
                  }
              }
              這些客戶端代碼是在泛型產(chǎn)生之前寫成的,但它使用了包c(diǎn)om.Fooblibar.widgets和集合框架類庫(kù),這兩者都在使用泛型??蛻舳酥袑?duì)泛型類型的使用使得它們成為了原生(Raw Type)類型。
              代碼行1產(chǎn)生了一個(gè)未檢查的警告,因?yàn)橐粋€(gè)原生Collection被傳入了一個(gè)期望是Collection<Part>出現(xiàn)的地方,而且編譯器無(wú)法保證這個(gè)原生Collection真的就是Part對(duì)象的Collection。
              作為一種可選的方法,你可以將這些代碼作為Java 1.4的源代碼進(jìn)行編譯,這就能保證不會(huì)出現(xiàn)警告。但這樣的話,你將不能使用到JDK 5.0中任何新的語(yǔ)言特性。
              --------------------------------------------------------------------------
              注意,"Fooblibar.com"是一個(gè)純屬虛構(gòu)的公司,目的僅僅只是為了本文中的例子。任何公司或機(jī)構(gòu)、任何健在或已故的個(gè)人與此有關(guān)的話,純屬巧合。
              譯者:看來(lái)老外做事情十分謹(jǐn)慎,對(duì)于這種"小問(wèn)題"我們又怎么會(huì)如此鄭重其事的發(fā)表一個(gè)聲明呢。

          7 良好的打印
              一個(gè)泛型類被它的所有應(yīng)用共享
              下面的代碼片斷是打印出什么呢?
              List <String> l1 = new ArrayList<String>();
              List<Integer> l2 = new ArrayList<Integer>();
              System.out.println(l1.getClass() == l2.getClass());
              你可能會(huì)被引誘得說(shuō)是false,但你錯(cuò)了。打印的是true,因?yàn)橐粋€(gè)泛型類的所有實(shí)際擁有相同的運(yùn)行時(shí)類,而不管它們具體的類型參數(shù)。
              確實(shí),對(duì)一個(gè)類的泛型所做的事實(shí)就是這個(gè)泛型類對(duì)它所有可能的類型參數(shù)都有相同的行為;相同的這個(gè)類可以被視為它有很多不同的類型。
              同樣的結(jié)果,泛型類中的靜態(tài)變量和方法也被該類的所有實(shí)例共享。這就是為什么在一個(gè)靜態(tài)方法或初始化器中、在一個(gè)靜態(tài)變量的聲明或初始化器中引用類型變量是非法的。

              Cast和Instanceof
              一個(gè)泛型類被它的所有實(shí)例共享的另一個(gè)隱含意義就是,如果某個(gè)實(shí)例是這個(gè)泛型類的一種特定類型的實(shí)例,那么通常情況下請(qǐng)求這個(gè)類的實(shí)例是無(wú)意義的:
              Collection cs = new ArrayList<String>();
              if (cs instanceof Collection<String>) { ...} // Illegal.
              類似地,如下面這個(gè)強(qiáng)制類型轉(zhuǎn)換
              Collection<String> cstr = (Collection<String>) cs; // Unchecked warning,
              會(huì)報(bào)一個(gè)未檢查的警告,因?yàn)檫@不應(yīng)該是運(yùn)行時(shí)系統(tǒng)將要為你檢查的事情。
              對(duì)類型變量也是如此
              <T> T badCast(T t, Object o) {return (T) o; // Unchecked warning.
              }
              類型變量在運(yùn)行時(shí)并不存在。這就意味著在時(shí)間和空間上,它們都不可能避免地?zé)o法產(chǎn)生作用。不幸的是,這也意味著你不能可靠地在強(qiáng)制類型轉(zhuǎn)換中使用它們。

              數(shù)組
              一個(gè)數(shù)組對(duì)象中元素的類型不會(huì)是一個(gè)類型變量或參數(shù)化的類型,除非它是一個(gè)(非受限的)通配符類型。你可以聲明數(shù)組類型的元素類型是一個(gè)類型變量或參數(shù)化的類型,但數(shù)組對(duì)象本身不行。
              這很煩人,但卻是真的。該約束對(duì)避免如下例子中的情況是有必要的:
              List<String>[] lsa = new List<String>[10]; // Not really allowed.
              Object o = lsa;
              Object[] oa = (Object[]) o;
              List<Integer> li = new ArrayList<Integer>();
              li.add(new Integer(3));
              oa[1] = li; // Unsound, but passes run time store check
              String s = lsa[1].get(0); // Run-time error: ClassCastException.
          如果允許有參數(shù)化類型的數(shù)組,上面的例子將會(huì)通過(guò)編譯且不報(bào)任何未檢查的警告,然而會(huì)在運(yùn)行時(shí)失敗。我們已經(jīng)知道設(shè)計(jì)泛型的主要目的就是為了類型安全。特別地說(shuō),Java語(yǔ)言被設(shè)計(jì)為,如果你的整個(gè)程序使用javac -source 1.5進(jìn)行編譯時(shí)沒(méi)有報(bào)任何未檢查的警告,那么這個(gè)程序就是類型安全的。
              然而,你仍然可以使用通配符數(shù)組。這兒有上面代碼的兩個(gè)變種。第一個(gè)變種放棄使用參數(shù)化類型的數(shù)組對(duì)象和參數(shù)化類型元素。這樣我們?yōu)榱嗽跀?shù)組外得到String對(duì)象不得不在顯示地使用強(qiáng)制類型轉(zhuǎn)換。
              List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
              Object o = lsa;
              Object[] oa = (Object[]) o;
              List<Integer> li = new ArrayList<Integer>();
              li.add(new Integer(3));
              oa[1] = li; // Correct.
              String s = (String) lsa[1].get(0); // Run time error, but cast is explicit.
          在第二個(gè)變種中,我們限制了數(shù)組對(duì)象的創(chuàng)建,這個(gè)數(shù)組的元素的類型被參數(shù)化了,但仍然要將一個(gè)參數(shù)化的元素類型用于這個(gè)數(shù)組。這是合法的,但產(chǎn)生一個(gè)未檢查的警告。確實(shí),這段代碼是不安全的,甚至?xí)?dǎo)致一個(gè)錯(cuò)誤。
              List<String>[] lsa = new List<?>[10]; // Unchecked warning. This is unsafe!
              Object o = lsa;
              Object[] oa = (Object[]) o;
              List<Integer> li = new ArrayList<Integer>();
              li.add(new Integer(3));
              oa[1] = li; // Correct.
              String s = lsa[1].get(0); // Run time error, but we were warned.
          譯者:根據(jù)我的測(cè)試(JDK 1.5.0_11),"List<String>[] lsa = new List<?>[10]"這一句無(wú)法通過(guò)編譯,理由也很直觀"類型不匹配,不能將List<?>[]轉(zhuǎn)化為L(zhǎng)ist< String>[]"。
              類似地,試圖創(chuàng)建一個(gè)元素類型是類型變量的數(shù)組對(duì)象會(huì)導(dǎo)致一個(gè)運(yùn)行時(shí)錯(cuò)誤:
              <T> T[] makeArray(T t) {
                  return new T[100]; // Error.
              }
              因?yàn)轭愋妥兞吭谶\(yùn)行時(shí)并不存在,這就沒(méi)有辦法確定數(shù)組的實(shí)際類型。
              圍繞著這些限制的工作方法是使用了將類字面量當(dāng)作運(yùn)行時(shí)類型標(biāo)記的機(jī)制,該機(jī)制將在下一節(jié)"類字面量作為運(yùn)行時(shí)標(biāo)記"中進(jìn)行敘述。

          8 類字面量作為運(yùn)行時(shí)標(biāo)記
              JDK 5.0的變量之一就是java.lang.Class也被泛型化了。這是一個(gè)不容器類而在其它地方使用泛型機(jī)制的有趣例子。
              既然Class類有一個(gè)類型參數(shù)T,你可能會(huì)問(wèn),這個(gè)T代表什么?它代表這個(gè)Class對(duì)象表示的類型。
              例如,String.class的類型是Class<String>,而Serializable.class的類型就是Class<Serializable>。這種機(jī)制用于提高在你的反射程序中的類型安全性。
              特別地,由于Class類中的方法netInstance現(xiàn)在是返回一個(gè)T,這樣當(dāng)你在使用反射機(jī)制創(chuàng)建對(duì)象時(shí)能夠得到更加精確的類型。
              例如,假設(shè)你需要一個(gè)執(zhí)行數(shù)據(jù)庫(kù)查詢的工具方法,給入的是SQL字符串,返回的是數(shù)據(jù)庫(kù)中匹配該查詢語(yǔ)言的對(duì)象的集合。
              一種方法就是顯示地傳入一個(gè)工廠對(duì)象中,所寫的代碼就像:
              interface Factory<T> { T make();}

              public <T> Collection<T> select(Factory<T> factory, String statement) {
                  Collection<T> result = new ArrayList<T>();
                  /* Run sql query using jdbc */
                  for (/* Iterate over jdbc results. */) {
                      T item = factory.make();
                      /* Use reflection and set all of item's fields from sql results. */
                      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");
          這個(gè)解決方案最終還需要:
              * 在調(diào)用點(diǎn)使用冗長(zhǎng)的匿名工廠類,
              * 或者,為每個(gè)被使用的類型聲明一個(gè)工廠類,并將這個(gè)工廠類的實(shí)例傳遞到調(diào)用點(diǎn),但這種方法有點(diǎn)不自然。
          可以很自然地將類字面量用作工廠對(duì)象,這個(gè)工廠稍后可被反射機(jī)制使用?,F(xiàn)在這個(gè)程序(不用泛型)可以寫為:
              Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
              ...
              public static Collection select(Class c, String sqlStatement) {
                  Collection result = new ArrayList();
                  /* Run sql query using jdbc. */
                  for (/* Iterate over jdbc results. */ ) {
                      Object item = c.newInstance();
                      /* Use reflection and set all of item's fields from sql results. */
                      result.add(item);
                  }
                  return result;
              }
          可是,這不能給我們一個(gè)所期望的精確類型的集合。既然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>();
                  /* Run sql query using jdbc. */
                  for (/* Iterate over jdbc results. */ ) {
                      T item = c.newInstance();
                      /* Use reflection and set all of item's fields from sql results. */
                      result.add(item);
                  }
                  return result;
              }
          上面的程序以一種類型安全的方法給了我們精確類型的集合。
              將類字面量作為運(yùn)行時(shí)標(biāo)記的技術(shù)被認(rèn)為十分狡猾。例如,為了操作Annotation,這種技術(shù)在新API中被擴(kuò)展使用了。

          9 通配符的更多趣味
              在本節(jié),我們將考慮一些更高級(jí)的通配符用法。我們已經(jīng)看了幾個(gè)受限的通配符用于讀取數(shù)據(jù)結(jié)構(gòu)時(shí)例子?,F(xiàn)在反過(guò)來(lái)想想一個(gè)只可寫的數(shù)據(jù)結(jié)構(gòu)。接口Sink是這種類型的一個(gè)簡(jiǎn)單的例子:
              interface Sink<T> {
                  flush(T t);
              }
          我們可以想像將它作為一個(gè)范例用于下面的代碼。方法writeAll被設(shè)計(jì)為刷新集合coll中的所有元素到Sink的實(shí)例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); // Illegal call.
          就已經(jīng)寫出來(lái)的,對(duì)writeAll方法的調(diào)用是非法的,由于無(wú)法推斷出有效的類型實(shí)參;String或Object都不是T的合適類型,因?yàn)镃ollection的元素和Sink必須是相同的類型。
              我們可以通過(guò)修改writeAll的方法簽名來(lái)修正這個(gè)錯(cuò)誤,如下所示,使用了通配符:
              public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
              ...
              String str = writeAll(cs, s); // Call is OK, but wrong return type.
          該調(diào)用是合法的,但賦值是錯(cuò)的,是由于返回類型被推斷成了Object,因?yàn)門匹配s的類型,但s的類型是Object。
              該解決方案使用了一種我們尚未見(jiàn)過(guò)的受限通配符形式:有一個(gè)較低限度的通配符。語(yǔ)法"? super T"表示未知類型是T的超類型(或者是T本身;記住,超類型關(guān)系是彈性的)。
              public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
                  ...
              }
              String str = writeAll(cs, s); // Yes!
          使用了這種語(yǔ)法,方法的調(diào)用就是合法的,并且被推斷的類型正如所愿是String。
              現(xiàn)在讓我們轉(zhuǎn)向更為實(shí)際的例子。java.util.TreeSet<E>表示了一個(gè)排序了的以類型為E的對(duì)象作為元素的樹(shù)。構(gòu)造一個(gè) TreeSet對(duì)象的方法之一是傳遞一個(gè)Comparator對(duì)象給這個(gè)構(gòu)造器。該Comparator對(duì)象將被用于根據(jù)期望的規(guī)則對(duì)TreeSet中的元素進(jìn)行排序。
              TreeSet(Comparator<E> c)
              Comparator接口是必須的:
              interface Comparator<T> {
                  int compare(T fst, T snd);
              }
          假設(shè)我們想創(chuàng)建一個(gè)TreeSet<String>對(duì)象,并傳入一個(gè)合適的比較器對(duì)象。我們就需要一個(gè)能比較String的 Comparator對(duì)象,一個(gè)Comparator<String>就可以做到,但一個(gè)Comparator<Object> 對(duì)象也能做到。然而,我們不能調(diào)用上面Comparator<Object>所提供的構(gòu)造器。
              TreeSet(Comparator<? super E> c)
          上述代碼允許適用的比較器被使用。
              作為最后一個(gè)低位受限通配符的例子,讓我們看看Collections.max方法,該方法返回一個(gè)集合中的極大元素。為了讓max文件能夠工作,集合中所有的傳入該集合的元素都必須實(shí)現(xiàn)了Comparable接口。此外,它們相互之間必須是可被比較的。
              在第一次嘗試創(chuàng)建這個(gè)方法后有如下結(jié)果:
              public static <T extends Comparable<T>>
                      T max(Collection<T> coll)
          即,這個(gè)方法有一個(gè)某類型T的集合對(duì)象,T的實(shí)例之間可以進(jìn)行比較,該方法并返回一個(gè)該類型的元素。然而,這個(gè)程序?qū)崿F(xiàn)起來(lái)太受限制了。看看是為什么,考慮一個(gè)對(duì)象,它能與任意對(duì)象進(jìn)行比較:
              class Foo implements Comparable<Object> {
                  ...
              }
              Collection<Foo> cf = ... ;
              Collections.max(cf); // Should work.
          Collection cf中的每個(gè)元素都能與該集合中的其它元素進(jìn)行比較,因?yàn)槊總€(gè)這樣的元素都是一個(gè)Foo的實(shí)例,而Foo的實(shí)例能夠與任意對(duì)象進(jìn)行比較,則與另一個(gè)Foo 對(duì)象比較那就更沒(méi)問(wèn)題了。然而,使用前面的方法簽名,我們可以發(fā)現(xiàn)上面對(duì)方法max的調(diào)用會(huì)被拒絕。被推斷出的類型必須是Foo,但Foo并沒(méi)有實(shí)現(xiàn) Comparable<Foo>。
              沒(méi)有必要精確地要求T與它自己的實(shí)例進(jìn)行比較。所有被要求的是T的實(shí)例能夠與它的某個(gè)超類型的實(shí)例進(jìn)行比較。這就讓我們有了如下代碼:
              public static <T extends Comparable<? super T>>
                      T max(Collection<T> coll)
          注意到Collections.max真實(shí)的方法簽名更難以理解。我們將在下一節(jié)"將遺留代碼轉(zhuǎn)化到使用泛型"中再講述它。這個(gè)適用于幾乎任何一個(gè) Comprarable應(yīng)用的理論是打算能用于任意的類型:你總是想使用Comprarable<? super T>。
              一般地,如果你的API只是將類型參數(shù)T作為類型變量使用,那就應(yīng)該利于低位受限通配符(? super T)。相反地,如果這個(gè)API只需返回T,你就要使用高位受限通配符(? extends T)以給這個(gè)API的客戶端程序更大的靈活性。

          通配符捕獲
              到目前為此,下面的程序應(yīng)該更清晰些:
              Set<?> unknownSet = new HashSet<String>();
              ...
              /** Add an element  t to a Set s. */
              public static <T> void addToSet(Set<T> s, T t) {
                  ...
              }
          但下面的調(diào)用是非法的。
              addToSet(unknownSet, "abc"); // Illegal.
          傳入該方法的一個(gè)精確的Set是一個(gè)String的Set這沒(méi)有影響;問(wèn)題在于作為實(shí)參傳入表達(dá)式的是一個(gè)未知類型的Set,這并不能保證它一定就是String或其它任何特定類型的Set。
              現(xiàn)在考慮下面的代碼:
              class Collections {
                  ...
                  <T> public static Set<T> unmodifiableSet(Set<T> set) {
                      ...
                  }
              }
              ...
              Set<?> s = Collections.unmodifiableSet(unknownSet); // This works! Why?
          看起來(lái)它應(yīng)該不被允許;然而,看看這個(gè)特殊的調(diào)用,它確實(shí)是安全的而可以允許這么做。畢竟,unmodifiableSet方法可用于任何類型的Set,而不管這個(gè)Set中的元素的類型。
              因?yàn)檫@種情況發(fā)生地相對(duì)比較頻繁,所以有一個(gè)特殊的規(guī)則允許這些在一個(gè)非常特殊的環(huán)境中的代碼是合法的,在這個(gè)環(huán)境中這些代碼被證明是安全的。這個(gè)名為"通配符捕獲"的規(guī)則允許編譯器將通配符的未知類型作為類型實(shí)參推斷到泛型方法中。

          10 將遺留代碼轉(zhuǎn)化為使用泛型
              早先,我們展示了新、老代碼之間如何交互。現(xiàn)在是時(shí)候看看"泛型化"老代碼這個(gè)困難的問(wèn)題了。
              如果你決定將老代碼轉(zhuǎn)換成使用泛型,你需要仔細(xì)考慮如何去修改你的API。
              你需要確定泛型化的API不會(huì)造成過(guò)度的限制;它必須能繼續(xù)地支持API原先的功能。再次考慮一些來(lái)自于java.util.Collection中的例子。沒(méi)有使用泛型的API看起來(lái)像:
              interface Collection {
                  public boolean containsAll(Collection c);
                  public boolean addAll(Collection c);
              }
          一種自然的泛型化嘗試可能像下面那樣:
              interface Collection<E> {

                  public boolean containsAll(Collection<E> c);
                  public boolean addAll(Collection<E> c);
              }
          肯定是類型安全的了,但它并沒(méi)有實(shí)現(xiàn)該API之前的功能。containsAll方法用于任何引入的集合對(duì)象,如果引入的集合真地僅包含E的實(shí)例時(shí),該方法才會(huì)成功。但是:
              * 引入集合的靜態(tài)類型可能有所不同,或許是因?yàn)檎{(diào)用者不知道傳入的集合對(duì)象的準(zhǔn)確類型,或者可能是因?yàn)樗且粋€(gè)Collection<S>,而S是E的子類型。
              * 能夠合法地使用一個(gè)不同的類型的集合調(diào)用containsAll方法則最為理想了。這種方法應(yīng)該能工作,并將返回false。
              在這個(gè)例子中的addAll方法,我們應(yīng)該能夠加入由任何由E的子類型的實(shí)例組成的集合對(duì)象。我們?cè)?泛型方法"這一節(jié)中已經(jīng)看過(guò)了如何正確地處理此類情況。
              你也需要保證修改后的API要保持與老的客戶端程序的二進(jìn)制兼容性。這就暗示著"擦除"后的API必須與以前的非泛型化API相同。在大部分例子中,這自然會(huì)引用爭(zhēng)吵,但也有一些精妙的例子。我們將測(cè)試我們已經(jīng)遇到過(guò)的最精妙例子中的一個(gè),即Collections.max()方法。根據(jù)我們?cè)?通配符的更多樂(lè)趣"一節(jié)所看到的,一個(gè)模糊的max方法簽名是:
              public static <T extends Comparable<? super T>>
                      T max(Collection<T> coll)
          除了擦除后的簽名之外,這些都很好:
              public static Comparable max(Collection coll)
          這與max之前的方法簽名不同:
              public static Object max(Collection coll)
          當(dāng)然可以這樣指定max方法的簽名,但這沒(méi)有什么用。所有老的調(diào)用Collections.max方法的二進(jìn)制class文件都依賴于返回類型為Object的方法的簽名。
              通過(guò)顯示地在限度中為形式類型參數(shù)T指定一個(gè)超類,我們能夠強(qiáng)制這個(gè)擦除產(chǎn)生不同的結(jié)果。
              public static <T extends Object & Comparable<? super T>>
                      T max(Collection<T> coll)
          這是一個(gè)單個(gè)類型參數(shù)有多個(gè)限度的例子,使用語(yǔ)法"T1 & T2 ... & Tn"。有多個(gè)限度的類型變量是被認(rèn)為是限度中所有類型的一個(gè)子類型。當(dāng)使用多限度時(shí),限度中第一個(gè)被提及的類型將作為該類型變量被擦除后的類型。
              最后,我們應(yīng)該回想到max方法只需從輸入的Collection中進(jìn)行讀取操作,所以這適合于T的任何子類型的集合。
              這就把我們帶入到JDK中該方法的真實(shí)簽名中:
              public static <T extends Object & Comparable<? super T>>
                      T max(Collection<? extends T> coll)
          在實(shí)踐中產(chǎn)生如此晦澀的應(yīng)用是十分罕見(jiàn)的,但是當(dāng)轉(zhuǎn)換現(xiàn)有API時(shí),專家型的類庫(kù)設(shè)計(jì)者們應(yīng)該要準(zhǔn)備著去進(jìn)行非常細(xì)致地地思考。
              另一個(gè)問(wèn)題需要密切關(guān)注的就是"協(xié)變返回",即在一個(gè)子類型中精煉了返回類型。你不需要在老的API中使用這個(gè)特性。為了找到原因,讓我們看一個(gè)例子。
              假設(shè)你原先的API是如下形式:
              public class Foo {
                  public Foo create() {
                      ...
                  } // Factory. Should create an instance of whatever class it is declared in.
              }

              public class Bar extends Foo {
                  public Foo create() {
                      ...
                  } // Actually creates a Bar.
              }
          為了利用"協(xié)變返回",你將它修改為:
              public class Foo {
                  public Foo create() {
                      ...
                  } // Factory. Should create an instance of whatever class it is declared in.
              }

              public class Bar extends Foo {
                  public Bar create() {
                      ...
                  } // Actually creates a Bar.
              }
          現(xiàn)在假設(shè)有一個(gè)像下面那樣寫的你代碼的第三方客戶端程序:
              public class Baz extends Bar {
                  public Foo create() {
                      ...
                  } // Actually creates a Baz.
              }
              Java 虛擬機(jī)不直接支持有著不同返回類型的方法的覆蓋,該特性由編譯器支持。因此,除非Baz類被重新編譯,否則它不能正常地覆蓋Bar的create方法。另外,Baz將不得不被修改,因?yàn)檫@些代碼將如前面所寫的那樣被拒絕--Baz中的create方法返回類型并不是Bar中create方法返回類型的子類型。
              譯者:根據(jù)我的測(cè)試(JDK 1.5.0_11),Baz類中的create方法無(wú)法通過(guò)編譯,理由就是Baz.create方法與Bar.create方法的返回不兼容,返回類型須是Bar,而不是Foo。

          致謝
              Erik Ernst, Christian Plesner Hansen, Jeff Norton, Mads Torgersen, Peter von der Ahe和Philip Wadler為該教程提供了材料。
              感謝David Biesack, Bruce Chapman, David Flanagan, Neal Gafter, Orjan Petersson, Scott Seligman, Yoshiki Shibata和Kresten Krab Thorup為該教程的早期版本所提出的富有價(jià)值的反饋。向我忘記列出來(lái)的每個(gè)人道歉。
          posted on 2007-06-20 11:11 John Jiang 閱讀(4339) 評(píng)論(17)  編輯  收藏 所屬分類: JavaSE 、Generics 、翻譯 、JavaTutorials

          評(píng)論

          # re: Java Tutorials -- Generics(譯) 2007-06-28 12:50 Sha Jiang
          Thinking in Java 4th的中文版出了,可惜我已經(jīng)買了英語(yǔ)版的(不過(guò)也好,可以鍛煉英文 :-D)。
          CSDN上有TiJ 4th泛型那一章的樣文,給我提供了一個(gè)翻譯Generic關(guān)鍵名詞的極好資料。

          我第一個(gè)要查的詞就是Type Argument。
          Generics中有Type Parameter,Type Argument甚至還有Type Variable等名詞。
          Type Parameter翻譯為"類型參數(shù)",應(yīng)該是沒(méi)有異義的;那么Type Argument呢?
          我暫時(shí)翻譯為"類型實(shí)參",即實(shí)際的類型參數(shù),即用來(lái)替代類型參數(shù)的實(shí)際類型。  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-06-28 12:57 Sha Jiang
          把本文翻譯完了之后,我又Google了一下Java Generic,發(fā)現(xiàn)原來(lái)早就有朋友翻譯了本文。
          不過(guò),自我感覺(jué)大家翻譯的水平差不多 :-),我原則上只使用直譯,但其他朋友用意譯較多。所以我的譯文比較繞口。
          但我一貫認(rèn)為,直譯太多的話可能會(huì)失去英文原文的風(fēng)味,盡管中文讀者會(huì)感覺(jué)輕松些。
          不過(guò),對(duì)于關(guān)鍵詞、句的翻譯還是參照看權(quán)威文獻(xiàn)中的翻譯,這次TiJ 4th中文版就大有用處了。  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-06-28 16:42 Sha Jiang
          看了TiJ 4th中文版樣文,以及IBM dW上的一些英文文章,仍然將Type Argument翻譯為"類型參數(shù)"。可能這也沒(méi)辦法的事情,但如果是這樣的話,在中文中無(wú)法將"Type Parameter"和"Type Argument"區(qū)分開(kāi)。
          一個(gè)是"形參",另一個(gè)是"實(shí)參",在相應(yīng)的上下文環(huán)境中不會(huì)生成大的混亂,但確實(shí)已經(jīng)缺少了英文原文的那種"第一感覺(jué)"。  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-06-28 16:53 sitinspring
          為java新語(yǔ)言特性買TiJ 4th值得嗎?老貴的,打了折也是。

          倒是想買本專為java新語(yǔ)言特性寫的書(shū),有沒(méi)好的推薦一下。  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-06-28 17:17 Sha Jiang
          > 為java新語(yǔ)言特性買TiJ 4th值得嗎?老貴的,打了折也是。
          也不能說(shuō)只是為了Java的新特性去買TiJ,學(xué)習(xí)Java基礎(chǔ)的任何一個(gè)方面,TiJ都是很值得一看的。

          > 倒是想買本專為java新語(yǔ)言特性寫的書(shū),有沒(méi)好的推薦一下。
          關(guān)于Java新特性的書(shū)那己是"鋪天蓋地",應(yīng)該沒(méi)有專門講新特性的書(shū)。
          完全沒(méi)有必要,也不可能。JDK的每個(gè)版本的新特性都很多,而且很多新特性之間沒(méi)什么關(guān)聯(lián)。
          但一本"Java Generics and Collections",但國(guó)內(nèi)沒(méi)有賣的。
            回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-06-28 17:19 Sha Jiang
          > 關(guān)于Java新特性的書(shū)那己是"鋪天蓋地",應(yīng)該沒(méi)有專門講新特性的書(shū)。
          Sorry,寫錯(cuò)了。本句第一個(gè)出現(xiàn)的"書(shū)"字應(yīng)該改成"文章",如下:
          關(guān)于Java新特性的文章那己是"鋪天蓋地",應(yīng)該沒(méi)有專門講新特性的書(shū)。  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-06-28 20:31 sitinspring
          @Sha Jiang

          其實(shí)我覺(jué)得核心編程系列對(duì)初學(xué)者更好些,Tij淺了點(diǎn),只是思想不錯(cuò).
          深入弄的話O'realy的系列講得還算詳細(xì).
            回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-06-29 08:35 Sha Jiang
          > TiJ淺了點(diǎn)
          不能吧,TiJ講得很深了,全部都是Java語(yǔ)言級(jí)基礎(chǔ)和少量核心API。
          反而核心編程系列都是講的純應(yīng)用級(jí)的。雖然幾卷本下來(lái),內(nèi)容也很多,但不是每章對(duì)每個(gè)人都用。因?yàn)橐茨憔唧w是做哪個(gè)方面的應(yīng)用。  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-06-29 09:57 Sha Jiang
          我可能范了一個(gè)很嚴(yán)重的錯(cuò)誤:關(guān)于Bound的翻譯。
          [1]我將Bound翻譯為"限度";
          [2]IBM dW的一篇譯文(http://www.ibm.com/developerworks/cn/java/j-jtp01255.html)將它翻譯為"綁定";
          [3]TiJ 4th中文版將它翻譯為"邊界"。

          這三種翻譯應(yīng)該似乎都是有其它一定的道理。
          [0]在字典中查Bound,它有"跳躍;限制,范圍,限度;被束縛,綁定"等含義。
          [1]從Bound在泛型中的實(shí)際意義(當(dāng)然,只是我個(gè)人的理解)來(lái)看,它對(duì)通配符的范圍進(jìn)行了限制(上限或下限),我就選用了"限度"這個(gè)詞。"限制"這個(gè)詞太普通了,容易讓人聯(lián)想到"Limitation,Limit"等詞,所以我沒(méi)有選用它。
          [2]IBM dW中的文章將它翻譯為"綁定",就是將Bound看成Bind的過(guò)去式(bind是不規(guī)則動(dòng)詞),過(guò)去式可以作形容詞,形容詞又可名詞化。(請(qǐng)?jiān)试S我賣弄一下我那僅有的一點(diǎn)點(diǎn)英文語(yǔ)法知識(shí):D)。
          但從Bound在Generic在上下文環(huán)境中來(lái)看,"綁定"可能會(huì)造成誤解或者根本就不能理解。"綁定"什么了?通配符被綁定了?至少我不會(huì)做這樣的理解。
          [3]TiJ 4th中文版將它翻譯為"邊界"。"邊界"與"范圍"、"限度"或"限制"應(yīng)該是有相近意義的,特別是針對(duì)此處Bound在Java Generic中的作用,這些詞的含義更是相近。
          Bound只有上限(上邊界)和/或下限(下邊界),而不能定義很多、很復(fù)雜的"限制、限度('約束'可能更貼切)"。相比較于"限度"這個(gè)詞,"邊界"這個(gè)詞可能比較容易被大家接受。

          另,看得出來(lái),正式出版的譯文中,適當(dāng)?shù)囊庾g是必須的,否則沒(méi)人能看得懂。
          以后,我也要有意識(shí)地使用"意譯" *_*

          P.S. 翻譯就是二次創(chuàng)作,需要不斷的學(xué)習(xí)、積累經(jīng)驗(yàn)。  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-06-29 10:08 Sha Jiang
          關(guān)于學(xué)習(xí)Java泛型,Neal Gafter的幾篇Blog應(yīng)該要看看。
          http://gafter.blogspot.com
          其中關(guān)于"Erasure"、"Class作為Type Token"等主題的Blog被廣泛引用。
          我也有計(jì)劃將它們翻譯出來(lái)(可能已經(jīng)有人做過(guò)了,我還要重復(fù)發(fā)明"輪子"嗎?)。

          Neal Gafter原來(lái)是Java SE 5的主要領(lǐng)導(dǎo)之一,Java Puzzlers的作者(本書(shū)的另一位作者就是"超級(jí)大牛" Josh Bloch),目前是Google Calendar的領(lǐng)導(dǎo)。
          另外,他還是Java Closure的專家組領(lǐng)導(dǎo),Closure是開(kāi)發(fā)中的JDK 7的新語(yǔ)法特性(已經(jīng)確定了嗎?),目前關(guān)于該語(yǔ)法的討論也很火熱,爭(zhēng)論較多。
          說(shuō)不定以后,Neal的相關(guān)Blog也將成為我們學(xué)習(xí)Closure這種"奇特"語(yǔ)法的極好資料呢。  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-07-02 15:23 Sha Jiang
          > [3]TiJ 4th中文版將它翻譯為"邊界"。"邊界"與"范圍"、"限度"或"限制"應(yīng)該是有
          > 相近意義的,特別是針對(duì)此處Bound在Java Generic中的作用,這些詞的含義
          > 更是相近。
          > Bound只有上限(上邊界)和/或下限(下邊界),而不能定義很多、很復(fù)雜的"限
          > 制、限度('約束'可能更貼切)"。相比較于"限度"這個(gè)詞,"邊界"這個(gè)詞可能比較容
          > 易被大家接受。
          今天又看到了JDK中的核心異常之一的IndexOutOfBoundsException,對(duì)于這個(gè)異常大家肯定都很熟悉了。
          我現(xiàn)在才注意到,原來(lái)這里面的"邊界"就是用的"Bound"。
          看來(lái)TiJ 4th中文版將Bound翻譯為"邊界",是最合適的了,我也得改改自己的這篇譯文了 :-D  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯)[未登錄](méi) 2007-09-02 15:20 vivi
          謝謝,非常受用,感謝您的翻譯  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2007-09-02 19:28 Sha Jiang
          如有任何微小的幫助,就很高興了 :-)  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2009-12-21 09:53 Sarah24
          This sould be really executable to order thesis title related to this good post in the <a href="http://www.exclusivethesis.com">dissertation</a> service particularly if people don’t have time.   回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2013-03-07 12:47 Jiun Sinang
          Has any one seen this Generics tutorial form http://javarevisited.blogspot.com.br/2011/09/generics-java-example-tutorial.html from Javarevisited Blog, looks great to me  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2013-05-16 10:17 jianfeng
          Argument一般都是函數(shù)的入口參數(shù),比如

          public class AClass<T> {

          public void classMethod1(T args) {};
          public <V> void classMethod2(V args) {};
          }

          T肯定是就是所謂的Type para.
          V應(yīng)該是Type argument。

          Thanks.  回復(fù)  更多評(píng)論
            

          # re: Java Tutorials -- Generics(譯) 2014-02-22 23:23 sewa mobil
          Nice article, thanks for the information. It's very complete information. I will bookmark for next reference.
            回復(fù)  更多評(píng)論
            

          主站蜘蛛池模板: 赤峰市| 高要市| 精河县| 上蔡县| 大城县| 马关县| 大邑县| 定远县| 枣阳市| 泸定县| 紫阳县| 嵩明县| 佛坪县| 麻栗坡县| 高台县| 张家口市| 文水县| 桦甸市| 和林格尔县| 贵德县| 衡东县| 永年县| 洛南县| 溧水县| 马公市| 汝城县| 黄陵县| 亳州市| 泸州市| 潞城市| 桂林市| 岫岩| 涿州市| 张家港市| 淳化县| 宽城| 英吉沙县| 酉阳| 皮山县| 新竹县| 阿坝|