Java1.5泛型指南中文版(Java1.5 Generic Tutorial)

Java1.5泛型指南中文版(Java1.5 Generic Tutorial)前端

英文版pdf下載連接:http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdfjava

                                                 譯者: chengchengji@163.com程序員

 

        sql

 

摘要和關鍵字數據庫

1.       介紹express

2.       定義簡單的泛型編程

3.       泛型和子類繼承數組

4.       通配符(Wildcards)安全

4.1.       有限制的通配符(Bounded Wildcards)數據結構

5.       泛型方法

6.       與舊代碼交互

6.1.       在泛型代碼中使用老代碼

6.2.       擦除和翻譯(Erasure and Translation)

6.3.     在老代碼中使用泛型代碼

7.       要點(The Fine Print)

7.1.       一個泛型類被其全部調用共享

7.2.       轉型和instanceof

7.3.       數組Arrays

8.       Class Literals as Run-time Type Tokens

9.       More fun with *

9.1.       通配符匹配(wildcard capture)

10.     泛型化老代碼

11.     致謝

 

摘要和關鍵字

       genericstype safetype parameter(variable)formal type parameteractual type parameterwildcards(?)unknown type? extends T? super TerasuretranslationcastinstanceofarraysClass Literals as Run-time Type Tokenswildcard capturemultiple bounds(T extends T1& T2 ... & Tn)covariant returns

     


 

1.            介紹

JDK1.5中引入了對java語言的多種擴展,泛型(generics)即其中之一。

這個教程的目標是向您介紹java的泛型(generic)。你可能熟悉其餘語言的泛型,最著名的是C++的模板(templates)。若是這樣,你很快就會看到二者的類似之處和重要差別。若是你不熟悉類似的語法結構,那麼更好,你能夠從頭開始而不須要忘記誤解。

Generics容許對類型進行抽象(abstract over types)。最多見的例子是集合類型(Container types)Collection的類樹中任意一個便是。

下面是那種典型用法:

       List myIntList = new LinkedList();// 1

        myIntList.add(new Integer(0));// 2

       Integer x = (Integer) myIntList.iterator().next();// 3

3行的類型轉換有些煩人。一般狀況下,程序員知道一個特定的list裏邊放的是什麼類型的數據。可是,這個類型轉換是必須的(essential)。編譯器只能保證iterator返回的是Object類型。爲了保證對Integer類型變量賦值的類型安全,必須進行類型轉換。

固然,這個類型轉換不只僅帶來了混亂,它還可能產生一個運行時錯誤(run time error),由於程序員可能會犯錯。

程序員如何才能明確表示他們的意圖,把一個list中的內容限制爲一個特定的數據類型呢?這是generics背後的核心思想。這是上面程序片段的一個泛型版本:

       List<Integer> myIntList = new LinkedList<Integer>(); // 1

       myIntList.add(new Integer(0)); // 2

       Integer x = myIntList.iterator().next(); // 3

注意變量myIntList的類型聲明。它指定這不是一個任意的List,而是一個IntegerList,寫做:List<Integer>。咱們說List是一個帶一個類型參數的泛型接口(a generic interface that takes a type parameter),本例中,類型參數是Integer。咱們在建立這個List對象的時候也指定了一個類型參數。

另外一個須要注意的是第3行沒了類型轉換。

如今,你可能認爲咱們已經成功地去掉了程序裏的混亂。咱們用第1行的類型參數取代了第3行的類型轉換。然而,這裏還有個很大的不一樣。編譯器如今可以在編譯時檢查程序的正確性。當咱們說myIntList被聲明爲List<Integer>類型,這告訴咱們不管什麼時候何地使用myIntList變量,編譯器保證其中的元素的正確的類型。與之相反,一個類型轉換說明程序員認爲在那個代碼點上它應該是那種類型。

實際結果是,這能夠增長可讀性和穩定性(robustness),尤爲在大型的程序中。

2.            定義簡單的泛型

下面是從java.util包中的List接口和Iterator接口的定義中摘錄的片段:

public interface List<E> {

           void add(E x);

           Iterator<E> iterator();

}

public interface Iterator<E> {

           E next();

           boolean hasNext();

}

這些都應該是很熟悉的,除了尖括號中的部分,那是接口ListIterator中的形式類型參數的聲明(the declarations of the formal type parameters of the interfaces List and Iterator)

類型參數在整個類的聲明中可用,幾乎是全部但是使用其餘普通類型的地方(可是有些重要的限制,請參考第7部分)

