本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接http://item.jd.com/12299018.html


上节我们提到,类Collections中大概有两类功能,第一类是对容器接口对象进行操作,第二类是返回一个容器接口对象,上节我们介绍了第一类,本节我们介绍第二类。

第二类方法大概可以分为两组:

  1. 接受其他类型的数据,转换为一个容器接口,目的是使其他类型的数据更为方便的参与到容器类协作体系中,这是一种常见的设计模式,被称为适配器。
  2. 接受一个容器接口对象,并返回一个同样接口的对象,目的是使该对象更为安全的参与到容器类协作体系中,这也是一种常见的设计模式,被称为装饰器(不过,装饰器不一定是为了安全)。

下面我们就来介绍这两组方法,以及对应的设计模式。

适配器

适配器就是将一种类型的接口转换成另一种接口,类似于电子设备中的各种USB转接头,一端连接某种特殊类型的接口,一段连接标准的USB接口。Collections类提供了几组类似于适配器的方法:

  • 空容器方法:类似于将null或"空"转换为一个标准的容器接口对象
  • 单一对象方法:将一个单独的对象转换为一个标准的容器接口对象
  • 其他适配方法:将Map转换为Set等

空容器方法

Collections中有一组方法,返回一个不包含任何元素的容器接口对象,如下所示:

  1. public static final <T> List<T> emptyList()
  2. public static final <T> Set<T> emptySet()
  3. public static final <K,V> Map<K,V> emptyMap()
  4. public static <T> Iterator<T> emptyIterator()

分别返回一个空的List, Set, Map和Iterator对象。比如,可以这么用:

  1. List<String> list = Collections.emptyList();
  2. Map<String, Integer> map = Collections.emptyMap();
  3. Set<Integer> set = Collections.emptySet();

一个空容器对象有什么用呢?经常用作方法返回值。比如,有一个方法,可以将可变长度的整数转换为一个List,方法声明为:

  1. public static List<Integer> asList(int... elements)

在参数为空时,这个方法应该返回null还是一个空的List呢?如果返回null,方法调用者必须进行检查,然后分别处理,代码结构大概如下所示:

  1. int[] arr = ...; //从别的地方获取到的arr
  2. List<Integer> list = asList(arr);
  3. if(list==null){
  4. ...
  5. }else{
  6. ....
  7. }

这段代码比较啰嗦,而且如果不小心忘记检查,则有可能会抛出空指针异常,所以推荐做法是返回一个空的List,以便调用者安全的进行统一处理,比如,asList可以这样实现:

  1. public static List<Integer> asList(int... elements){
  2. if(elements.length==0){
  3. return Collections.emptyList();
  4. }
  5. List<Integer> list = new ArrayList<>(elements.length);
  6. for(int e : elements){
  7. list.add(e);
  8. }
  9. return list;
  10. }

返回一个空的List,也可以这样实现:

  1. return new ArrayList<Integer>();

这与emptyList方法有什么区别呢?emptyList返回的是一个静态不可变对象,它可以节省创建新对象的内存和时间开销。我们来看下emptyList的具体定义:

  1. public static final <T> List<T> emptyList() {
  2. return (List<T>) EMPTY_LIST;
  3. }

EMPTY_LIST的定义为:

  1. public static final List EMPTY_LIST = new EmptyList<>();

是一个静态不可变对象,类型为EmptyList,它是一个私有静态内部类,继承自AbstractList,主要代码为:

  1. private static class EmptyList<E>
  2. extends AbstractList<E>
  3. implements RandomAccess {
  4. public Iterator<E> iterator() {
  5. return emptyIterator();
  6. }
  7. public ListIterator<E> listIterator() {
  8. return emptyListIterator();
  9. }
  10.  
  11. public int size() {return 0;}
  12. public boolean isEmpty() {return true;}
  13.  
  14. public boolean contains(Object obj) {return false;}
  15. public boolean containsAll(Collection<?> c) { return c.isEmpty(); }
  16.  
  17. public Object[] toArray() { return new Object[0]; }
  18.  
  19. public <T> T[] toArray(T[] a) {
  20. if (a.length > 0)
  21. a[0] = null;
  22. return a;
  23. }
  24.  
  25. public E get(int index) {
  26. throw new IndexOutOfBoundsException("Index: "+index);
  27. }
  28.  
  29. public boolean equals(Object o) {
  30. return (o instanceof List) && ((List<?>)o).isEmpty();
  31. }
  32.  
  33. public int hashCode() { return 1; }
  34. }

