計算機程式的思維邏輯 (54) - 剖析Collections - 設計模式

swiftma發表於2016-12-07

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (54) - 剖析Collections - 設計模式

上節我們提到,類Collections中大概有兩類功能,第一類是對容器介面物件進行操作,第二類是返回一個容器介面物件,上節我們介紹了第一類,本節我們介紹第二類。

第二類方法大概可以分為兩組:

  1. 接受其他型別的資料,轉換為一個容器介面,目的是使其他型別的資料更為方便的參與到容器類協作體系中,這是一種常見的設計模式,被稱為介面卡
  2. 接受一個容器介面物件,並返回一個同樣介面的物件,目的是使該物件更為安全的參與到容器類協作體系中,這也是一種常見的設計模式,被稱為裝飾器(不過,裝飾器不一定是為了安全)。

下面我們就來介紹這兩組方法,以及對應的設計模式。

介面卡

介面卡就是將一種型別的介面轉換成另一種介面,類似於電子裝置中的各種USB轉接頭,一端連線某種特殊型別的介面,一段連線標準的USB介面。Collections類提供了幾組類似於介面卡的方法:

  • 空容器方法:類似於將null或"空"轉換為一個標準的容器介面物件
  • 單一物件方法:將一個單獨的物件轉換為一個標準的容器介面物件 其他適配方法:將Map轉換為Set等

空容器方法

Collections中有一組方法,返回一個不包含任何元素的容器介面物件,如下所示:

public static final <T> List<T> emptyList()
public static final <T> Set<T> emptySet()
public static final <K,V> Map<K,V> emptyMap()
public static <T> Iterator<T> emptyIterator()
複製程式碼

分別返回一個空的List, Set, Map和Iterator物件。比如,可以這麼用:

List<String> list = Collections.emptyList();
Map<String, Integer> map = Collections.emptyMap();
Set<Integer> set = Collections.emptySet();
複製程式碼

一個空容器物件有什麼用呢?經常用作方法返回值。比如,有一個方法,可以將可變長度的整數轉換為一個List,方法宣告為:

public static List<Integer> asList(int... elements)
複製程式碼

在引數為空時,這個方法應該返回null還是一個空的List呢?如果返回null,方法呼叫者必須進行檢查,然後分別處理,程式碼結構大概如下所示:

int[] arr = ...; //從別的地方獲取到的arr
List<Integer> list = asList(arr);
if(list==null){
    ...
}else{
    ....
}
複製程式碼

這段程式碼比較囉嗦,而且如果不小心忘記檢查,則有可能會丟擲空指標異常,所以推薦做法是返回一個空的List,以便呼叫者安全的進行統一處理,比如,asList可以這樣實現:

public static List<Integer> asList(int... elements){
    if(elements.length==0){
        return Collections.emptyList();
    }
    List<Integer> list = new ArrayList<>(elements.length);
    for(int e : elements){
        list.add(e);
    }
    return list;
}
複製程式碼

返回一個空的List,也可以這樣實現:

return new ArrayList<Integer>();
複製程式碼

這與emptyList方法有什麼區別呢?emptyList返回的是一個靜態不可變物件,它可以節省建立新物件的記憶體和時間開銷。我們來看下emptyList的具體定義:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
複製程式碼

EMPTY_LIST的定義為:

public static final List EMPTY_LIST = new EmptyList<>();
複製程式碼

是一個靜態不可變物件,型別為EmptyList,它是一個私有靜態內部類,繼承自AbstractList,主要程式碼為:

private static class EmptyList<E>
    extends AbstractList<E>
    implements RandomAccess {
    public Iterator<E> iterator() {
        return emptyIterator();
    }
    public ListIterator<E> listIterator() {
        return emptyListIterator();
    }

    public int size() {return 0;}
    public boolean isEmpty() {return true;}

    public boolean contains(Object obj) {return false;}
    public boolean containsAll(Collection<?> c) { return c.isEmpty(); }

    public Object[] toArray() { return new Object[0]; }

    public <T> T[] toArray(T[] a) {
        if (a.length > 0)
            a[0] = null;
        return a;
    }

    public E get(int index) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }

    public boolean equals(Object o) {
        return (o instanceof List) && ((List<?>)o).isEmpty();
    }

    public int hashCode() { return 1; }
}
複製程式碼

emptyIterator和emptyListIterator返回空的迭代器,emptyIterator的程式碼為:

public static <T> Iterator<T> emptyIterator() {
    return (Iterator<T>) EmptyIterator.EMPTY_ITERATOR;
}
複製程式碼