(原文:Type parameters can be used throughout the generic declaration, pretty much where you would use ordinary types (though there are some important restrictions; see section 7)

 

在介紹那一節咱們看到了對泛型類型聲明List(the generic type declaration List)的調用,如List<Integer>。在這個調用中(一般稱做一個參數化類型a parameterized type),全部出現形式類型參數(formal type parameter,這裏是E)都被替換成實體類型參數(actual type argument)(這裏是Integer)

你可能想象,List<Integer>表明一個E被所有替換成Integer的版本:

public interface IntegerList {

void add(Integer x)

Iterator<Integer> iterator();

}

這種直覺可能有幫助,可是也可能致使誤解。

它有幫助,由於List<Integer>的聲明確實有相似這種替換的方法。

它可能致使誤解,由於泛型聲明毫不會實際的被這樣替換。沒有代碼的多個拷貝,源碼中沒有、二進制代碼中也沒有;磁盤中沒有,內存中也沒有。若是你是一個C++程序員,你會理解這是和C++模板的很大的區別。

一個泛型類型的聲明只被編譯一次,而且獲得一個class文件,就像普通的class或者interface的聲明同樣。

類型參數就跟在方法或構造函數中普通的參數同樣。就像一個方法有形式參數(formal value parameters)來描述它操做的參數的種類同樣,一個泛型聲明也有形式類型參數(formal type parameters)。當一個方法被調用,實參(actual arguments)替換形參,方法體被執行。當一個泛型聲明被調用,實際類型參數(actual type arguments)取代形式類型參數。

一個命名的習慣:咱們推薦你用簡練的名字做爲形式類型參數的名字(若是可能,單個字符)。最好避免小寫字母,這使它和其餘的普通的形式參數很容易被區分開來。許多容器類型使用E做爲其中元素的類型,就像上面舉的例子。在後面的例子中還會有一些其餘的命名習慣。

 

3.            泛型和子類繼承

讓咱們測試一下咱們對泛型的理解。下面的代碼片段合法麼?

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

List<Object> lo = ls; //2

1行固然合法,可是這個問題的狡猾之處在於第2行。

這產生一個問題:

一個StringList是一個ObjectList麼?大多數人的直覺是回答:固然!

好,在看下面的幾行:

lo.add(new Object()); // 3

String s = ls.get(0); // 4: 試圖把Object賦值給String

這裏,咱們使用lo指向ls。咱們經過lo來訪問ls,一個Stringlist。咱們能夠插入任意對象進去。結果是ls中保存的再也不是String。當咱們試圖從中取出元素的時候,會獲得意外的結果。

java編譯器固然會阻止這種狀況的發生。第2行會致使一個編譯錯誤。

總之,若是FooBar的一個子類型(子類或者子接口),而G是某種泛型聲明,那麼G<Foo>G<Bar>的子類型並不成立!!

這多是你學習泛型中最難理解的部分,由於它和你的直覺相反。

這種直覺的問題在於它假定這個集合不改變。咱們的直覺認爲這些東西都不可改變。

舉例來講,若是一個交通部(DMV)提供一個駕駛員裏表給人口普查局,這彷佛很合理。咱們想,一個List<Driver>是一個List<Person>,假定DriverPerson的子類型。實際上,咱們傳遞的是一個駕駛員註冊的拷貝。然而,人口普查局可能往駕駛員list中加入其餘人,這破壞了交通部的記錄。

爲了處理這種狀況,考慮一些更靈活的泛型類型頗有用。到如今爲止咱們看到的規則限制比較大。

4.            通配符(Wildcards)

考慮寫一個例程來打印一個集合(Collection)中的全部元素。下面是在老的語言中你可能寫的代碼:

            void printCollection(Collection c) {

                 Iterator i = c.iterator();

                 for (int k = 0; k < c.size(); k++) {

                       System.out.println(i.next());

                  }

} 

下面是一個使用泛型的幼稚的嘗試(使用了新的循環語法):

      void printCollection(Collection<Object> c) {

           for (Object e : c) {

                 System.out.println(e);

           }

} 

問題是新版本的用處比老版本小多了。老版本的代碼可使用任何類型的collection做爲參數,而新版本則只能使用Collection<Object>,咱們剛纔闡述了,它不是全部類型的collections的父類。

那麼什麼是各類collections的父類呢?它寫做: Collection<?>(發音爲:"collection of unknown"),就是,一個集合,它的元素類型能夠匹配任何類型。顯然,它被稱爲通配符。咱們能夠寫:

void printCollection(Collection<?> c) {

for (Object e : c) {

System.out.println(e);

}

}

如今,咱們可使用任何類型的collection來調用它。注意,咱們仍然能夠讀取c中的元素,其類型是Object。這永遠是安全的,由於無論collection的真實類型是什麼,它包含的都是objects。可是將任意元素加入到其中不是類型安全的:

Collection<?> c = new ArrayList<String>();

c.add(new Object()); // 編譯時錯誤

由於咱們不知道c的元素類型,咱們不能向其中添加對象。

add方法有類型參數E做爲集合的元素類型。咱們傳給add的任何參數都必須是一個未知類型的子類。由於咱們不知道那是什麼類型,因此咱們沒法傳任何東西進去。惟一的例外是null,它是全部類型的成員。

另外一方面,咱們能夠調用get()方法並使用其返回值。返回值是一個未知的類型,可是咱們知道,它老是一個Object,所以把get的返回值賦值給一個Object類型的對象或者放在任何但願是Object類型的地方是安全的。

4.1.     有限制的通配符(Bounded Wildcards)

考慮一個簡單的畫圖程序,它能夠用來畫各類形狀,好比矩形和圓形。

爲了在程序中表示這些形狀,你能夠定義下面的類繼承結構:

public abstract class Shape {

public abstract void draw(Canvas c);

}

public class Circle extends Shape {

private int    xyradius;

public void draw(Canvas c) { // ...

}

}

public class Rectangle extends Shape {

private int    xywidthheight;

public void draw(Canvas c) {

// ...

}

}

這些類能夠在一個畫布(Canvas)上被畫出來:

public class Canvas {

public void draw(Shape s) {

s.draw(this);

}

}

全部的圖形一般都有不少個形狀。假定它們用一個list來表示,Canvas裏有一個方法來畫出全部的形狀會比較方便:

      public void drawAll(List<Shape> shapes) {

          for (Shape s : shapes) {

             s.draw(this);

         }

}

如今,類型規則致使drawAll()只能使用Shapelist來調用。它不能,好比說對List<Circle>來調用。這很不幸,由於這個方法所做的只是從這個list讀取shape,所以它應該也能對List<Circle>調用。咱們真正要的是這個方法可以接受一個任意種類的shape:

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

這裏有一處很小可是很重要的不一樣:咱們把類型 List<Shape> 替換成了 List<? extends Shape>。如今drawAll()能夠接受任何Shape的子類的List,因此咱們能夠對List<Circle>進行調用。

List<? extends Shape>是有限制通配符的一個例子。這裏?表明一個未知的類型,就像咱們前面看到的通配符同樣。可是,在這裏,咱們知道這個未知的類型其實是Shape的一個子類(它能夠是Shape自己或者Shape的子類而沒必要是extendsShape)。咱們說Shape是這個通配符的上限(upper bound)

像日常同樣,要獲得使用通配符的靈活性有些代價。這個代價是,如今像shapes中寫入是非法的。好比下面的代碼是不容許的:

         public void addRectangle(List<? extends Shape> shapes) {

       shapes.add(0, new Rectangle()); // compile-time error!

    }

你應該可以指出爲何上面的代碼是不容許的。由於shapes.add的第二個參數類型是extends Shape——一個Shape未知的子類。所以咱們不知道這個類型是什麼,咱們不知道它是否是Rectangle的父類;它多是也可能不是一個父類,因此這裏傳遞一個Rectangle不安全。

有限制的通配符正是咱們解決DMV給人口普查局傳送名單的例子所須要的。咱們的例子假定數據用一個姓名(String)到people(用Person或其子類來表示,好比Driver)。Map<K,V>是一個有兩個類型參數的泛型類型的例子,表示map的鍵key和值value

再一次,注意形式類型參數的命名習慣——K表明keysV表明vlaues

public class Census {

public static void  addRegistry(Map<String, ? extends Personregistry) ...}

}...