emptyIterator和emptyListIterator返回空的迭代器,emptyIterator的代码为:

  1. public static <T> Iterator<T> emptyIterator() {
  2. return (Iterator<T>) EmptyIterator.EMPTY_ITERATOR;
  3. }

EmptyIterator是一个静态内部类,代码为:

  1. private static class EmptyIterator<E> implements Iterator<E> {
  2. static final EmptyIterator<Object> EMPTY_ITERATOR
  3. = new EmptyIterator<>();
  4.  
  5. public boolean hasNext() { return false; }
  6. public E next() { throw new NoSuchElementException(); }
  7. public void remove() { throw new IllegalStateException(); }
  8. }

以上这些代码都比较简单,就不赘述了。

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

  1. Collections.emptyList().add("hello");

会抛出异常UnsupportedOperationException。

如果返回值只是用于读取,可以使用emptyList方法,但如果返回值还用于写入,则需要新建一个对象。

其他空容器方法与emptyList类似,我们就不赘述了。它们都可以被用于方法返回值,以便调用者统一进行处理,同时节省时间和内存开销,它们的共同限制是返回值不能用于写入。

我们将空容器方法看做是适配器,是因为它将null或"空"转换为了容器对象。

单一对象方法

Collections中还有一组方法,可以将一个单独的对象转换为一个标准的容器接口对象,如下所示:

  1. public static <T> Set<T> singleton(T o)
  2. public static <T> List<T> singletonList(T o)
  3. public static <K,V> Map<K,V> singletonMap(K key, V value)

比如,可以这么用:

  1. Collection<String> coll = Collections.singleton("编程");
  2. Set<String> set = Collections.singleton("编程");
  3. List<String> list = Collections.singletonList("老马");
  4. Map<String, String> map = Collections.singletonMap("老马", "编程");

这些方法也经常用于构建方法返回值,相比新建容器对象并添加元素,这些方法更为简洁方便,此外,它们的实现更为高效,它们的实现类都针对单一对象进行了优化。比如,我们看singleton方法的代码:

  1. public static <T> Set<T> singleton(T o) {
  2. return new SingletonSet<>(o);
  3. }

新建了一个SingletonSet对象,SingletonSet是一个静态内部类,主要代码为:

  1. private static class SingletonSet<E>
  2. extends AbstractSet<E>
  3. {
  4. private final E element;
  5.  
  6. SingletonSet(E e) {element = e;}
  7.  
  8. public Iterator<E> iterator() {
  9. return singletonIterator(element);
  10. }
  11.  
  12. public int size() {return 1;}
  13.  
  14. public boolean contains(Object o) {return eq(o, element);}
  15. }

singletonIterator是一个内部方法,将单一对象转换为了一个迭代器接口对象,代码为:

  1. static <E> Iterator<E> singletonIterator(final E e) {
  2. return new Iterator<E>() {
  3. private boolean hasNext = true;
  4. public boolean hasNext() {
  5. return hasNext;
  6. }
  7. public E next() {
  8. if (hasNext) {
  9. hasNext = false;
  10. return e;
  11. }
  12. throw new NoSuchElementException();
  13. }
  14. public void remove() {
  15. throw new UnsupportedOperationException();
  16. }
  17. };
  18. }

eq方法就是比较两个对象是否相同,考虑了null的情况,代码为:

  1. static boolean eq(Object o1, Object o2) {
  2. return o1==null ? o2==null : o1.equals(o2);
  3. }

需要注意的是,singleton方法返回的也是不可变对象,只能用于读取,写入会抛出UnsupportedOperationException异常。

其他singletonXXX方法的实现思路是类似的,返回值也都只能用于读取,不能写入,我们就不赘述了。

除了用于构建返回值,这些方法还可用于构建方法参数。比如,从容器中删除对象,Collection有如下方法:

  1. boolean remove(Object o);
  2. boolean removeAll(Collection<?> c);

remove方法只会删除第一条匹配的记录,removeAll可以删除所有匹配的记录,但需要一个容器接口对象,如果需要从一个List中删除所有匹配的某一对象呢?这时,就可以使用Collections.singleton封装这个要删除的对象,比如,从list中删除所有的"b",代码如下所示:

  1. List<String> list = new ArrayList<>();
  2. Collections.addAll(list, "a", "b", "c", "d", "b");
  3. list.removeAll(Collections.singleton("b"));
  4. System.out.println(list);

其他方法

