天天看點

泛型(Generics)

泛型于Java1.5中被提出,這個期待已久的泛型增強了系統類型,在提供運作時安全類型,允許一個類型或者方法在不同類型的對象上執行操作。它給集合架構增加了編譯時的安全類型和減少了強制轉換類型的繁瑣工作。

  • 簡介
  • 簡單的泛型
  • 泛型和子類型化
  • 通配符
  • 泛型方法
  • 互動遺留代碼
  • 要點(The Fine Print)
  • 類文字作為運作時類型标記(Class Literals as Runtime-Type Tokens)
  • 更加有趣的通配符
  • 将遺留代碼改為使用泛型
  • 鳴謝

簡介

     對Java程式設計語言,JDK5.0引進了一些新的擴充,其中的一項是泛型。

     本節将會介紹泛型。你可能從其他的語言中暑期相似的結構,如c++中的模闆,如果這樣,你将會看到這兩者之間的相似點和不同點。而如果你從其他地方沒了解過,不熟悉這中結構,也沒關系,你将作為一個全新的初學者,不用學習任何混雜的概念。

     泛型允許你從類型中抽象出來。最常見的例子是集合類型,如集合層次結構中架構。

下面是一個簡短的典型例子:

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

     第三行中的強制轉換有些令人煩惱。在開始,程式知道什麼樣的類型資料被放到指定的集合中,然後,強制轉換又是必須的,編譯隻能保證疊代器傳回的使一個Object類型,為了保證配置設定給Integer類型變量是類型安全的,則必須要強制轉換。

     強制轉換不僅混亂,而且,由于程式員的錯誤,可能造成運作時錯誤。

     假如程式員能夠表達他們的意圖,并将限定為隻能存放指定的類型?這就是泛型的核心思想。下面的版本片段是使用泛型來實作上述的例子:

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

     注意myIntList中聲明的變量類型。它說明了這不是一個任意類型的集合,而是一組Integer的集合,寫成List.我們成List是一個泛型接口,它需要一個類型參數,在這個例子中,就是Integer。我們同時在建立集合對象的時候指定了類型參數。

請注意,第三行中的強制轉換已經不見了。

     現在,你可能認為我們所實作的是移動了雜亂的部分。替代第三行中的強制轉換為Integer,我們在第一行中有Integer作為類型參數。然而,這裡面有一個很大的差別,編譯器可以在編譯時校驗類型的正确性。當我們将myIntList聲明為List,它告訴我們了一些關于變量myIntList,無論何時何地使用它,編譯器都會保證它正确性。相反,強制轉換隻告訴了我們,程式員隻在部分代碼中認為它是正确的。

     整體影響,尤其是在大型程式中,它提高了可讀性和魯棒性。

簡單的泛型

     以下是摘自java.util包中List和Iterator接口的定義片段:

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

public interface Iterator<E>{
    E next();
    boolean hasNext();
}
           

     這段代碼應該都比較熟悉,除了尖括号中的東西。這些是List和Iterator接口的正式類型參數的聲明。

     類型參數可以再整個泛型聲明中使用,幾乎任何使用傳統類型的地方(盡管存在一些重要的限制;查閱要點 部分。在簡介部分,我們看到聲明為泛型類型List的調用,如List< Integer>,在調用中(通常稱為參數化類型),所有形式類型參數(這個例子中的E)都被實際類型(這個例子中的Integer)替代)。

     你可能想象List< Integer>代表了List的一個版本,其中E全部都被Integer替代:

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

     這種直覺是有意義的,但是它也是一種誤導。

     有意義是因為參數化類型List< Integer> 确實有方法,這些方法和擴充的類似。

     誤導,是因為泛型聲明不是使用這種方式來擴充。沒有代碼的拷貝,包括源碼、位元組碼、磁盤和記憶體。如果你是一個c++程式設計者,你就會明白這和c++中的模闆有很大不同點。

     泛型類型聲明一次編譯并且使用所有,并生成單一一個檔案,和普通類或者接口一樣。

     類型參數和普通方法或者構造函數中使用的參數類似。很像一個方法有形式值參數,這些參數描述方法對它操作的多種值。一個泛型聲明有形式類型參數。當一個方法被調用,實際參數替代形式參數,并且方法體被執行,當一個泛型聲明被調用,實際類型參數替代形式類型參數。

     一個命名約定的注釋。我們建議你們使用簡介但有意義的名字來命名形式類型參數,最好避免使用小寫字元,使它容易和普通的類或者接口區分開來。很多容器類型使用E,作為元素,如上面的例子中使用。我們将會後續的例子中看到其他的約定。

