上節我們提到,類Collections中大概有兩類功能,第一類是對容器接口對象進行操作,第二類是返回一個容器接口對象,上節我們介紹了第一類,本節我們介紹第二類。
第二類方法大概可以分為兩組:
下面我們就來介紹這兩組方法,以及對應的設計模式。
適配器
適配器就是將一種類型的接口轉換成另一種接口,類似於電子設備中的各種USB轉接頭,一端連接某種特殊類型的接口,一段連接標准的USB接口。Collections類提供了幾組類似於適配器的方法:
空容器方法
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編程及計算機技術的本質。用心原創,保留所有版權。