除了以上两组方法,Collections中还有如下适配器方法:

  1. //将Map接口转换为Set接口
  2. public static <E> Set<E> newSetFromMap(Map<E,Boolean> map)
  3. //将Deque接口转换为后进先出的队列接口
  4. public static <T> Queue<T> asLifoQueue(Deque<T> deque)
  5. //返回包含n个相同对象o的List接口
  6. public static <T> List<T> nCopies(int n, T o)

这些方法实际用的相对比较少,我们就不深入介绍了。

装饰器

装饰器接受一个接口对象,并返回一个同样接口的对象,不过,新对象可能会扩展一些新的方法或属性,扩展的方法或属性就是所谓的"装饰",也可能会对原有的接口方法做一些修改,达到一定的"装饰"目的。

Collections有三组装饰器方法,它们的返回对象都没有新的方法或属性,但改变了原有接口方法的性质,经过"装饰"后,它们更为安全了,具体分别是写安全、类型安全和线程安全,我们分别来看下。

写安全

这组方法有:

  1. public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c)
  2. public static <T> List<T> unmodifiableList(List<? extends T> list)
  3. public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m)
  4. public static <T> Set<T> unmodifiableSet(Set<? extends T> s)
  5. public static <K,V> SortedMap<K,V> unmodifiableSortedMap(SortedMap<K, ? extends V> m)
  6. public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<T> s)

顾名思义,这组unmodifiableXXX方法就是使容器对象变为只读的,写入会抛出UnsupportedOperationException异常。为什么要变为只读的呢?典型场景是,需要传递一个容器对象给一个方法,这个方法可能是第三方提供的,为避免第三方误写,所以在传递前,变为只读的,如下所示:

  1. public static void thirdMethod(Collection<String> c){
  2. c.add("bad");
  3. }
  4.  
  5. public static void mainMethod(){
  6. List<String> list = new ArrayList<>(Arrays.asList(
  7. new String[]{"a", "b", "c", "d"}));
  8. thirdMethod(Collections.unmodifiableCollection(list));
  9. }

这样,调用就会触发异常,从而避免了将错误数据插入。

这些方法是如何实现的呢?每个方法内部都对应一个类,这个类实现了对应的容器接口,它内部是待装饰的对象,只读方法传递给这个内部对象,写方法抛出异常。我们以unmodifiableCollection方法为例来看,代码为:

  1. public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) {
  2. return new UnmodifiableCollection<>(c);
  3. }

UnmodifiableCollection是一个静态内部类,代码为:

  1. static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
  2. private static final long serialVersionUID = 1820017752578914078L;
  3.  
  4. final Collection<? extends E> c;
  5.  
  6. UnmodifiableCollection(Collection<? extends E> c) {
  7. if (c==null)
  8. throw new NullPointerException();
  9. this.c = c;
  10. }
  11.  
  12. public int size() {return c.size();}
  13. public boolean isEmpty() {return c.isEmpty();}
  14. public boolean contains(Object o) {return c.contains(o);}
  15. public Object[] toArray() {return c.toArray();}
  16. public <T> T[] toArray(T[] a) {return c.toArray(a);}
  17. public String toString() {return c.toString();}
  18.  
  19. public Iterator<E> iterator() {
  20. return new Iterator<E>() {
  21. private final Iterator<? extends E> i = c.iterator();
  22.  
  23. public boolean hasNext() {return i.hasNext();}
  24. public E next() {return i.next();}
  25. public void remove() {
  26. throw new UnsupportedOperationException();
  27. }
  28. };
  29. }
  30.  
  31. public boolean add(E e) {
  32. throw new UnsupportedOperationException();
  33. }
  34. public boolean remove(Object o) {
  35. throw new UnsupportedOperationException();
  36. }
  37.  
  38. public boolean containsAll(Collection<?> coll) {
  39. return c.containsAll(coll);
  40. }
  41. public boolean addAll(Collection<? extends E> coll) {
  42. throw new UnsupportedOperationException();
  43. }
  44. public boolean removeAll(Collection<?> coll) {
  45. throw new UnsupportedOperationException();
  46. }
  47. public boolean retainAll(Collection<?> coll) {
  48. throw new UnsupportedOperationException();
  49. }
  50. public void clear() {
  51. throw new UnsupportedOperationException();
  52. }
  53. }

代码比较简单,其他unmodifiableXXX方法的实现也都类似,我们就不赘述了。

类型安全