EmptyIterator是一個靜態內部類,程式碼為:

private static class EmptyIterator<E> implements Iterator<E> {
    static final EmptyIterator<Object> EMPTY_ITERATOR
        = new EmptyIterator<>();

    public boolean hasNext() { return false; }
    public E next() { throw new NoSuchElementException(); }
    public void remove() { throw new IllegalStateException(); }
}
複製程式碼

以上這些程式碼都比較簡單,就不贅述了。

需要注意的是,EmptyList不支援修改操作,比如:

Collections.emptyList().add("hello");
複製程式碼

會丟擲異常UnsupportedOperationException。

如果返回值只是用於讀取,可以使用emptyList方法,但如果返回值還用於寫入,則需要新建一個物件。

其他空容器方法與emptyList類似,我們就不贅述了。它們都可以被用於方法返回值,以便呼叫者統一進行處理,同時節省時間和記憶體開銷,它們的共同限制是返回值不能用於寫入。

我們將空容器方法看做是介面卡,是因為它將null或"空"轉換為了容器物件。

單一物件方法

Collections中還有一組方法,可以將一個單獨的物件轉換為一個標準的容器介面物件,如下所示:

public static <T> Set<T> singleton(T o)
public static <T> List<T> singletonList(T o)
public static <K,V> Map<K,V> singletonMap(K key, V value)
複製程式碼

比如,可以這麼用:

Collection<String> coll = Collections.singleton("程式設計");
Set<String> set = Collections.singleton("程式設計");
List<String> list = Collections.singletonList("老馬");
Map<String, String> map = Collections.singletonMap("老馬", "程式設計");
複製程式碼

這些方法也經常用於構建方法返回值,相比新建容器物件並新增元素,這些方法更為簡潔方便,此外,它們的實現更為高效,它們的實現類都針對單一物件進行了優化。比如,我們看singleton方法的程式碼:

public static <T> Set<T> singleton(T o) {
    return new SingletonSet<>(o);
}
複製程式碼

新建了一個SingletonSet物件,SingletonSet是一個靜態內部類,主要程式碼為:

private static class SingletonSet<E>
    extends AbstractSet<E>
{
    private final E element;

    SingletonSet(E e) {element = e;}

    public Iterator<E> iterator() {
        return singletonIterator(element);
    }

    public int size() {return 1;}

    public boolean contains(Object o) {return eq(o, element);}
}
複製程式碼

singletonIterator是一個內部方法,將單一物件轉換為了一個迭代器介面物件,程式碼為:

static <E> Iterator<E> singletonIterator(final E e) {
    return new Iterator<E>() {
        private boolean hasNext = true;
        public boolean hasNext() {
            return hasNext;
        }
        public E next() {
            if (hasNext) {
                hasNext = false;
                return e;
            }
            throw new NoSuchElementException();
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}
複製程式碼

eq方法就是比較兩個物件是否相同,考慮了null的情況,程式碼為:

static boolean eq(Object o1, Object o2) {
    return o1==null ? o2==null : o1.equals(o2);
}
複製程式碼

需要注意的是,singleton方法返回的也是不可變物件,只能用於讀取,寫入會丟擲UnsupportedOperationException異常。

其他singletonXXX方法的實現思路是類似的,返回值也都只能用於讀取,不能寫入,我們就不贅述了。

除了用於構建返回值,這些方法還可用於構建方法引數。比如,從容器中刪除物件,Collection有如下方法:

boolean remove(Object o);
boolean removeAll(Collection<?> c);
複製程式碼

remove方法只會刪除第一條匹配的記錄,removeAll可以刪除所有匹配的記錄,但需要一個容器介面物件,如果需要從一個List中刪除所有匹配的某一物件呢?這時,就可以使用Collections.singleton封裝這個要刪除的物件,比如,從list中刪除所有的"b",程式碼如下所示:

List<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "b");
list.removeAll(Collections.singleton("b"));
System.out.println(list);
複製程式碼

其他方法

除了以上兩組方法,Collections中還有如下介面卡方法:

//將Map介面轉換為Set介面
public static <E> Set<E> newSetFromMap(Map<E,Boolean> map)
//將Deque介面轉換為後進先出的佇列介面
public static <T> Queue<T> asLifoQueue(Deque<T> deque)
//返回包含n個相同物件o的List介面
public static <T> List<T> nCopies(int n, T o)
複製程式碼

這些方法實際用的相對比較少,我們就不深入介紹了。

裝飾器

裝飾器接受一個介面物件,並返回一個同樣介面的物件,不過,新物件可能會擴充套件一些新的方法或屬性,擴充套件的方法或屬性就是所謂的"裝飾",也可能會對原有的介面方法做一些修改,達到一定的"裝飾"目的。

Collections有三組裝飾器方法,它們的返回物件都沒有新的方法或屬性,但改變了原有介面方法的性質,經過"裝飾"後,它們更為安全了,具體分別是寫安全、型別安全和執行緒安全,我們分別來看下。

寫安全

這組方法有:

public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c)
public static <T> List<T> unmodifiableList(List<? extends T> list)
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m)
public static <T> Set<T> unmodifiableSet(Set<? extends T> s)
public static <K,V> SortedMap<K,V> unmodifiableSortedMap(SortedMap<K, ? extends V> m)
public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<T> s)
複製程式碼

