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中的泛型沒有協變特性的倒置的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM