PECS法則與extends和super關鍵字

通配符

在本文的前面的部分裏已經說過了泛型類型的子類型的不相關性。但有些時候,我們希望能夠像使用普通類型那樣使用泛型類型:

  • 向上造型一個泛型對象的引用
  • 向下造型一個泛型對象的引用

向上造型一個泛型對象的引用

例如,假設我們有很多箱子,每個箱子裏都裝有不同的水果,我們需要找到一種方法能夠通用的處理任何一箱水果。更通俗的說法,A是B的子類型,我們需要找到一種方法能夠將C<A>類型的實例賦給一個C<B>類型的聲明。

爲了完成這種操作,我們需要使用帶有通配符的擴展聲明,就像下面的例子裏那樣:

1

2

List<Apple> apples = new ArrayList<Apple>();

List<? extends Fruit> fruits = apples;

“? extends”是泛型類型的子類型相關性成爲現實:Apple是Fruit的子類型,List<Apple> 是 List<? extends Fruit> 的子類型。

向下造型一個泛型對象的引用

現在我來介紹另外一種通配符:? super。如果類型B是類型A的超類型(父類型),那麼C<B> 是 C<? super A> 的子類型:

1

2

List<Fruit> fruits = new ArrayList<Fruit>();

List<? super Apple> = fruits;

爲什麼使用通配符標記能行得通?

原理現在已經很明白:我們如何利用這種新的語法結構?

? extends

讓我們重新看看這第二部分使用的一個例子,其中談到了Java數組的子類型相關性:

1

2

3

Apple[] apples = new Apple[1];

Fruit[] fruits = apples;

fruits[0] = new Strawberry();

就像我們看到的,當你往一個聲明爲Fruit數組的Apple對象數組裏加入Strawberry對象後,代碼可以編譯,但在運行時拋出異常。

現在我們可以使用通配符把相關的代碼轉換成泛型:因爲Apple是Fruit的一個子類,我們使用? extends 通配符,這樣就能將一個List<Apple>對象的定義賦到一個List<? extends Fruit>的聲明上:

1

2

3

List<Apple> apples = new ArrayList<Apple>();

List<? extends Fruit> fruits = apples;

fruits.add(new Strawberry());

這次,代碼就編譯不過去了!Java編譯器會阻止你往一個Fruit list里加入strawberry。在編譯時我們就能檢測到錯誤,在運行時就不需要進行檢查來確保往列表里加入不兼容的類型了。即使你往list里加入Fruit對象也不行:

1

fruits.add(new Fruit());

你沒有辦法做到這些。事實上你不能夠往一個使用了? extends的數據結構裏寫入任何的值。

原因非常的簡單,你可以這樣想:這個? extends T 通配符告訴編譯器我們在處理一個類型T的子類型,但我們不知道這個子類型究竟是什麼。因爲沒法確定,爲了保證類型安全,我們就不允許往裏面加入任何這種類型的數據。另一方面,因爲我們知道,不論它是什麼類型,它總是類型T的子類型,當我們在讀取數據時,能確保得到的數據是一個T類型的實例:

1

Fruit get = fruits.get(0);

? super

使用 ? super 通配符一般是什麼情況?讓我們先看看這個:

1

2

List<Fruit> fruits = new ArrayList<Fruit>();

List<? super Apple> = fruits;

我們看到fruits指向的是一個裝有Apple的某種超類(supertype)的List。同樣的,我們不知道究竟是什麼超類,但我們知道Apple和任何Apple的子類都跟它的類型兼容。既然這個未知的類型即是Apple,也是GreenApple的超類,我們就可以寫入:

1

2

fruits.add(new Apple());

fruits.add(new GreenApple());

如果我們想往裏面加入Apple的超類,編譯器就會警告你:

1

2

fruits.add(new Fruit());

fruits.add(new Object());

因爲我們不知道它是怎樣的超類,所有這樣的實例就不允許加入。

