1. 程式人生 > >第二十八條 利用有限制萬用字元來提升API的靈活性

第二十八條 利用有限制萬用字元來提升API的靈活性

書中提到,引數化型別是不可變的,意思就是兩個截然不同的型別 Type1 和 Type2, List<Type1> 即不是 List<Type2> 的子型別,也不是它的超型別。即使 Type1 是 Type2 的子類,List<Type1>也不是 List<Type2> 的子類。最直觀的的例子, String 是 Object 的子類,但   List<String> 卻不是 List<Object> 的子類,為什麼呢? List<Object> 能裝進去任意物件,List<String> 只能裝進去 String 型別, 基類有的功能,子類都是可以用的,但這個例子中,明顯不符合這個特徵。

    public class Stack<E> {         private static final int DEFAULT_INITIAL_CAPACITY = 16;         private E[] elements;         private int size = 0;

                 public void push(E e) {                     }              }

假如新增一個新方法,

    public void pushAll(Iterable<E> src){         for(E e : src){             push(e);         }     }

解釋一下,Iterable 是個介面,Collection 集合是實現這個介面的,如果不實現它,迭代器 和 增強for迴圈恐怕都不能直接使用。pushAll(Iterable<E> src) 這個方法咋一看沒問題,實際上不完美,因為實際中 Iterable<E> src 和 Stack<E> 元素型別不一定一樣。例如,有 Stack<Number> ,呼叫了 pushAll()方法,穿進去的引數為 Integer 型別,我們知道, Integer 是 Number 的子類,所以實際中很可能有這個需求,但如果我們真的這麼寫了,是會報錯的,編譯狀態就出錯了。

    Stack<Number> stack = new Stack<>();     ArrayList<Integer> list = new ArrayList<>();     stack.pushAll(list);

ArrayList 是 實現 Iterable 介面的,所以可以這麼用。很明顯,pushAll()方法報錯了,Number in Stack cannot be applied to Integer。如果都是用 Number 型別,則可以

    Stack<Number> stack = new Stack<>();     ArrayList<Number> list = new ArrayList<>();     stack.pushAll(list);  這種寫法是可以的,但侷限性太大了。 有辦法解決這種問題嗎? 有限制萬用字元型別,此刻閃亮登場,來解決問題。引數型別應該是 E的子型別的Iterable 介面,萬用字元寫法為Iterable<? extendsE>,在此注意,每個型別都是自己的子型別, 所以  Iterable<? extends E> 的意思是,既可以是 E,也可以是 E 的子型別。

    public void pushAll(Iterable<? extends E> src){         for(E e : src){             push(e);         }     } 這樣,就可以了。修改後,Integer 型別的 list 集合,就可以當做引數傳進去了。如果,我們再寫一個方法與pushAll()相對應,初次寫法如下

    public void popAll(Collection<E> dst){         while (!isEmpty()){             dst.add(pop());         }     } 意思就是穿進去一個 集合,然後把Stack中的元素,裝到該集合中。猛一看,沒問題。但實際上犯了pushAll()方法一開始犯的毛病。如果引數型別一樣,可以編譯通過。但實際中,往往也有子類父類的使用。比如, Object 是 Number 的父類, Collection<Object> 集合裡面新增一個 Number 物件是再正常不過了,但此時,呼叫這個方法,編譯都通不過

    Stack<Number> stack = new Stack<>();     ArrayList<Integer> list = new ArrayList<>();     stack.pushAll(list);

    ArrayList<Object> objList = new ArrayList<>();     stack.popAll(objList); 最後一行程式碼會報錯,提示  Number in Stack cannot be applied to Object, Collection<Object> 不是 Collection<Number> 的子型別。 怎麼辦呢?還是萬用字元, 找個 E 的超類的集合,寫法如下 Collection<? super E> dst ,同理,意思是 既可以是 E,也可以是 E 的基型別。

    public void popAll(Collection<? super E> dst){         while (!isEmpty()){             dst.add(pop());         }     } 這樣編譯就通過了。 看到這,發現優先萬用字元還挺好用的。書中給了兩個名詞,生產者 和 消費者。 pushAll()方法中src引數通過產生E來提供Stack使用,因此是生產者,用<? extends T>,popAll()方法中dst引數通過Stack,填裝Stack的物件,消費E的例項,所以是消費者,使用 <? super T>。 生產者 和 消費者 是個使用萬用字元的原則。如果感覺這個地方太繞口,根據實際業務邏輯來做判斷也行。就像上面 使用 集合 新增新的物件一樣, 基類的集合可以裝進去子類的物件,記住類似這句話的邏輯,也能寫出好的有限制萬用字元。