Map<String, DriverallDrivers = ...;

Census.addRegistry(allDrivers);

5.            泛型方法

考慮寫一個方法,它用一個Object的數組和一個collection做爲參數,完成把數組中全部object放入collection中的功能。

下面是第一次嘗試:

static void fromArrayToCollection(Object[] a, Collection<?c) {

for (Object o : a) {

c.add(o); // 編譯期錯誤

}

}

如今,你應該可以學會避免初學者試圖使用Collection<Object>做爲集合參數類型的錯誤了。或許你已經意識到使用 Collection<?>也不能工做。會議一下,你不能把對象放進一個未知類型的集合中去。

解決這個問題的辦法是使用generic methods就像類型聲明,方法的聲明也能夠被泛型化——就是說,帶有一個或者多個類型參數。

static <T> void fromArrayToCollection(T[] a, Collection<T> c){

       for (T o : a) {

           c.add(o); // correct

       }

    }

咱們可使用任意集合來調用這個方法,只要其元素的類型是數組的元素類型的父類。

      Object[] oa = new Object[100];

      Collection<Object> co = new ArrayList<Object>();

      fromArrayToCollection(oa, co);// T 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

注意,咱們並無傳送真實類型參數(actual type argument)給一個泛型方法。編譯器根據實參爲咱們推斷類型參數的值。它一般推斷出能使調用類型正確的最明確的類型參數(原文是:It will generally infer the most specific type argument that will make the call type-correct.)

如今有一個問題:咱們應該何時使用泛型方法,又何時使用通配符類型呢?

爲了理解答案,讓咱們先看看Collection庫中的幾個方法。

public interface Collection<E> {

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

}

咱們也可使用泛型方法來代替:

public interface Collection<E> {

        <T> boolean containsAll(Collection<T> c);

        <T extends E> boolean addAll(Collection<T> c);

        //  hey, type variables can have bounds too!

}

可是,在 containsAll  addAll中,類型參數都只使用一次。返回值的類型既不依賴於類型參數(type parameter)也不依賴於方法的其餘參數(這裏,只有簡單的一個參數)。這告訴咱們類型參數(type argument)被用做多態(polymorphism),它惟一的效果是容許在不一樣的調用點,可使用多種實參類型(actual argument)。若是是這種狀況,應該使用通配符。通配符就是被設計用來支持靈活的子類化的,這是咱們在這裏要強調的。

泛型函數容許類型參數被用來表示方法的一個或多個參數之間的依賴關係,或者參數與其返回值的依賴關係。若是沒有這樣的依賴關係,不該該使用泛型方法。