從這種形式的類型裏獲取數據又是怎麼樣的呢?結果表明,你只能取出Object實例:因爲我們不知道超類究竟是什麼,編譯器唯一能保證的只是它是個Object,因爲Object是任何Java類型的超類。

存取原則和PECS法則

總結 ? extends 和 the ? super 通配符的特徵,我們可以得出以下結論:

  • 如果你想從一個數據類型裏獲取數據,使用 ? extends 通配符
  • 如果你想把對象寫入一個數據結構裏,使用 ? super 通配符
  • 如果你既想存,又想取,那就別用通配符。

這就是Maurice Naftalin在他的《Java Generics and Collections》這本書中所說的存取原則,以及Joshua Bloch在他的《Effective Java》這本書中所說的PECS法則。

Bloch提醒說,這PECS是指”Producer Extends, Consumer Super”,這個更容易記憶和運用。

http://www.importnew.com/14985.html

 

什麼是PECS? 

PECS指“Producer Extends,Consumer Super”。換句話說,如果參數化類型表示一個生產者,就使用<? extends T>;如果它表示一個消費者,就使用<? super T>,可能你還不明白,不過沒關係,接着往下看好了。

下面是一個簡單的Stack的API接口:

1

2

3

4

5

6

public class  Stack<E>{

    public Stack();

    public void push(E e):

    public E pop();

    public boolean isEmpty();

}

假設想增加一個方法,按順序將一系列元素全部放入Stack中,你可能想到的實現方式如下:

1

2

3

4

public void pushAll(Iterable<E> src){

    for(E e : src)

        push(e)

}

假設有個Stack<Number>,想要靈活的處理Integer,Long等Number的子類型的集合

1

2

3

Stack<Number> numberStack = new Stack<Number>();

Iterable<Integer> integers = ....;

numberStack.pushAll(integers);

此時代碼編譯無法通過,因爲對於類型Number和Integer來說,雖然後者是Number的子類,但是對於任意Number集合(如List<Number>)不是Integer集合(如List<Integer>)的超類,因爲泛型是不可變的。

幸好java提供了一種叫有限通配符的參數化類型,pushAll參數替換爲“E的某個子類型的Iterable接口”:

1

2

3

4

public void pushAll(Iterable<? extends E> src){

    for (E e: src)

        push(e);

}

這樣就可以正確編譯了,這裏的<? extends E>就是所謂的 producer-extends。這裏的Iterable就是生產者,要使用<? extends E>。因爲Iterable<? extends E>可以容納任何E的子類。在執行操作時,可迭代對象的每個元素都可以當作是E來操作。

與之對應的是:假設有一個方法popAll()方法,從Stack集合中彈出每個元素,添加到指定集合中去。

1

2

3

4

5

public void popAll(Collection<E> dst){

       if(!isEmpty()){

                dst.add(pop());

        }

}

假設有一個Stack<Number>和Collection<Object>對象:

1

2

3

Stack<Number> numberStack = new Stack<Number>();

Collection<Object> objects = ...;

numberStack.popAll(objects);

同樣上面這段代碼也無法通過,解決的辦法就是使用Collection<? super E>。這裏的objects是消費者,因爲是添加元素到objects集合中去。使用Collection<? super E>後,無論objects是什麼類型的集合,滿足一點的是他是E的超類,所以不管這個參數化類型具體是什麼類型都能將E裝進objects集合中去。

總結:

  1. 如果你是想遍歷collection,並對每一項元素操作時,此時這個集合是生產者(生產元素),應該使用 Collection<? extends Thing>.
  2. 如果你是想添加元素到collection中去,那麼此時集合是消費者(消費元素)應該使用Collection<? super Thing>

注:此文根據《Effective Java》以及Java Generics: What is PECS? 整理成文。想了解更多有關泛型相關知識,請讀者閱讀《Effective Java》的第五章。

http://www.importnew.com/8966.html

泛型是在Java 1.5中被加入了,這裏不討論泛型的細節問題,這個在Thinking in Java第四版中講的非常清楚,這裏要講的是super和extends關鍵字,以及在使用這兩個關鍵字的時候爲什麼會不同的限制。 
   首先,我們定義兩個類,A和B,並且假設B繼承自A。下面的代碼中,定義了幾個靜態泛型方法,這幾個例子隨便寫的,並不是特別完善,我們主要考量編譯失敗的問題: 

 

 