前面講過一個方法     public static <E> Set union(Set<E> s1, Set<E> s2) {         Set result = new HashSet(s1);         result.addAll(s2);         return result;     } 按照今天的講法,可以修改為 

    public static <E> Set union(Set<? extends E> s1, Set<? extends E> s2) {         Set result = new HashSet(s1);         result.addAll(s2);         return result;     } 但實際上,這種寫法也有一定侷限性。比如 E 的型別是 Object, 任何物件都是 Object 的子類, 那麼任何不相干的兩個 Set 集合, 都可以呼叫這個方法。如果我們能確定只用於 Number 型別,可以把功能縮小,改為 

    public static <E> Set union(Set<? extends Number> s1, Set<? extends Number> s2) {         Set result = new HashSet(s1);         result.addAll(s2);         return result;     } 這樣就能保證兩個Set集合的元素,都是 Number 本身,或者是它的子類。 我們再來看看上一章的一個例子,

    public static <T extends Comparator<T>>T max(List<T> list){              } 我們可以修改為     public static <T extends Comparator<? super T>>T max(List<T> list){

    } 此時,如果進一步,則為

    public static <T extends Comparable<? super T>> T max(List<? extends T> list) {         Iterator<? extends T> iterator = list.iterator();         T result = iterator.next();         T t;         while (iterator.hasNext()) {             t = iterator.next();             if (t.compareTo(result) > 0) {                 result = t;             }         }         return result;     } 好吧,這個方法裡,有 extends ,也有 super, 關鍵是 兩一塊出現了。我剛開始看到這也蒙了,一年後,隨著見識增長,才明白為什麼這麼寫,這麼寫到底有什麼好處。List<? extends T> list的意思是,傳進來的引數型別集合中的元素,既可以是 T, 也可以是T的子類,大大增加了該方法的靈活性。<T extends Comparable<? super T>> 意思是該T 必須實現 Comparable 介面,這有實現這個介面了,才能比較。注意,前面的兩點都好理解,最惹人頭痛的地方來了, 為什麼是 Comparable<? super T> 而不是 Comparable<T> ,Comparable<T> 意思是 T 型別本身 實現了 Comparable介面,Comparable<? super T> 意思 是 T 或者 T 的父類,只要任意一個實現 Comparable介面就行了,靈活性更高,更符合實際業務需求。舉例說明

class Shape implements Comparable<Shape>{

    @Override     public int compareTo(Shape o) {         return o.hashCode();     } }

class Rectangle extends Shape{

}

class Circle extends Shape{

}

Shape 是形狀,Rectangle 是長方形,是個子類, Circle 是圓形,也是子類。如果說,使用 <T extends Comparator<T>> 這種方法修飾的話,咱們建立一個 List<Rectangle> rectangleList = new ArrayList<>();  max(rectangleList); 編譯會報錯,因為此時 T 是 Rectangle 而非是 Shape, <T extends Comparator<T>> 的意思是 要 T 本身實現 Comparator 介面,如果修改 Shape 和 Rectangle 類,則編譯可以通過。

class Shape { }

class Rectangle extends Shape implements Comparable<Rectangle>{

    @Override     public int compareTo(Rectangle o) {         return o.hashCode();     } }

但是改動太大,Shape 的作用根本就沒體現。 Shape 已經實現了 Comparable 介面, 新新增一個子類,子類完全可以直接使用 compareTo()方法,不用每寫一個子類,就去實現一遍,如果不滿足子類的業務修改,可以重寫該方法,繼承體系的好處就是這裡。所以還是得讓父類去實現這個方法,這樣,子類都方便管理,也增加靈活性。<T extends Comparable<? super T>> 可以理解為 T 就是Shape,List<? extends T> 集合中的元素物件,既可以是 Shape,也可以是 Rectangle 或者 Circle, 只要基類 Shape 實現了 Comparable 介面, 子類就可以當做引數直接傳進該方法。大家仔細想想這個道理,並且 Collections 中的sort()方法,排序,也是這麼寫的 public static <T extends Comparable<? super T>> void sort(List<T> list) {} 異曲同工之妙。