顧名思義,這組unmodifiableXXX方法就是使容器物件變為只讀的,寫入會丟擲UnsupportedOperationException異常。為什麼要變為只讀的呢?典型場景是,需要傳遞一個容器物件給一個方法,這個方法可能是第三方提供的,為避免第三方誤寫,所以在傳遞前,變為只讀的,如下所示:

public static void thirdMethod(Collection<String> c){
    c.add("bad");
}

public static void mainMethod(){
    List<String> list = new ArrayList<>(Arrays.asList(
            new String[]{"a", "b", "c", "d"}));
    thirdMethod(Collections.unmodifiableCollection(list));
}
複製程式碼

這樣,呼叫就會觸發異常,從而避免了將錯誤資料插入。

這些方法是如何實現的呢?每個方法內部都對應一個類,這個類實現了對應的容器介面,它內部是待裝飾的物件,只讀方法傳遞給這個內部物件,寫方法丟擲異常。我們以unmodifiableCollection方法為例來看,程式碼為:

public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) {
    return new UnmodifiableCollection<>(c);
}
複製程式碼

UnmodifiableCollection是一個靜態內部類,程式碼為:

static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
    private static final long serialVersionUID = 1820017752578914078L;

    final Collection<? extends E> c;

    UnmodifiableCollection(Collection<? extends E> c) {
        if (c==null)
            throw new NullPointerException();
        this.c = c;
    }

    public int size()                   {return c.size();}
    public boolean isEmpty()            {return c.isEmpty();}
    public boolean contains(Object o)   {return c.contains(o);}
    public Object[] toArray()           {return c.toArray();}
    public <T> T[] toArray(T[] a)       {return c.toArray(a);}
    public String toString()            {return c.toString();}

    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private final Iterator<? extends E> i = c.iterator();

            public boolean hasNext() {return i.hasNext();}
            public E next()          {return i.next();}
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    public boolean add(E e) {
        throw new UnsupportedOperationException();
    }
    public boolean remove(Object o) {
        throw new UnsupportedOperationException();
    }

    public boolean containsAll(Collection<?> coll) {
        return c.containsAll(coll);
    }
    public boolean addAll(Collection<? extends E> coll) {
        throw new UnsupportedOperationException();
    }
    public boolean removeAll(Collection<?> coll) {
        throw new UnsupportedOperationException();
    }
    public boolean retainAll(Collection<?> coll) {
        throw new UnsupportedOperationException();
    }
    public void clear() {
        throw new UnsupportedOperationException();
    }
}
複製程式碼

程式碼比較簡單,其他unmodifiableXXX方法的實現也都類似,我們就不贅述了。

型別安全

所謂型別安全是指確保容器中不會儲存錯誤型別的物件。容器怎麼會允許儲存錯誤型別的物件呢?我們看段程式碼:

List list = new ArrayList<Integer>();
list.add("hello");
System.out.println(list);
複製程式碼

我們建立了一個Integer型別的List物件,但新增了字串型別的物件"hello",編譯沒有錯誤,執行也沒有異常,程式輸出為:

[hello]
複製程式碼

之所以會出現這種情況,是因為Java是通過擦除來實現泛型的,而且型別引數是可選的。正常情況下,我們會加上型別引數,讓泛型機制來保證型別的正確性。但,由於泛型是Java 1.5以後才加入的,之前的程式碼可能沒有型別引數,而新的程式碼可能需要與老的程式碼互動。

為了避免老的程式碼用錯型別,確保在泛型機制失靈的情況下型別的正確性,可以在傳遞容器物件給老程式碼之前,使用如下方法"裝飾"容器物件:

public static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)
public static <E> List<E> checkedList(List<E> list, Class<E> type)
public static <K, V> Map<K, V> checkedMap(Map<K, V> m, Class<K> keyType, Class<V> valueType)
public static <E> Set<E> checkedSet(Set<E> s, Class<E> type)
public static <K,V> SortedMap<K,V> checkedSortedMap(SortedMap<K, V> m, Class<K> keyType, Class<V> valueType)
public static <E> SortedSet<E> checkedSortedSet(SortedSet<E> s, Class<E> type)
複製程式碼

使用這組checkedXXX方法,都需要傳遞型別物件,這些方法都會使容器物件的方法在執行時檢查型別的正確性,如果不匹配,會丟擲ClassCastException異常。比如:

List list = new ArrayList<Integer>();
list = Collections.checkedList(list, Integer.class);
list.add("hello");
複製程式碼

這次,執行就會丟擲異常,從而避免錯誤型別的資料插入:

java.lang.ClassCastException: Attempt to insert class java.lang.String element into collection with element type class java.lang.Integer
複製程式碼

這些checkedXXX方法的實現機制是類似的,每個方法內部都對應一個類,這個類實現了對應的容器介面,它內部是待裝飾的物件,大部分方法只是傳遞給這個內部物件,但對新增和修改方法,會首先進行型別檢查,型別不匹配會丟擲異常,型別匹配才傳遞給內部物件。以checkedCollection為例,我們來看下程式碼:

public static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type) {
    return new CheckedCollection<>(c, type);
}
複製程式碼

CheckedCollection是一個靜態內部類,主要程式碼為:

static class CheckedCollection<E> implements Collection<E>, Serializable {
    private static final long serialVersionUID = 1578914078182001775L;

    final Collection<E> c;
    final Class<E> type;

    void typeCheck(Object o) {
        if (o != null && !type.isInstance(o))
            throw new ClassCastException(badElementMsg(o));
    }

    private String badElementMsg(Object o) {
        return "Attempt to insert " + o.getClass() +
            " element into collection with element type " + type;
    }

    CheckedCollection(Collection<E> c, Class<E> type) {
        if (c==null || type == null)
            throw new NullPointerException();
        this.c = c;
        this.type = type;
    }

    public int size()                 { return c.size(); }
    public boolean isEmpty()          { return c.isEmpty(); }
    public boolean contains(Object o) { return c.contains(o); }
    public Object[] toArray()         { return c.toArray(); }
    public <T> T[] toArray(T[] a)     { return c.toArray(a); }
    public String toString()          { return c.toString(); }
    public boolean remove(Object o)   { return c.remove(o); }
    public void clear()               {        c.clear(); }

    public boolean containsAll(Collection<?> coll) {
        return c.containsAll(coll);
    }
    public boolean removeAll(Collection<?> coll) {
        return c.removeAll(coll);
    }
    public boolean retainAll(Collection<?> coll) {
        return c.retainAll(coll);
    }

    public Iterator<E> iterator() {
        final Iterator<E> it = c.iterator();
        return new Iterator<E>() {
            public boolean hasNext() { return it.hasNext(); }
            public E next()          { return it.next(); }
            public void remove()     {        it.remove(); }};
    }

    public boolean add(E e) {
        typeCheck(e);
        return c.add(e);
    }
}
複製程式碼

程式碼比較簡單,add方法中,會先呼叫typeCheck進行型別檢查。其他checkedXXX方法的實現也都類似,我們就不贅述了。

執行緒安全

關於執行緒安全我們後續章節會詳細介紹,這裡簡要說明下。之前我們介紹的各種容器類都不是執行緒安全的,也就是說,如果多個執行緒同時讀寫同一個容器物件,是不安全的。Collections提供了一組方法,可以將一個容器物件變為執行緒安全的,如下所示:

public static <T> Collection<T> synchronizedCollection(Collection<T> c)
public static <T> List<T> synchronizedList(List<T> list)
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
public static <T> Set<T> synchronizedSet(Set<T> s)
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)
複製程式碼

需要說明的,這些方法都是通過給所有容器方法加鎖來實現的,這種實現並不是最優的,Java提供了很多專門針對併發訪問的容器類,我們留待後續章節介紹。

小結

本節介紹了類Collections中的第二類方法,它們都返回一個容器介面物件,這些方法代表兩種設計模式,一種是介面卡,另一種是裝飾器,我們介紹了這兩種設計模式,以及這些方法的用法、適用場合和實現機制。

至此,關於容器類,我們就要介紹完了,下一節,讓我們一起來回顧一下,進行簡要總結。


未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (54) - 剖析Collections - 設計模式

相關文章