public class Generic{
//方法一
public static <T extends A> void get(List<T extends A> list)
{
    list.get(0);
}

//方法二
public static <T extends A> void set(List<T extends A> list, A a)
{
    list.add(a);
}

//方法三
public static <T super B> void get(List<T super B> list)
{
    list.get(0);
}

//方法四
public static <T super B> void set(List<T super B> list, B b)
{
    list.add(b);
}
}

 

 

 

編譯之後,我們會發現,方法二和方法三沒有辦法通過編譯。按照Thinking in Java上的說法,super表示下界,而extends表示上界,方法二之所以沒有辦法通過,是因爲被放到List裏面去的可能是A,也可能是任何A的子類,所以編譯器沒有辦法確保類型安全。而方法三之所以編譯失敗,則是因爲編譯器不知道get出來的是B還是B的其他的什麼子類,因爲set方法四允許在list放入B,也允許在list中放入B的子類,也就沒有辦法保證類型安全。 
  上面的這段解釋聽起來可能有點奇怪,都是因爲編譯器無法判斷要獲取或者設置的是A和B本身還是A和B的其他的子類才導致的失敗。那麼Java爲什麼不乾脆用一個關鍵字來搞定呢? 
  如果從下面的角度來解釋,就能把這個爲什麼編譯會出錯的問題解釋的更加的直白和清除,也讓人更容易理解,先看下面的代碼,還是A和B兩個類,B繼承自A: 

 

 

 

public class Generic2{
   public static void main(String[] args){
      List<? extends A> list1 = new ArrayList<A>();
      List<? extends A> list2 = new ArrayList<B>();
      List<? super B> list3 = new ArrayList<B>();
      List<? super B> list4 = new ArrayList<A>();
   }
}

 

 

 

   從上面這段創建List的代碼我們就更加容易理解super和extends關鍵字的含義了。首先要說明的一點是,Java強制在創建對象的時候必須給類型參數制定具體的類型,不能使用通配符,也就是說new ArrayList<? extends A>(),new ArrayList<?>()這種形式的初始化語句是不允許的。 
   從上面main函數的第一行和第二行,我們可以理解extends的含義,在創建ArrayList的時候,我們可以指定A或者B作爲具體的類型,也就是,如果<? extends X>,那麼在創建實例的時候,我們就可以用X或者擴展自X的類爲泛型參數來作爲具體的類型,也可以理解爲給?號指定具體類型,這就是extends的含義。 
   同樣的,第三行和第四行就說明,如果<? super X>,那麼在創建實例的時候,我們可以指定X或者X的任何的超類來作爲泛型參數的具體類型。 
   當我們使用List<? extends X>這種形式的時候,調用List的add方法會導致編譯失敗,因爲我們在創建具體實例的時候,可能是使用了X也可能使用了X的子類,而這個信息編譯器是沒有辦法知道的,同時,對於ArrayList<T>來說,只能放一種類型的對象。這就是問題的本質。而對於get方法來說,由於我們是通過X或者X的子類來創建實例的,而用超類來引用子類在Java中是合法的,所以,通過get方法能夠拿到一個X類型的引用,當然這個引用可以指向X也可以指向X的任何子類。 
   而當我們使用List<? super X>這種形式的時候,調用List的get方法會失敗。因爲我們在創建實例的時候,可能用了X也可能是X的某一個超類,那麼當調用get的時候,編譯器是無法準確知曉的。而調用add方法正好相反,由於我們使用X或者X的超類來創建的實例,那麼向這個List中加入X或者X的子類肯定是沒有問題的(?超類有多個,編譯器怎麼知道是哪個真超類),因爲超類的引用是可以指向子類的。 
  最後還有一點,這兩個關鍵字的出現都是因爲Java中的泛型沒有協變特性的倒置的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章