(原文:Generic methods allow type parameters to be used to express dependencies among the types of one or more arguments to a method and/or its return type. If there isn’t such a dependency, a generic method should not be used.

前一後的同時使用泛型方法和通配符也是可能的。下面是方法 Collections.copy():

class Collections {

public static <T>  void copy(List<Tdest, List<extends Tsrc){...}

}

注意兩個參數的類型的依賴關係。任何被從源list從拷貝出來的對象必須可以將其指定爲目標list(dest) 的元素的類型——T類型。所以源類型的元素類型能夠是T的任意子類型,咱們不關心具體的類型。

copy方法的簽名使用一個類型參數表示了類型依賴,可是使用了一個通配符做爲第二個參數的元素類型。咱們也能夠用其餘方式寫這個函數的簽名而根本不使用通配符:

class Collections {

public static <T, S extends T>  void copy(List<Tdest, List<Ssrc){...}

}

這也能夠,可是第一個類型參數在dst的類型和第二個參數的類型參數S的上限這兩個地方都有使用,而S自己只使用一次,在src的類型中——沒有其餘的依賴於它。這意味着咱們能夠用通配符來代替S。使用通配符比聲明顯式的類型參數更加清晰和準確,因此在可能的狀況下使用通配符更好。

通配符還有一個優點式他們能夠在方法簽名以外被使用,好比field的類型,局部變量和數組。這就有一個例子。

回到咱們的畫圖問題,假定咱們想要保持畫圖請求的歷史記錄。咱們能夠把歷史記錄保存在Shape類的一個靜態成員變量裏,在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);

}

}

最終,再說一下類型參數的命名習慣。

咱們使用表明類型,不管什麼時候都沒有比這更具體的類型來區分它。這常常見於泛型方法。若是有多個類型參數,咱們可能使用字母表中T的臨近的字母,好比S。若是一個泛型函數在一個泛型類裏邊出現,最好避免在方法的類型參數和類的類型參數中使用一樣的名字來避免混淆。對內部類也是一樣。

 

6.            與舊代碼交互

直到如今,咱們的例子中都假定了一個理想的世界,那裏全部人使用的都是最新版本的java編程語言,它支持泛型。

唉,現實並不是如此。百萬行代碼都是在早先版本的語言下寫做的,他們不可能一夜就轉換過來。

後面,在第10部分,咱們會解決把老代碼轉換爲使用泛型的代碼的問題。在這裏,咱們把注意力放在一個更簡單的問題:老代碼怎麼和泛型代碼交互?這個問題包括兩部分:在泛型中使用老代碼和在老代碼中使用泛型代碼。

6.1.     在泛型代碼中使用老代碼

怎樣才能使用老代碼的同時在本身的代碼中享受泛型帶來的好處?

做爲一個例子,假定你像使用包 com.Fooblibar.widgetsFooblibar.com(徹底虛構出來的公司) 的人們出售一種進行庫存管理的系統,下面是主要代碼:

package com.Fooblibar.widgets;

public interface Part ...}

public class Inventory {

/**

添加一個新配件到庫存數據庫

配件有名字name, 並由零件(Part)的集合組成。

零件由parts 指定. collection parts 中的元素必須實現Part接口。

**/

public static void addAssembly(String name, Collection parts) {...public static Assembly getAssembly(String name) {...}

}

public interface Assembly {

Collection getParts(); // Returns a collection of Parts

}

如今,你想使用上述API寫新代碼。若是能保證調用addAssembly()時老是使用正確的參數會很棒——就是說,你傳進去的確實時一個PartCollection。固然,泛型能夠實現這個目的:

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<Partc = new ArrayList<Part>();

c.add(new Guillotine()) ;

c.add(new Blade());

Inventory.addAssembly(」thingee」, c);

Collection<Partk = Inventory.getAssembly(」thingee」).getParts();

}

}

當咱們調用addAssembly,它但願第二個參數是Collection類型。而實際參數是Collection<Part> 類型。這能夠工做,可是爲何?畢竟,大多數集合不包含Part對象,並且總的來講,編譯器沒法知道Collection指的是什麼類型的集合。

在嚴格的泛型代碼裏,Collection應該老是帶着類型參數。當一個泛型類型,好比Collection被使用而沒有類型參數時,它被稱做一個raw type(天然類型??)

大多數人的第一直覺時Collection實際上意味着 Collection<Object>。可是,像咱們前面看到的,當須要Collection<Object>時傳遞 Collection<Part>是不安全的。類型Collection表示一個未知類型元素的集合,就像Collection<?>,這樣說更準確。

可是等一下,那也不正確。考慮getParts()這個調用,它返回一個Collection。而後它被賦值給k,而kCollection<Part>。若是這個調用的結果是一個Collection<?>,這個賦值應該是一個錯誤。

事實上,這個賦值是合法的,可是它產生一個未檢查警告(unchecked warning)。這個警告是必要的,由於事實是編譯器沒法保證其正確性。咱們沒有辦法檢查getAssembly()中的舊代碼來保證返回的確實是一個Collection<Part>。代碼裏使用的類型是Collection,能夠合法的向其中加入任何Object

那麼,這應該是一個錯誤麼?理論上講,Yes,可是實際上講,若是泛型代碼要調用舊代碼,那麼這必須被容許。這取決於你,程序員,在這種狀況下來知足你本身。這個賦值是合法的由於getAssembly()的調用約定中說它返回一個Part的集合,即便這個類型聲明中沒有顯示出這一點。