所谓类型安全是指确保容器中不会保存错误类型的对象。容器怎么会允许保存错误类型的对象呢?我们看段代码:

  1. List list = new ArrayList<Integer>();
  2. list.add("hello");
  3. System.out.println(list);

我们创建了一个Integer类型的List对象,但添加了字符串类型的对象"hello",编译没有错误,运行也没有异常,程序输出为:

  1. [hello]

之所以会出现这种情况,是因为Java是通过擦除来实现泛型的,而且类型参数是可选的。正常情况下,我们会加上类型参数,让泛型机制来保证类型的正确性。但,由于泛型是Java 1.5以后才加入的,之前的代码可能没有类型参数,而新的代码可能需要与老的代码互动。

为了避免老的代码用错类型,确保在泛型机制失灵的情况下类型的正确性,可以在传递容器对象给老代码之前,使用如下方法"装饰"容器对象:

  1. public static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)
  2. public static <E> List<E> checkedList(List<E> list, Class<E> type)
  3. public static <K, V> Map<K, V> checkedMap(Map<K, V> m, Class<K> keyType, Class<V> valueType)
  4. public static <E> Set<E> checkedSet(Set<E> s, Class<E> type)
  5. public static <K,V> SortedMap<K,V> checkedSortedMap(SortedMap<K, V> m, Class<K> keyType, Class<V> valueType)
  6. public static <E> SortedSet<E> checkedSortedSet(SortedSet<E> s, Class<E> type)

使用这组checkedXXX方法,都需要传递类型对象,这些方法都会使容器对象的方法在运行时检查类型的正确性,如果不匹配,会抛出ClassCastException异常。比如:

  1. List list = new ArrayList<Integer>();
  2. list = Collections.checkedList(list, Integer.class);
  3. list.add("hello");

这次,运行就会抛出异常,从而避免错误类型的数据插入:

  1. java.lang.ClassCastException: Attempt to insert class java.lang.String element into collection with element type class java.lang.Integer

这些checkedXXX方法的实现机制是类似的,每个方法内部都对应一个类,这个类实现了对应的容器接口,它内部是待装饰的对象,大部分方法只是传递给这个内部对象,但对添加和修改方法,会首先进行类型检查,类型不匹配会抛出异常,类型匹配才传递给内部对象。以checkedCollection为例,我们来看下代码:

  1. public static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type) {
  2. return new CheckedCollection<>(c, type);
  3. }

CheckedCollection是一个静态内部类,主要代码为:

  1. static class CheckedCollection<E> implements Collection<E>, Serializable {
  2. private static final long serialVersionUID = 1578914078182001775L;
  3.  
  4. final Collection<E> c;
  5. final Class<E> type;
  6.  
  7. void typeCheck(Object o) {
  8. if (o != null && !type.isInstance(o))
  9. throw new ClassCastException(badElementMsg(o));
  10. }
  11.  
  12. private String badElementMsg(Object o) {
  13. return "Attempt to insert " + o.getClass() +
  14. " element into collection with element type " + type;
  15. }
  16.  
  17. CheckedCollection(Collection<E> c, Class<E> type) {
  18. if (c==null || type == null)
  19. throw new NullPointerException();
  20. this.c = c;
  21. this.type = type;
  22. }
  23.  
  24. public int size() { return c.size(); }
  25. public boolean isEmpty() { return c.isEmpty(); }
  26. public boolean contains(Object o) { return c.contains(o); }
  27. public Object[] toArray() { return c.toArray(); }
  28. public <T> T[] toArray(T[] a) { return c.toArray(a); }
  29. public String toString() { return c.toString(); }
  30. public boolean remove(Object o) { return c.remove(o); }
  31. public void clear() { c.clear(); }
  32.  
  33. public boolean containsAll(Collection<?> coll) {
  34. return c.containsAll(coll);
  35. }
  36. public boolean removeAll(Collection<?> coll) {
  37. return c.removeAll(coll);
  38. }
  39. public boolean retainAll(Collection<?> coll) {
  40. return c.retainAll(coll);
  41. }
  42.  
  43. public Iterator<E> iterator() {
  44. final Iterator<E> it = c.iterator();
  45. return new Iterator<E>() {
  46. public boolean hasNext() { return it.hasNext(); }
  47. public E next() { return it.next(); }
  48. public void remove() { it.remove(); }};
  49. }
  50.  
  51. public boolean add(E e) {
  52. typeCheck(e);
  53. return c.add(e);
  54. }
  55. }