泛型和子類型化

     現在測試一下你對泛型的了解。下面代碼片段是否合法?

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

第一行肯定是合法的,這個問題中比較棘手的是第二行,這可以歸結為問題:String類型的List集合是Object類型的List集合嗎,大部分人直覺的答案是“當然了”。

     好吧,來看看下面的幾行:

lo.add(new Object());//3
    String s = ls.get();//4
           

這裡,我們有别名為ls和lo。當通路ls,String類型的集合,使用lo,我們可以将抽象類對象插入。ls将不能再繼續保留String,當我們試圖從中擷取時,我們将會獲得意料之外的。

     當然,Java編譯器将會阻止這樣發生,編譯時,第二行将會報錯。

     通常情況,如果Foo是Bar子類型(子類或者子接口),G是某種泛型類型,那麼G< Foo>不是G< Bar>的子類型。這也許是你學習泛型的最難部分,因為它和我們根深蒂固的思維相違背了。

     我們不應該假設集合不會改變,我們的直覺可能引導我們認為這事情是不可變的。

     例如,如果車輛管理所部門向人口普查局提供一份司機名單,這看起來似乎是有道理的。我們認為,List< Driver>是一種 List< Person>,假設Driver是Person的一個子類型。事實上,傳遞過去的隻是注冊司機的一份拷貝。然而,人口普查調查局可以将那些不是司機的人員添加到清單中,破壞DMV’s記錄。

     為了處理這種情況,考慮更靈活的泛型是有用的,到目前為止我們所看到的規則是相當嚴格的。

通配符

     思考寫一個程式,将一個集合中的元素輸出存在的問題。下面是一個你可能寫出來的例子,jdk5.0之前版本:

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

然而,這裡有一個天真的寫法,使用泛型:

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

     這裡的問題是新版本比老版本更加沒用。老版本的代碼可以被任何類型參數的集合調用,而新版本的代碼隻能使用Collection< Object>,這個我們剛剛示範的,不是所有集合的超級類型!

     什麼是所有類型集合的超級類型呢?它可以寫為Collection

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

     現在,我們叫它為任何類型的集合。注意printCollection(),我們仍然能夠從集合c讀取元素并将它們賦給Object類型。這樣總是安全的,由于不管集合的實際類型,它總能夠存放對象。然而,存放抽象對象是不安全的:

Collection<?> c = new ArrayList<String>();
    c.add(new Object());// compile time error
           

     由于我們不知道集合c代表什麼樣的類型,我們不能給它添加對象。方法add()需要參數類型為E,即元素的類型。當實際類型參數是?,它代表一些未知類型。任何我們傳遞給add的參數必須是未知類型的子類型。由于我們不知道它是什麼類型,我們不能傳遞任何東西。唯一的異常就是null,這是任何類型的成員。

     另外,給出一個List

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

pubic 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()隻能在類型為Shape的集合中才能被調用,比如,它不能再List< Circle>集合中被調用。麻煩的是,由于所有的方法從集合中擷取shapes,是以它僅能在List< Circle>中被調用。我們真正需要的是可以接收任何形狀的參數的方法:

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

     這是一個非常小但是非常重要的不同點,我們使用List< ? extends Shape>替代List< Shape>。現在,drawAll()将會接收任何Shape的子類,是以如果我們有需要,我們可以在List< Circle>中調用它。

     List

public void addRectangle(List<? extends Shape> shapes){
    shapes.add(,new Rectangle());
}
           

&nsbp;&nsbp;&nsbp;&nsbp; 你應該能夠明白為什麼以上代碼是不被允許的。shapes.add()中第二個參數類型是? extends Shape,也就是Shape的一個未知子類型。由于我們不知道它是什麼類型,我們不知道它是否是Rectangle的一個超級類型;它可能不是這種類型,是以傳遞一個Rectangle是不安全的。

&nsbp;&nsbp;&nsbp;&nsbp; 有限通配符是我們需要處理之前例子的一種方式,它傳遞資料給人口普查局。我們的示例假設資料由從名稱(表示為字元串)映射到人(由引用類型,例如Person或其子類型,例如驅動)來表示。Map < K,v>是一個具有兩種類型參數的泛型,表示映射的鍵和值。

待續。。。

泛型方法

互動遺留代碼

要點

類文字作為運作時類型标記

更加有趣的通配符

将遺留代碼改為使用泛型

鳴謝