所以,天然類型和通配符類型很像,可是他們的類型檢查不是一樣嚴格。容許泛型與已經存在的老代碼相交互是一個深思熟慮的決定。

從泛型代碼中調用老代碼具備先天的危險性,一旦你把泛型編程和非泛型編程混合起來,泛型系統所提供的全部安全保證都失效。然而,你仍是比你根本不用泛型要好。至少你知道你這一端的代碼是穩定的。

在非泛型代碼遠比泛型代碼多的時候,不可避免會出現二者必須混合的狀況。

若是你發現你不得不混合舊代碼和泛型代碼,仔細注意未檢查警告(unchecked warnings)仔細考慮你怎樣才能證實出現警告的部分代碼是正確的。

若是你仍然犯了錯,而致使警告的代碼確實不是類型安全的,那麼會發生什麼?讓咱們看一下這種情形。在這個過程當中,咱們將瞭解一些編譯器工做的內幕。

 

6.2.     擦除和翻譯(Erasure and Translation)

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();

}

這裏,咱們用一個老的普通的list的引用來指向一個Stringlist。咱們插入一個Integer到這個list中,而且試圖獲得一個String。這是明顯的錯誤。若是咱們忽略這個警告而且試圖運行以上代碼,它將在咱們試圖使用錯誤的類型的地方失敗。在運行的時候,上面的代碼與下面的代碼的行爲同樣:

public String loophole(Integer x) {

       List ys = new LinkedList();

       List xs = ys;

       xs.add(x);

       return (String) ys.iterator().next(); // run time error

}

當咱們從list中獲取一個元素的時候,而且試圖經過轉換爲String而把它看成一個string,咱們獲得一個ClassCastException。徹底同樣的事情發生在使用泛型的代碼上。

這樣的緣由是,泛型是經過java編譯器的稱爲擦除(erasure)的前端處理來實現的。你能夠(基本上就是)把它認爲是一個從源碼到源碼的轉換,它把泛型版本的loophole()轉換成非泛型版本。

結果是,java虛擬機的類型安全和穩定性決不能冒險,即便在又unchecked warning的狀況下。

(原文:As a result, the type safety and integrity of the Java virtual machine are never at risk, even in the presence of unchecked warnings.

基本上,擦除去掉了全部的泛型類型信息。全部在尖括號之間的類型信息都被扔掉了,所以,好比說一個List<String>類型被轉換爲List。全部對類型變量的引用被替換成類型變量的上限(一般是Object)。並且,不管什麼時候若是結果代碼類型不正確,會插入一個到合適的類型的轉換,就像loophole的最後一行那樣。

擦除的所有的細節超出了本文的範圍,可是咱們給出的簡單描述與事實很接近。知道一點這個有好處,特別是若是你要做一些複雜的事,好比把現有API轉換成使用泛型的代碼(第10部分)或者僅僅是想理解爲何會這樣。

6.3. 在老代碼中使用泛型代碼

如今讓咱們來考慮相反的情形。假定Fooblibar.com公司的人決定把他們的代碼轉換爲使用泛型來實現,可是他們的一些客戶沒有轉換。如今代碼就像下面:

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<Partparts) {...}

public static Assembly getAssembly(String name) {...}

}

public interface Assembly {

Collection<PartgetParts(); // Returns 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和集合庫,它們都使用了泛型。客戶端代碼中的泛型類的聲明都是使用了天然類型(raw types)。第1行產生一個unchecked warning,由於一個天然的Collection被傳遞到一個須要Collection<Part>的地方,而編譯器沒法保證Collection就是一個Collection<Part>

你還有另外一種選擇,你可使用source 1.4 標誌來編譯客戶端代碼,以保證不會產生警告。可是這種狀況下你沒法使用jdk1.5 中的任何新特性。

7.            要點(The Fine Print)

7.1.     一個泛型類被其全部調用共享

下面的代碼打印的結果是什麼?

       List<String> l1 = new ArrayList<String>();

       List<Integer> l2 = new ArrayList<Integer>();