代码比较简单,add方法中,会先调用typeCheck进行类型检查。其他checkedXXX方法的实现也都类似,我们就不赘述了。

线程安全

关于线程安全我们后续章节会详细介绍,这里简要说明下。之前我们介绍的各种容器类都不是线程安全的,也就是说,如果多个线程同时读写同一个容器对象,是不安全的。Collections提供了一组方法,可以将一个容器对象变为线程安全的,如下所示:

  1. public static <T> Collection<T> synchronizedCollection(Collection<T> c)
  2. public static <T> List<T> synchronizedList(List<T> list)
  3. public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
  4. public static <T> Set<T> synchronizedSet(Set<T> s)
  5. public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
  6. public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)

需要说明的,这些方法都是通过给所有容器方法加锁来实现的,这种实现并不是最优的,Java提供了很多专门针对并发访问的容器类,我们留待后续章节介绍。

小结

本节介绍了类Collections中的第二类方法,它们都返回一个容器接口对象,这些方法代表两种设计模式,一种是适配器,另一种是装饰器,我们介绍了这两种设计模式,以及这些方法的用法、适用场合和实现机制。

至此,关于容器类,我们就要介绍完了,下一节,让我们一起来回顾一下,进行简要总结。

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

Java编程的逻辑 (54) - 剖析Collections - 设计模式的更多相关文章

  1. Java编程的逻辑 (53) - 剖析Collections - 算法

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  2. 计算机程序的思维逻辑 (54) - 剖析Collections - 设计模式

    上节我们提到,类Collections中大概有两类功能,第一类是对容器接口对象进行操作,第二类是返回一个容器接口对象,上节我们介绍了第一类,本节我们介绍第二类. 第二类方法大概可以分为两组: 接受其他 ...

  3. Java编程的逻辑 (27) - 剖析包装类 (中)

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  4. Java编程的逻辑 (38) - 剖析ArrayList

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  5. Java编程的逻辑 (31) - 剖析Arrays

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  6. Java编程的逻辑 (51) - 剖析EnumSet

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  7. Java编程的逻辑 (26) - 剖析包装类 (上)

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  8. Java编程的逻辑 (28) - 剖析包装类 (下)

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  9. Java编程的逻辑 (32) - 剖析日期和时间

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

随机推荐

  1. 解决虚拟机vmware虚拟机安装64位系统“此主机支持 Intel VT-x,但 Intel VT-x 处于禁用状态”的问题

    前言 虚拟机使用的是VMware Workstation,并且首次在虚拟机体验64 位系统.在新建好虚拟机,运行时候就出现了VMware Workstation 的提醒:此主机支持 Intel VT- ...

  2. Source Insight中代码块注释

    转载:http://blog.csdn.net/cindylx422/article/details/7560786 修改工程目录下的utils.em文件,将下面代码添加到末尾,然后将此文件添加到工程 ...

  3. UnicodeDecodeError: 'utf-8' codec can't decode byte

    for line in open('u.item'): #read each line whenever I run this code it gives the following error: U ...

  4. springboot 中文文档

    springboot 中文文档https://qbgbook.gitbooks.io/spring-boot-reference-guide-zh/content/

  5. Appium升级后安装UnicodeIME-debug.apk 提示

    使用appium1.8 ,启动app过程中报错: 2018-05-08 17:09:16:890 - [W3C] Encountered internal error running command: ...

  6. jota-time 练习

    public static void main(String[] args) { LocalDate now = new LocalDate(); //输出形式为yyyy-MM-dd System.o ...

  7. java 时间间隔天数

    public static Long getDaysBetween(long startDate, long endDate) { Calendar fromCalendar = Calendar.g ...

  8. ios--网页js调用oc代码+传递参数+避免中文参数乱码的解决方案(实例)

    此解决方案原理: 1.在ViewController.h中声明方法和成员变量,以及webView的委托: // //  ViewController.h //  JS_IOS_01 // //  Cr ...

  9. [svc]traceroute(udp+icmp)&tracert(icmp)原理

    2018年4月11日 11:41:29更新 工具 发包 触发点 结局 traceroute 初始发udp包 ttl递增,icmp每一跳报ttl超时 udp端口不可达 tracert 初始发icmp r ...

  10. C++11 强枚举类型

    在标准C++11之前的枚举是继承C的,枚举类型不是类型安全的.枚举类型被视为整数,这使得两种不同的枚举类型之间可以进行比较. 一.C中enum类型的局限语法: enum type1{a, b, c}; ...