       System.out.println(l1.getClass() == l2.getClass());

或許你會說false,可是那你就錯了。它打印出true。由於全部的泛型類型在運行時有一樣的類(class),而無論他們的實際類型參數。

事實上,泛型之因此爲泛型就是由於它對全部其可能的類型參數,它有一樣的行爲;一樣的類能夠被看成許多不一樣的類型。

做爲一個結果,類的靜態變量和方法也在全部的實例間共享。這就是爲何在靜態方法或靜態初始化代碼中或者在靜態變量的聲明和初始化時使用類型參數申明是不合法的緣由。

(原文:As consequence, the static variables and methods of a class are also shared among all the instances. That is why it is illegal to refer to the type parameters of a type declaration in a static method or initializer, or in the declaration or initializer of a static variable.

7.2.     轉型和instanceof

泛型類被全部其實例(instances)共享的另外一個暗示是檢查一個實例是否是一個特定類型的泛型類是沒有意義的。

       Collection cs = new ArrayList<String>();

       if (cs instanceof Collection<String>) { ...} // 非法

相似的,以下的類型轉換

Collection<String> cstr = (Collection<String>) cs;

獲得一個unchecked warning,由於運行時環境不會爲你做這樣的檢查。

對類型變量也是同樣:

      <T> T badCast(T t, Object o) {

         return (T) o; // unchecked warning

  }

類型參數在運行時並不存在。這意味着它們不會添加任何的時間或者空間上的負擔,這很好。不幸的是,這也意味着你不能依靠他們進行類型轉換。

7.3.     數組Arrays

數組對象的組成類型不能是一個類型變量或者類型參數,除非它是無上限的通配符類型。你能夠聲明元素類型是一個類型參數或者參數化類型的數組類型,但不是數組對象(譯註:得不到對象,只能聲明)。

(原文:The component type of an array object may not be a type variable or a parameterized type, unless it is an (unbounded) wildcard type.You can declare array types whose element type is a type variable or a parameterized type, but not array objects.

這很煩人,可是確實時這樣。爲了不下面的狀況,必須有這樣的限制:

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

若是參數化類型能夠是數組,那麼意味着上面的例子能夠沒有任何unchecked warnings的經過編譯,可是在運行時失敗。咱們把類型安全(type-safety)做爲泛型首要的設計目標。特別的,java語言被設計爲保證:若是你的整個程序沒有unchecked warnings的使用javac –source1.5經過編譯,那麼它是類型安全的(原文: if your entire application has been compiled without unchecked warnings using javac -source 1.5, it is type safe)

然和,你仍然可使用通配符數組。上面的代碼有兩種變化。第一種改變放棄使用數組對象和元素類型參數化的數組類型。結果是,咱們不得不顯式的進行類型轉換來從數組中得到一個String

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

在下面的變體中,咱們避免了產生一個元素類型是參數化的數組對象,可是使用了元素類型參數化的類型。(譯註:意思以下面的第一行代碼所示,聲明一個泛型化的數組,可是new的時候使用的是raw type,原文中是 new ArrayList<?>(10),那是錯的,已經修正爲new ArrayList(10);)這是合法的,可是產生一個unchecked warning。實際上,這個代碼是不安全的,最後產生一個錯誤。

       List<String>[] lsa = new ArrayList[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

相似的,建立一個元素類型是一個類型變量的數組對象致使一個編譯時錯誤:

        <T> T[] makeArray(T t) {

           return new T[100]; // error

}

由於類型變量在運行時並不存在,因此沒有辦法決定實際類型是什麼。

解決這些限制的辦法是使用字面的類做爲運行時類型標誌(原文:use class literals as run time type tokens),見第8部分。

8.   Class Literals as Run-time Type Tokens

JDK1.5中一個變化是類 java.lang.Class是泛型化的。這是把泛型做爲容器類以外的一個頗有意思的例子(using genericity for something other than a container class)

如今,Class有一個類型參數T, 你極可能會問,表明什麼?

它表明Class對象表明的類型。好比說,String.class類型表明 Class<String>Serializable.class表明Class<Serializable>。着能夠被用來提升你的反射代碼的類型安全。

特別的,由於 Class newInstance() 方法如今返回一個T, 你能夠在使用反射建立對象時獲得更精確的類型。

好比說,假定你要寫一個工具方法來進行一個數據庫查詢,給定一個SQL語句,並返回一個數據庫中符合查詢條件的對象集合(collection)

一個方法時顯式的傳遞一個工廠對象,像下面的代碼:

        interface Factory<T> {

           public T[] make();

}

public <T> Collection<T> select(Factory<T> factory, String statement) {

Collection<T> result = new ArrayList<T>();

            /* run sql query using jdbc */

            for (int i=0;i<10;i++/* 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;

}

你能夠這樣調用:

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();}

}

而後調用:

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

這個解決方案的缺點是它須要下面的兩者之一:

l         調用處那冗長的匿名工廠類,或

l         爲每一個要使用的類型聲明一個工廠類並傳遞其對象給調用的地方

這很不天然。

使用class literal做爲工廠對象是很是天然的,它能夠被髮射使用。沒有泛型的代碼多是:

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;

}

可是這不能給咱們返回一個咱們要的精確類型的集合。如今Class是泛型的,咱們能夠寫:

Collection<EmpInfoemps=sqlUtility.select(EmpInfo.class, 」select * from emps」); ...

public static <TCollection<Tselect(Class<T>c, String sqlStatement) {

Collection<Tresult = 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;

}

來經過一種類型安全的方式獲得咱們要的集合。

這項技術是一個很是有用的技巧,它已成爲一個在處理註釋(annotations)的新API中被普遍使用的習慣用法。

 

 

9.            More fun with *

在這一部分,咱們來考慮一些通配符得高級用法。咱們已經看到了上限通配符在從一個數據結構中進行讀取的幾個例子。如今考慮相反的狀況,一個只寫的數據結構。

接口Sink是這種狀況的一個簡單例子。

            interface Sink<T> {

              void flush(T t);

    }

咱們能夠想象他被以下面的代碼同樣使用。方法writeAll() 被設計來把集合coll的全部元素flushsink snk,而且返回最後一個flush的元素。

        public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {

           T last = null;

           for (T t : coll) {

               last = t;

               snk.flush(last);

           }

            return last;

}

Sink<Objects;

Collection<Stringcs;

String str = writeAll(cs, s); // 非法的調用!!

像上面所寫,writeAll() 的調用是非法的,由於沒有有效的類型參數能夠被推斷出來。String  Object都不是T的合適的類型,由於Collection的元素和 Sink的元素必須是一樣的類型。

咱們能夠解決這個問題,經過使用通配符來修改writeAll()的方法簽名,以下:

<T> T writeAll(Collection<? extends T> coll, Sink<T> snk) { … }

String str = writeAll(cs, s); //能夠調用可是返回值類型錯誤

這個調用如今是合法的,可是賦值產生錯誤,由於推斷出的返回值類型是 Object由於匹配了Sink的類型,Object

解決方案是使用一種咱們尚未見過的有限制的通配符:有下限的通配符。語法 ? super T 表示T的一個未知的父類(或者是T本身)。這跟咱們用? extends T 表示T的一個未知的子類是對應的。

<T> T writeAll(Collection<T> coll, Sink<? super T> snk) { … }

String str = writeAll(cs, s); // YES!!!

使用這個語法,這個調用是合法的,推斷出來的TString,正是咱們想要的。

如今讓咱們看一個更現實的例子。一個 java.util.TreeSet<E> 表明一個有序的元素是E類型的樹。建立一個TreeSet的一個方法是傳遞一個 Comparator 對象給構造函數。這個Comparator將會用來按照須要對TreeSet進行排序。

TreeSet(Comparator<Ec)

Comparator 接口是核心:

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

假定咱們要建立一個 TreeSet<String> 並傳遞一個合適的 Comparator,咱們須要傳一個能比較StringComparator。這能夠是一個 Comparator<String>,也能夠是一個 Comparator<Object>。然而咱們不能用Comparator<Object>來調用上面的構造函數。咱們可使用一個有下限的通配符來獲得咱們須要的靈活性:

TreeSet(Comparator<super Ec)

這容許任何可用的Comparator被傳遞進去。

做爲使用下限通配符最終的例子,讓咱們來看看方法 Collections.max(),它返回一個集合中的最大的元素。

如今,爲了讓max()能工做,傳進來的集合中的全部元素必須實現 Comparatable接口。並且,他們必須都可以被彼此比較(all be comparable to each other)。第一個嘗試是:

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

就是說,方法的參數是某一個能和本身進行比較的T的集合。這限制太嚴格了。

爲何?考慮一個能和任何對象進行比較的類型:

class Foo implements Comparable<Object{......

Collection<Foocf = ...;

Collections.max(cf); // 應該能工做

cf 中的每一個元素均可以和每一個cf中的其餘元素進行比較,由於每一個這樣的元素都是一個Foo,它能夠和任意的對象進行比較,也能夠和另外一個Foo進行比較。

可是,使用上面的方法簽名,咱們發現這個調用被拒絕。推斷出來的類型必須是Foo,可是Foo沒有實現接口 Comparable<Foo>

精確的(exactly)和本身能比較是不須要的。所須要的是 T可以和它的父類中的一個進行比較,這導出:(注:Collections.max()的實際方法簽名更復雜,咱們在第10部分再討論。)

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

這個推論對大多數想讓 Comparable 對任意類型生效的用法中都有效:你老是應該使用 Comparable<? super T>

總之,若是你有一個只使用類型參數T做爲參數的API,它的使用應該利用下限通配符( ? super T )的好處。相反的,若是API只返回T,你應該使用上限通配符( ? extends T )來給你的客戶端更大的靈活性。

(原文:This reasoning applies to almost any usage of Comparable that is intended to work for arbitrary types: You always want to use Comparable<? super T>.

In general, if you have an API that only uses a type parameter T as an argument, its uses should take advantage of lower bounded wildcards (? super T). Conversely, if the API only returns T, you'll give your clients more flexibility by using upper bounded wildcards (? extends T). )。

9.1.     通配符匹配(wildcard capture)

如今應該很清晰,若是給定:

Set<?> unknownSet = new HashSet<String>(); ...

/**  Set s 中添加一個元素*/

public static <T> void addToSet(Set<T> s, T t) {...}

這個調用是非法的:

addToSet(unknownSet, "abc"); // 非法

實際的set是一個Stringset並不起做用,起做用的是傳進來的表達式是一個unknown typeset,它不能保證是一個Stringset或者任何其餘的特定類型。

如今,考慮:

class Collections ...

<Tpublic static Set<TunmodifiableSet(Set<Tset) ... }

}...

Set<?s = Collections.unmodifiableSet(unknownSet); // this works! Why?

彷佛這應該不被容許,可是,研究這個特定的調用,容許它是很是安全的。畢竟,unmodifiableSet 確實對任何種類的Set能工做,無論它的元素類型。

由於這種狀況相對出現的次數比較多,有一個特殊的規則在能證實代碼是安全的狀況下容許這樣的代碼。()這個規則,稱爲wildcard capture,容許編譯器推斷出通配符爲unknown type做爲一個泛型方法的類型參數。

(原文:Because this situation arises relatively frequently, there is a special rule that allows such code under very specific circumstances in which the code can be proven to be safe. This rule, known as wildcard capture, allows the compiler to infer the unknown type of a wildcard as a type argument to a generic method.

10.  泛型化老代碼

前面,咱們講述了新老代碼如何交互。如今,是時候研究更難的泛型化老代碼的問題了。

若是你決定把老代碼轉換成使用泛型的代碼,你須要仔細考慮怎麼修改你的API

你必須肯定泛型化的API不會過度嚴格,它必須繼續支持原來的API調用契約(original contract of the API)。在考慮幾個 java.util.Collection中的例子。泛型代碼以前的API像:

interface Collection {

public boolean containsAll(Collection c);

public boolean addAll(Collection c);

}

一個稚嫩的泛型化嘗試:

interface Collection<E{

public boolean containsAll(Collection<Ec);

public boolean addAll(Collection<Ec);

}

這固然是類型安全的,可是它不支持這個API的原始契約(original contract)

containsAll() 方法能對全部進來的任意類型的collection工做。它只有在傳進來的collection中真正只包含E的實例才成功,可是:

l         傳進來的collection的靜態類型可能不一樣,多是由於調用者不知道傳進來的colleciton的精確類型,或者由於它是一個Collection<S>SE的子類型。

l         用一個不一樣類型的collection來調用containsAll()應該是合法的。這個例程應該可以工做,返回false

addAll(),咱們應該可以添加任何元素是E的子類型的collection。咱們已經在第5部分講述了怎麼正確的處理這種狀況。

 你還應該保證修訂過的API保持與老客戶端的二進制兼容。者覺得者APIerasure必須與老的未泛型化版本同樣。在大多數狀況下,這是很天然的結果,可是有些精巧的情形(subtle cases)。咱們看看咱們已經碰到過的精巧的情形中的一個(one of the subtle cases),方法Collections.max()。就像咱們在第9部分看到的,一個似是而非的max()的方法簽名是:

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

這很好,除了擦除(erasure)後的簽名是:

public static Comparable max(Collection coll)

這和老版本的max() 的簽名不一樣:

public static Object max(Collection coll)

固然能夠把max()定義爲這個簽名,可是這沒有成爲現實,由於全部調用了Collections.max()的老的二進制class文件依賴於返回Object的簽名。

咱們能夠強迫the erasure不一樣,經過給形式類型參數T顯式的定義一個父類。

public static <extends Object & Comparable<super T>> T max(Collection<Tcoll)

這是一個對一個類型參數給定多個界限(multiple bounds)的例子,是用語法 T1 & T2 … & Tn。一個有多個界限的類型的參數是全部界限中列出來的類型的子類。當多個界限被使用的時候,界限中的第一個類型被用做這個類型參數的erasure

(原文:This is an example of giving multiple bounds for a type parameter, using the syntax T1& T2 ... & Tn. A type variable with multiple bounds is known to be a subtype of all of the types listed in the bound. When a multiple bound is used, the first type mentioned in the bound is used as the erasure of the type variable.

最後,咱們應該想到max只從傳進來collection中讀取數據,所以它對元素是T的子類的collection可用。這給咱們JDK中使用的真正的簽名:

public static <extends Object & Comparable<super T>> T max(Collection<extends Tcoll)

實際中出現那麼棘手的問題是很罕見的,可是專業庫設計師應該準備好很是仔細的考慮轉換現存的API

另外一個須要當心的問題是協變式返回值(covariant returns),就是說在子類中得到一個方法的返回值(refining the return type of a method in a subclass)。在老API中你沒法使用這個特性帶來的好處。

爲了知其緣由,讓咱們看一個例子。

假定你的原來的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

}

爲了使用協變式返回值的好處,你把它改爲:

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

}

如今,假定你的一個第三方客戶代碼:

public class Baz extends Bar {

public Foo create(){...// actually creates a Baz

}

Java虛擬機並不直接支持不一樣類型返回值的方法重載。這個特性是由編譯器來支持的。所以,除非Baz類被從新編譯,它不會正確的重載Barcreate()方法,並且,Baz必須被修改,由於Baz的代碼被拒絕,它的create的返回值不是Barcreate返回值的子類。(原文: Consequently, unless the class Baz is recompiled, it will not properly override the create() method of Bar.Furthermore, Baz will have to be modified, since the code will be rejected as written - the return type of create() in Baz is not a subtype of the return type of create() in Bar.

(譯註:上面的一段話有些莫名其妙,我測試過這個例子,在jdk1.4下,三個類都編譯以後改變Bar,只在jdk5下從新編譯Bar,而後在jdk5下,Baz仍然可以被使用,固然那,沒法使用 Baz b = baz.create();這樣的代碼。)

11.  致謝

Erik Ernst, Christian Plesner Hansen, Jeff Norton, Mads Torgersen, Peter von der Ah´e and Philip Wadler contributed material to this tutorial.

Thanks to David Biesack, Bruce Chapman, David Flanagan, Neal Gafter, ¨ Orjan Petersson,Scott Seligman, Yoshiki Shibata and Kresten Krab Thorup for valuable feedback on earlier versions of this tutorial. Apologies to anyone whom I’ve forgotten.

 

終於翻譯完了,感受挺累!譯文有些地方可能有誤或者不許確,因此保留了一些原文,對於重要的地方,也保留原文以助理解,歡迎來信指正或討論,mailto:chengchengji@163.com

相關文章
相關標籤/搜索