Java——通用容器类

通用容器类

具体容器类其实都不是从头构建的,它们都继承了一些抽象容器类。这些抽象类提供了容器接口的部分实现,方便了Java具体容器类的实现。此外,通过继承抽象类,自定义的类也可以更为容易地实现容器接口。为什么需要实现容器接口呢?至少有两个原因。

  1. 容器类是一个大家庭,它们之间可以方便地协作,比如很多方法的参数和返回值都是容器接口对象,实现了容器接口,就可以方便地参与这种协作。
  2. Java有一个类Collections,提供了很多针对容器接口的通用算法和功能,实现了容器接口,可以直接利用Collections中的算法和功能。

1、抽象容器类

抽象容器类与之前介绍的接口和具体容器类的关系如图所示。

虚线框表示接口,有Collection、List、Set、Queue、Deque和Map。有6个抽象容器类。

  1. AbstractCollection:实现了Collection接口,被抽象类AbstractList、AbstractSet、AbstractQueue继承,ArrayDeque也继承自AbstractCollection(图中未画出)。
  2. AbstractList:父类是AbstractCollection,实现了List接口,被ArrayList、Abstract-SequentialList继承。
  3. AbstractSequentialList:父类是AbstractList,被LinkedList继承。
  4. AbstractMap:实现了Map接口,被TreeMap、HashMap、EnumMap继承。
  5. AbstractSet:父类是AbstractCollection,实现了Set接口,被HashSet、TreeSet和EnumSet继承。
  6. AbstractQueue:父类是AbstractCollection,实现了Queue接口,被PriorityQueue继承。

1.1、AbstractCollection

AbstractCollection提供了Collection接口的基础实现,具体来说,它实现了如下方法:

java 复制代码
public boolean addAll(Collection<? extends E> c)
public boolean contains(Object o)
public boolean containsAll(Collection<? > c)
public boolean isEmpty()
public boolean remove(Object o)
public boolean removeAll(Collection<? > c)
public boolean retainAll(Collection<? > c)
public void clear()
public Object[] toArray()
public <T> T[] toArray(T[] a)
public String toString()

AbstractCollection又不知道数据是怎么存储的,它是如何实现这些方法的呢?它依赖于如下更为基础的方法:

java 复制代码
public boolean add(E e)
public abstract int size();
public abstract Iterator<E> iterator();

add方法的默认实现是:

java 复制代码
public boolean add(E e) {
    throw new UnsupportedOperationException();
}

抛出"操作不支持"异常,如果子类集合是不可被修改的,这个默认实现就可以了,否则,必须重写add方法。addAll方法的实现就是循环调用add方法。

size方法是抽象方法,子类必须重写。isEmpty方法就是检查size方法的返回值是否为0。toArray方法依赖size方法的返回值分配数组大小。

iterator方法也是抽象方法,它返回一个实现了迭代器接口的对象,子类必须重写。我们知道,迭代器定义了三个方法:

java 复制代码
boolean hasNext();
E next();
void remove();

如果子类集合是不可被修改的,迭代器不用实现remove方法,否则,三个方法都必须实现。

AbstractCollection中的大部分方法都是基于迭代器的方法实现的,比如contains方法,其代码为:

java 复制代码
public boolean contains(Object o) {
    Iterator<E> it = iterator();
    if(o==null) {
        while(it.hasNext())
            if(it.next()==null)
                return true;
    } else {
        while(it.hasNext())
            if(o.equals(it.next()))
                return true;
    }
    return false;
}

通过迭代器方法循环进行比较。

除了接口中的方法,Collection接口文档建议,每个Collection接口的实现类都应该提供至少两个标准的构造方法,一个是默认构造方法,另一个接受一个Collection类型的参数。

具体如何通过继承AbstractCollection来实现自定义容器呢?我们通过一个简单的例子来说明。我们使用在8.1节自己实现的动态数组容器类DynamicArray来实现一个简单的Collection。

DynamicArray当时没有实现根据索引添加和删除的方法,我们先来补充一下,如代码所示。

java 复制代码
public class DynamicArray<E> {
    //...
    public E remove(int index) {
        E oldValue = get(index);
        int numMoved = size - index - 1;
        if(numMoved > 0)
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        elementData[--size] = null;
        return oldValue;
    }
    public void add(int index, E element) {
        ensureCapacity(size + 1);
        System.arraycopy(elementData, index, elementData, index + 1,
                          size - index);
        elementData[index] = element;
        size++;
    }
}

基于DynamicArray,我们实现一个简单的迭代器类DynamicArrayIterator,如代所示。

java 复制代码
public class DynamicArrayIterator<E>   implements Iterator<E>{
   DynamicArray<E> darr;
   int cursor;
   int lastRet = -1;
   public DynamicArrayIterator(DynamicArray<E> darr){
       this.darr = darr;
   }
   @Override
   public boolean hasNext() {
         return cursor ! = darr.size();
   }
   @Override
   public E next() {
       int i = cursor;
       if(i >= darr.size())
           throw new NoSuchElementException();
       cursor = i + 1;
       lastRet = i;
       return darr.get(i);
   }
   @Override
   public void remove() {
       if(lastRet < 0)
           throw new IllegalStateException();
         darr.remove(lastRet);
         cursor = lastRet;
         lastRet = -1;
     }
 }

代码很简单,就不解释了,为简单起见,我们没有实现实际容器类中的有关检测结构性变化的逻辑。

基于DynamicArray和DynamicArrayIterator,通过继承AbstractCollection,我们来实现一个简单的容器类MyCollection,如代码所示。

java 复制代码
public class MyCollection<E> extends AbstractCollection<E> {
    DynamicArray<E> darr;
    public MyCollection(){
        darr = new DynamicArray<>();
    }
    public MyCollection(Collection<? extends E> c){
        this();
        addAll(c);
    }
    @Override
    public Iterator<E> iterator() {
        return new DynamicArrayIterator<>(darr);
    }
    @Override
    public int size() {
        return darr.size();
    }
    @Override
    public boolean add(E e) {
        darr.add(e);
        return true;
    }
}

代码很简单,就是按建议提供了两个构造方法,并重写了size、add和iterator方法,这些方法内部使用了DynamicArray和DynamicArrayIterator。

1.2、AbstractList

AbstractList提供了List接口的基础实现,具体来说,它实现了如下方法:

java 复制代码
public boolean add(E e)
public boolean addAll(int index, Collection<? extends E> c)
public void clear()
public boolean equals(Object o)
public int hashCode()
public int indexOf(Object o)
public Iterator<E> iterator()
public int lastIndexOf(Object o)
public ListIterator<E> listIterator()
public ListIterator<E> listIterator(final int index)
public List<E> subList(int fromIndex, int toIndex)

AbstractList是怎么实现这些方法的呢?它依赖于如下更为基础的方法:

java 复制代码
public abstract int size();
abstract public E get(int index);
public E set(int index, E element)
public void add(int index, E element)
public E remove(int index)

size方法与AbstractCollection一样,也是抽象方法,子类必须重写。get方法根据索引index获取元素,它也是抽象方法,子类必须重写。

set、add、remove方法都是修改容器内容,它们不是抽象方法,但默认实现都是抛出异常UnsupportedOperationException。如果子类容器不可被修改,这个默认实现就可以了。如果可以根据索引修改内容,应该重写set方法。如果容器是长度可变的,应该重写add和remove方法。

与AbstractCollection不同,继承AbstractList不需要实现迭代器类和相关方法, AbstractList内部实现了两个迭代器类,一个实现了Iterator接口,另一个实现了ListIterator接口,它们是基于以上的这些基础方法实现的,逻辑比较简单,就不赘述了。

具体如何扩展AbstractList呢?我们来看个例子,也通过DynamicArray来实现一个简单的List,如代码所示。

java 复制代码
public class MyList<E> extends AbstractList<E> {
   private DynamicArray<E> darr;
   public MyList(){
       darr = new DynamicArray<>();
   }
   public MyList(Collection<? extends E> c){
       this();
       addAll(c);
   }
   @Override
   public E get(int index) {
       return darr.get(index);
   }
   @Override
   public int size() {
       return darr.size();
   }
   @Override
     public E set(int index, E element) {
         return darr.set(index, element);
     }
     @Override
     public void add(int index, E element) {
         darr.add(index, element);
     }
     @Override
     public E remove(int index) {
         return darr.remove(index);
     }
 }

代码很简单,就是按建议提供了两个构造方法,并重写了size、get、set、add和remove方法,这些方法内部使用了DynamicArray。

1.3、AbstractSequentialList

AbstractSequentialList是AbstractList的子类,也提供了List接口的基础实现,具体来说,它实现了如下方法:

java 复制代码
public void add(int index, E element)
public boolean addAll(int index, Collection<? extends E> c)
public E get(int index)
public Iterator<E> iterator()
public E remove(int index)
public E set(int index, E element)

可以看出,它实现了根据索引位置进行操作的get、set、add、remove方法,它是怎么实现的呢?它是基于ListIterator接口的方法实现的,在AbstractSequentialList中,listIterator方法被重写为了一个抽象方法:

java 复制代码
public abstract ListIterator<E> listIterator(int index)

子类必须重写该方法,并实现迭代器接口。

我们来看段具体的代码,看get、set、add、remove是如何基于ListIterator实现的。get方法代码为:

java 复制代码
public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

代码很简单,其他方法也都类似,就不赘述了。

注意与AbstractList相区别,可以说,虽然AbstractSequentialList是AbstractList的子类,但实现逻辑和用法上,与AbstractList正好相反。

  • AbstractList需要具体子类重写根据索引操作的方法get、set、add、remove,它提供了迭代器,但迭代器是基于这些方法实现的。它假定子类可以高效地根据索引位置进行操作,适用于内部是随机访问类型的存储结构(如数组),比如ArrayList就继承自AbstractList。
  • AbstractSequentialList需要具体子类重写迭代器,它提供了根据索引操作的方法get、set、add、remove,但这些方法是基于迭代器实现的。它适用于内部是顺序访问类型的存储结构(如链表),比如LinkedList就继承自AbstractSequentialList。

具体如何扩展AbstractSequentialList呢?我们还是以DynamicArray举例来说明,在实际应用中,如果内部存储结构类似DynamicArray,应该继承AbstractList,这里主要是演示其用法。

扩展AbstractSequentialList需要实现ListIterator,前面介绍的DynamicArrayIterator只实现了Iterator接口,通过继承DynamicArrayIterator,我们实现一个新的实现了List-Iterator接口的类DynamicArrayListIterator,如代码所示。

java 复制代码
public class DynamicArrayListIterator<E>
    extends DynamicArrayIterator<E> implements ListIterator<E>{
    public DynamicArrayListIterator(int index, DynamicArray<E> darr){
        super(darr);
        this.cursor = index;
    }
    @Override
    public boolean hasPrevious() {
        return cursor > 0;
    }
    @Override
    public E previous() {
        if(! hasPrevious())
            throw new NoSuchElementException();
        cursor--;
        lastRet = cursor;
        return darr.get(lastRet);
    }
    @Override
    public int nextIndex() {
        return cursor;
    }
    @Override
    public int previousIndex() {
        return cursor - 1;
    }
    @Override
    public void set(E e) {
          if(lastRet==-1){
                throw new IllegalStateException();
          }
          darr.set(lastRet, e);
      }
      @Override
      public void add(E e) {
          darr.add(cursor, e);
          cursor++;
          lastRet = -1;
      }
  }

逻辑比较简单,就不解释了。有了DynamicArrayListIterator,我们看基于Abstract-SequentialList的List实现,如代码所示。

java 复制代码
public class MySeqList<E> extends AbstractSequentialList<E> {
    private DynamicArray<E> darr;
    public MySeqList(){
        darr = new DynamicArray<>();
    }
    public MySeqList(Collection<? extends E> c){
        this();
        addAll(c);
    }
    @Override
    public ListIterator<E> listIterator(int index) {
        return new DynamicArrayListIterator<>(index, darr);
    }
    @Override
    public int size() {
        return darr.size();
    }
}

代码很简单,就是按建议提供了两个构造方法,并重写了size和listIterator方法,迭代器的实现是DynamicArrayListIterator。

1.4、AbstractMap

AbstractMap提供了Map接口的基础实现,具体来说,它实现了如下方法:

java 复制代码
public void clear()
public boolean containsKey(Object key)
public boolean containsValue(Object value)
public boolean equals(Object o)
public V get(Object key)
public int hashCode()
public boolean isEmpty()
public Set<K> keySet()
public void putAll(Map<? extends K, ? extends V> m)
public V remove(Object key)
public int size()
public String toString()
public Collection<V> values()

AbstractMap是如何实现这些方法的呢?它依赖于如下更为基础的方法:

java 复制代码
public V put(K key, V value)
public abstract Set<Entry<K, V>> entrySet();

putAll就是循环调用put。put方法的默认实现是抛出异常UnsupportedOperation-Exception,如果Map是允许写入的,则需要重写该方法。

其他方法都基于entrySet方法。entrySet方法是一个抽象方法,子类必须重写,它返回所有键值对的Set视图,如果Map是允许删除的,这个Set的迭代器实现类,即entrySet(). iterator()的返回对象,必须实现迭代器的remove方法,这是因为AbstractMap的remove方法是通过entrySet().iterator().remove()实现的。

除了提供基础方法的实现,AbstractMap类内部还定义了两个公有的静态内部类,表示键值对:

java 复制代码
AbstractMap.SimpleEntry implements Entry<K, V>
AbstractMap.SimpleImmutableEntry implements Entry<K, V>

SimpleImmutableEntry用于表示只读的键值对,而SimpleEntry用于表示可写的。

Map接口文档建议:每个Map接口的实现类都应该提供至少两个标准的构造方法,一个是默认构造方法,另一个接受一个Map类型的参数。

具体如何扩展AbstractMap呢?我们定义一个简单的Map实现类MyMap,内部还是用DynamicArray,如代码所示。

java 复制代码
public class MyMap<K, V> extends AbstractMap<K, V> {
   private DynamicArray<Map.Entry<K, V>> darr;
   private Set<Map.Entry<K, V>> entrySet = null;
   public MyMap() {
       darr = new DynamicArray<>();
   }
   public MyMap(Map<? extends K, ? extends V> m) {
       this();
       putAll(m);
   }
   @Override
   public Set<Entry<K, V>> entrySet() {
       Set<Map.Entry<K, V>> es = entrySet;
       return es ! = null ? es : (entrySet = new EntrySet());
     }
     @Override
     public V put(K key, V value) {
         for(int i = 0; i < darr.size(); i++) {
             Map.Entry<K, V> entry = darr.get(i);
             if((key == null && entry.getKey() == null)
                     || (key ! = null && key.equals(entry.getKey()))) {
                 V oldValue = entry.getValue();
                 entry.setValue(value);
                 return oldValue;
             }
         }
         Map.Entry<K, V> newEntry = new AbstractMap.SimpleEntry<>(key, value);
         darr.add(newEntry);
         return null;
     }
     class EntrySet extends AbstractSet<Map.Entry<K, V>> {
         public Iterator<Map.Entry<K, V>> iterator() {
             return new DynamicArrayIterator<Map.Entry<K, V>>(darr);
         }
         public int size() {
             return darr.size();
         }
     }
 }

我们定义了两个构造方法,实现了put和entrySet方法。put方法先通过循环查找是否已存在对应的键,如果存在则修改值,否则新建一个键值对(类型为AbstractMap.Simple-Entry)并添加。entrySet返回的类型是一个内部类EntrySet,它继承自AbstractSet,重写了size和iterator方法,iterator方法中,返回的是迭代器类型是DynamicArrayIterator,它支持remove方法。

1.5、AbstractSet

AbstractSet提供了Set接口的基础实现,它继承自AbstractCollection,增加了equals和hashCode方法的默认实现。Set接口要求容器内不能包含重复元素,AbstractSet并没有实现该约束,子类需要自己实现。

扩展AbstractSet与AbstractCollection是类似的,只是需要实现无重复元素的约束,比如,add方法内需要检查元素是否已经添加过了。具体实现比较简单,我们就不赘述了。

1.6、AbstractQueue

AbstractQueue提供了Queue接口的基础实现,它继承自AbstractCollection,实现了如下方法:

java 复制代码
public boolean add(E e)
public boolean addAll(Collection<? extends E> c)
public void clear()
public E element()
public E remove()

这些方法是基于Queue接口的其他方法实现的,包括:

java 复制代码
E peek();
E poll();
boolean offer(E e);

扩展AbstractQueue需要实现这些方法,具体逻辑也比较简单,我们就不赘述了。

2、Collections

类Collections以静态方法的方式提供了很多通用算法和功能,这些功能大概可以分为两类。

  1. 对容器接口对象进行操作。
    • 查找和替换。
    • 排序和调整顺序。
    • 添加和修改。
  2. 返回一个容器接口对象。
    • 适配器:将其他类型的数据转换为容器接口对象。
    • 装饰器:修饰一个给定容器接口对象,增加某种性质。

2.1、查找和替换

查找和替换包含多组方法。查找包括二分查找、查找最大值/最小值、查找元素出现次数、查找子List、查看两个集合是否有交集等,下面具体介绍。

2.1.1、二分查找

我们在介绍Arrays类的时候介绍过二分查找,Arrays类有针对数组对象的二分查找方法,Collections提供了针对List接口的二分查找,如下所示:

java 复制代码
public static <T> int binarySearch(
List<? extends Comparable<? super T>> list, T key)
public static <T> int binarySearch(List<? extends T> list,
T key, Comparator<? super T> c)

从方法参数角度而言,一个要求List的每个元素实现Comparable接口,另一个不需要,但要求提供Comparator。二分查找假定List中的元素是从小到大排序的。如果是从大到小排序的,需要传递一个逆序Comparator对象,Collections提供了返回逆序Comparator的方法,之前我们也用过:

java 复制代码
public static <T> Comparator<T> reverseOrder()
public static <T> Comparator<T> reverseOrder(Comparator<T> cmp)

比如,可以这么用:

java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(new Integer[]{
    35, 24, 13, 12, 8, 7, 1
}));
System.out.println(Collections.binarySearch(list, 7,
Collections.reverseOrder()));

输出为:5。List的二分查找的基本思路与Arrays中的是一样的,数组可以根据索引直接定位任意元素,实现效率很高,但List就不一定了,具体分为两种情况,如果List可以随机访问(如数组)​,即实现了RandomAccess接口,或者元素个数比较少,则实现思路与Arrays一样,根据索引直接访问中间元素进行比较,否则使用迭代器的方式移动到中间元素进行比较。从效率角度,如果List支持随机访问,效率为O(log2(N)),如果通过迭代器,那么比较的次数为O(log2(N)),但遍历移动的次数为O(N), N为列表长度。

2.1.2、查找最大值/最小值

Collections提供了如下查找最大值/最小值的方法(省略了修饰符public static)​:

java 复制代码
<T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
<T> T max(Collection<? extends T> coll, Comparator<? super T> comp)
<T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll)
<T> T min(Collection<? extends T> coll, Comparator<? super T> comp)

含义和用法都很直接,实现思路也很简单,就是通过迭代器进行比较,比如:

java 复制代码
public static <T extends Object & Comparable<? super T>> T max(
      Collection<? extends T> coll) {
    Iterator<? extends T> i = coll.iterator();
    T candidate = i.next();
    while(i.hasNext()) {
        T next = i.next();
        if(next.compareTo(candidate) > 0)
            candidate = next;
    }
    return candidate;
}
2.1.3、其他方法

查找元素出现次数,方法为:

java 复制代码
public static int frequency(Collection<? > c, Object o)

返回元素o在容器c中出现的次数,o可以为null。含义很简单,实现思路也很简单,就是通过迭代器进行比较计数。

Collections提供了如下方法,在source List中查找target List的位置:

java 复制代码
public static int indexOfSubList(List<? > source, List<? > target)
public static int lastIndexOfSubList(List<? > source, List<? > target)

indexOfSubList从开头找,lastIndexOfSubList从结尾找,没找到返回-1,找到返回第一个匹配元素的索引位置。这两个方法的实现都是属于"暴力破解"型的,将target列表与source从第一个元素开始的列表逐个元素进行比较,如果不匹配,则与source从第二个元素开始的列表比较,再不匹配,与source从第三个元素开始的列表比较,以此类推。

查看两个集合是否有交集,方法为:

java 复制代码
public static boolean disjoint(Collection<? > c1, Collection<? > c2)

如果c1和c2有交集,返回值为false;没有交集,返回值为true。实现原理也很简单,遍历其中一个容器,对每个元素,在另一个容器里通过contains方法检查是否包含该元素,如果包含,返回false,如果最后不包含任何元素返回true。这个方法的代码会根据容器是否为Set以及集合大小进行性能优化,即选择哪个容器进行遍历,哪个容器进行检查,以减少总的比较次数,具体我们就不介绍了。

替换方法为:

java 复制代码
public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)

将List中的所有oldVal替换为newVal,如果发生了替换,返回值为true,否则为false。用法和实现都比较简单,就不赘述了。

2.2、排序和调整顺序

2.2.1、排序、交换位置与翻转

Arrays类有针对数组对象的排序方法,Collections提供了针对List接口的排序方法,如下所示:

java 复制代码
public static <T extends Comparable<? super T>> void sort(List<T> list)
public static <T> void sort(List<T> list, Comparator<? super T> c)

使用很简单,就不举例了,内部它是通过Arrays.sort实现的,先将List元素复制到一个数组中,然后使用Arrays.sort,排序后,再复制回List。代码如下所示:

java 复制代码
public static <T extends Comparable<? super T>> void sort(List<T> list) {
    Object[] a = list.toArray();
    Arrays.sort(a);
    ListIterator<T> i = list.listIterator();
    for(int j=0; j<a.length; j++) {
        i.next();
        i.set((T)a[j]);
    }
}

交换元素位置的方法为:

java 复制代码
public static void swap(List<? > list, int i, int j)

交换list中第i个和第j个元素的内容。实现代码为:

java 复制代码
public static void swap(List<? > list, int i, int j) {
    final List l = list;
    l.set(i, l.set(j, l.get(i)));
}

翻转列表顺序的方法为:

java 复制代码
public static void reverse(List<? > list)

将list中的元素顺序翻转过来。实现思路就是将第一个和最后一个交换,第二个和倒数第二个交换,以此类推,直到中间两个元素交换完毕。如果list实现了RandomAccess接口或列表比较小,根据索引位置,使用上面的swap方法进行交换,否则,由于直接根据索引位置定位元素效率比较低,使用一前一后两个listIterator定位待交换的元素,具体代码为:

java 复制代码
public static void reverse(List<? > list) {
  int size = list.size();
  if(size < REVERSE_THRESHOLD || list instanceof RandomAccess) {
        for(int i=0, mid=size>>1, j=size-1; i<mid; i++, j--)
            swap(list, i, j);
    } else {
        ListIterator fwd = list.listIterator();
        ListIterator rev = list.listIterator(size);
        for(int i=0, mid=list.size()>>1; i<mid; i++) {
            Object tmp = fwd.next();
            fwd.set(rev.previous());
            rev.set(tmp);
        }
    }
}
2.2.2、随机化排序

我们在随机一节介绍过洗牌算法,Collections直接提供了对List元素洗牌的方法:

java 复制代码
public static void shuffle(List<? > list)
public static void shuffle(List<? > list, Random rnd)

实现思路与随机一节介绍的是一样的:从后往前遍历列表,逐个给每个位置重新赋值,值从前面的未重新赋值的元素中随机挑选。如果列表实现了RandomAccess接口,或者列表比较小,直接使用前面swap方法进行交换,否则,先将列表内容复制到一个数组中,洗牌,再复制回列表。代码为:

java 复制代码
public static void shuffle(List<? > list, Random rnd) {
    int size = list.size();
    if(size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
        for(int i=size; i>1; i--)
            swap(list, i-1, rnd.nextInt(i));
    } else {
        Object arr[] = list.toArray();
        //对数组进行洗牌
        for(int i=size; i>1; i--)
            swap(arr, i-1, rnd.nextInt(i));
        //将数组中洗牌后的结果保存回list
        ListIterator it = list.listIterator();
        for(int i=0; i<arr.length; i++) {
            it.next();
            it.set(arr[i]);
        }
    }
}
2.2.3、循环位移

我们解释下循环移位的概念,比如列表为:

复制代码
        [8, 5, 3, 6, 2]

循环右移2位,会变为:

复制代码
        [6, 2, 8, 5, 3]

如果是循环左移2位,会变为:

复制代码
        [3, 6, 2, 8, 5]

因为列表长度为5,循环左移3位和循环右移2位的效果是一样的。

循环移位的方法是:

java 复制代码
public static void rotate(List<? > list, int distance)

distance表示循环移位个数,一般正数表示向右移,负数表示向左移,比如:

java 复制代码
List<Integer> list1 = Arrays.asList(new Integer[]{
        8, 5, 3, 6, 2
});
Collections.rotate(list1, 2);
System.out.println(list1);
List<Integer> list2 = Arrays.asList(new Integer[]{
        8, 5, 3, 6, 2
});
Collections.rotate(list2, -2);
System.out.println(list2);

输出为:

复制代码
[6, 2, 8, 5, 3]
[3, 6, 2, 8, 5]

这个方法很有用的一点是:它也可以用于子列表,可以调整子列表内的顺序而不改变其他元素的位置。比如,将第j个元素向前移动到k(k>j)​,可以这么写:

java 复制代码
Collections.rotate(list.subList(j, k+1), -1);

再举个例子:

java 复制代码
List<Integer> list = Arrays.asList(new Integer[]{
        8, 5, 3, 6, 2, 19, 21
});
Collections.rotate(list.subList(1, 5), 2);
System.out.println(list);

输出为:

复制代码
        [8, 6, 2, 5, 3, 19, 21]

这个类似于列表内的"剪切"和"粘贴"​,将子列表[5,3]​"剪切", "粘贴"到2后面。如果需要实现类似"剪切"和"粘贴"的功能,可以使用rotate()方法。

限于篇幅,我们只解释下其中的第二个算法,它将循环移位看作列表的两个子列表进行顺序交换。再来看上面的例子,循环左移2位:

复制代码
        [8, 5, 3, 6, 2] -> [3, 6, 2, 8, 5]

就是将[8, 5]和[3, 6, 2]两个子列表的顺序进行交换。循环右移两位:

复制代码
        [8, 5, 3, 6, 2] -> [6, 2, 8, 5, 3]

就是将[8, 5, 3]和[6, 2]两个子列表的顺序进行交换。

根据列表长度size和移位个数distance,可以计算出两个子列表的分隔点,有了两个子列表后,两个子列表的顺序交换可以通过三次翻转实现。比如,有A和B两个子列表,A有m个元素,B有n个元素:a1a2...amb1b2...bn,要变为b1b2...bna1a2...am,可经过三次翻转实现:

  1. 翻转子列表A
    a1a2...amb1b2...bn→ am...a2a1b1b2...bn
  2. 翻转子列表B
    am...a2a1b1b2...bn→ am...a2a1bn...b2b1
  3. 翻转整个列表
    am...a2a1bn...b2b1→ b1b2...bna1a2...am

这个算法的整体实现代码为:

java 复制代码
private static void rotate2(List<? > list, int distance) {
    int size = list.size();
    if(size == 0)
        return;
    int mid =   -distance % size;
    if(mid < 0)
        mid += size;
    if(mid == 0)
        return;
    reverse(list.subList(0, mid));
    reverse(list.subList(mid, size));
    reverse(list);
}

mid为两个子列表的分割点,调用了三次reverse方法以实现子列表顺序交换。

2.3、添加和修改

批量添加,方法为:

java 复制代码
public static <T> boolean addAll(Collection<? super T> c, T... elements)

elements为可变参数,将所有元素添加到容器c中。这个方法很方便,比如:

java 复制代码
List<String> list = new ArrayList<String>();
String[] arr = new String[]{"深入", "浅出"};
Collections.addAll(list, "hello", "world", "老马", "编程");
Collections.addAll(list, arr);
System.out.println(list);

输出为:

复制代码
[hello, world, 老马, 编程, 深入, 浅出]

批量填充固定值,方法为:

java 复制代码
public static <T> void fill(List<? super T> list, T obj)

这个方法与Arrays类中的fill方法是类似的,给每个元素设置相同的值。

批量复制,方法为:

java 复制代码
public static <T> void copy(List<? super T> dest, List<? extends T> src)

将列表src中的每个元素复制到列表dest的对应位置处,覆盖dest中原来的值,dest的列表长度不能小于src,dest中超过src长度部分的元素不受影响。

2.4、适配器

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

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

它们接受其他类型的数据,转换为一个容器接口,目的是使其他类型的数据更为方便地参与到容器类协作体系中,下面,我们分别来看下。

2.4.1、空容器方法

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

java 复制代码
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对象。比如,可以这么用:

java 复制代码
List<String> list = Collections.emptyList();
Map<String, Integer> map = Collections.emptyMap();
Set<Integer> set = Collections.emptySet();

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

java 复制代码
public static List<Integer> asList(int... elements)

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

java 复制代码
int[] arr = ...; //从别的地方获取到的arr
List<Integer> list = asList(arr);
if(list==null){
    //...
}else{
    //...
}

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

java 复制代码
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。也可以这样实现:

java 复制代码
return new ArrayList<Integer>();

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

java 复制代码
public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}

EMPTY_LIST的定义为:

java 复制代码
public static final List EMPTY_LIST = new EmptyList<>();

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

java 复制代码
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的代码为:

java 复制代码
public static <T> Iterator<T> emptyIterator() {
    return (Iterator<T>) EmptyIterator.EMPTY_ITERATOR;
}

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

java 复制代码
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不支持修改操作,比如:

java 复制代码
Collections.emptyList().add("hello");

会抛出异常UnsupportedOperationException。

如果返回值只是用于读取,可以使用emptyList方法,但如果返回值还用于写入,则需要新建一个对象。其他空容器方法与emptyList方法类似,我们就不赘述了。它们都可以被用于方法返回值,以便调用者统一进行处理,同时节省时间和内存开销,它们的共同限制是返回值不能用于写入。我们将空容器方法看作适配器,是因为它将null或"空"转换为了容器对象。

需要说明的是,在Java 9中,可以使用List、Map和Set不带参数的of方法返回一个空的只读容器对象,也就是说,如下两行代码的效果是相同的:

java 复制代码
1. List list = Collections.emptyList();
2. List list = List.of();
2.4.2、单一对象方法

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

java 复制代码
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)

比如,可以这么用:

java 复制代码
Collection<String> coll = Collections.singleton("编程");
Set<String> set = Collections.singleton("编程");
List<String> list = Collections.singletonList("老马");
Map<String, String> map = Collections.singletonMap("老马", "编程");

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

java 复制代码
public static <T> Set<T> singleton(T o) {
    return new SingletonSet<>(o);
}

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

java 复制代码
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是一个内部方法,将单一对象转换为了一个迭代器接口对象,代码为:

java 复制代码
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的情况,代码为:

java 复制代码
static boolean eq(Object o1, Object o2) {
    return o1==null ? o2==null : o1.equals(o2);
}

需要注意的是,singleton方法返回的也是不可变对象,只能用于读取,写入会抛出UnsupportedOperationException异常。其他singletonⅩⅩⅩ方法的实现思路是类似的,返回值也都只能用于读取,不能写入,我们就不赘述了。

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

java 复制代码
boolean remove(Object o);
boolean removeAll(Collection<? > c);

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

java 复制代码
List<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "b");
list.removeAll(Collections.singleton("b"));
System.out.println(list);

需要说明的是,在Java 9中,可以使用List、Map和Set的of方法达到singleton同样的功能,也就是说,如下两行代码的效果是相同的:

java 复制代码
1. Set<String> b = Collections.singleton("b");
2. Set<String> b = Set.of("b");

除了以上两组方法,Collections中还有如下适配器方法,用的相对较少,我们就不详细介绍了。

java 复制代码
//将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)

2.5、装饰器

装饰器接受一个接口对象,并返回一个同样接口的对象,不过,新对象可能会扩展一些新的方法或属性,扩展的方法或属性就是所谓的"装饰"​,也可能会对原有接口方法做一些修改,达到一定的"装饰"目的。Collections有三组装饰器方法,它们的返回对象都没有新的方法或属性,但改变了原有接口方法的性质,经过"装饰"后,它们更为安全了,具体分别是写安全、类型安全和线程安全,我们分别来看下。

2.5.1、写安全

写安全的主要方法有:

java 复制代码
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)

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

java 复制代码
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方法的代码为:

java 复制代码
public static <T> Collection<T> unmodifiableCollection(
    Collection<? extends T> c) {
    return new UnmodifiableCollection<>(c);
}

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

java 复制代码
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();
      }
  }

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

2.5.2、类型安全

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

java 复制代码
List list = new ArrayList<Integer>();
list.add("hello");
System.out.println(list);

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

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

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

java 复制代码
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)

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

java 复制代码
        List list = new ArrayList<Integer>();
        list = Collections.checkedList(list, Integer.class);
        list.add("hello");

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

java 复制代码
java.lang.ClassCastException:  Attempt  to  insert  class  java.lang.String  element
    into collection with element type class java.lang.Integer

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

以checkedCollection为例,我们来看下代码:

java 复制代码
public static <E> Collection<E> checkedCollection(
    Collection<E> c, Class<E> type) {
    return new CheckedCollection<>(c, type);
}

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

java 复制代码
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进行类型检查。其他checkedⅩⅩⅩ方法的实现也都类似,我们就不赘述了。

2.5.3、线程安全

Collections提供了一组方法,可以将一个容器对象变为线程安全的,比如:

java 复制代码
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)

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

3、容器类总结

3.1、用法和特点

容器类有两个根接口,分别是Collection和Map, Collection表示单个元素的集合,Map表示键值对的集合。

Collection表示的数据集合有基本的增、删、查、遍历等方法,但没有定义元素间的顺序或位置,也没有规定是否有重复元素。

List是Collection的子接口,表示有顺序或位置的数据集合,增加了根据索引位置进行操作的方法。它有两个主要的实现类:ArrayList和LinkedList。ArrayList基于数组实现, LinkedList基于链表实现;ArrayList的随机访问效率很高,但从中间插入和删除元素需要移动元素,效率比较低,LinkedList则正好相反,随机访问效率比较低,但增删元素只需要调整邻近节点的链接。

Set也是Collection的子接口,它没有增加新的方法,但保证不含重复元素。它有两个主要的实现类:HashSet和TreeSet。HashSet基于哈希表实现,要求键重写hashCode方法,效率更高,但元素间没有顺序;TreeSet基于排序二叉树实现,元素按比较有序,元素需要实现Comparable接口,或者创建TreeSet时提供一个Comparator对象。HashSet还有一个子类LinkedHashSet可以按插入有序。还有一个针对枚举类型的实现类EnumSet,它基于位向量实现,效率很高。

Queue是Collection的子接口,表示先进先出的队列,在尾部添加,从头部查看或删除。Deque是Queue的子接口,表示更为通用的双端队列,有明确的在头或尾进行查看、添加和删除的方法。普通队列有两个主要的实现类:LinkedList和ArrayDeque。LinkedList基于链表实现,ArrayDeque基于循环数组实现。一般而言,如果只需要Deque接口,Array-Deque的效率更高一些。

Queue还有一个特殊的实现类PriorityQueue,表示优先级队列,内部是用堆实现的。堆除了用于实现优先级队列,还可以高效方便地解决很多其他问题,比如求前K个最大的元素、求中值等。

Map接口表示键值对集合,经常根据键进行操作,它有两个主要的实现类:HashMap和TreeMap。HashMap基于哈希表实现,要求键重写hashCode方法,操作效率很高,但元素没有顺序。TreeMap基于排序二叉树实现,要求键实现Comparable接口,或提供一个Comparator对象,操作效率稍低,但可以按键有序。

HashMap还有一个子类LinkedHashMap,它可以按插入或访问有序。之所以能有序,是因为每个元素还加入到了一个双向链表中。如果键本来就是有序的,使用LinkedHashMap而非TreeMap可以提高效率。按访问有序的特点可以方便地用于实现LRU缓存。

如果键为枚举类型,可以使用专门的实现类EnumMap,它使用效率更高的数组实现。

需要说明的是,除了Hashtable、Vector和Stack,我们介绍的各种容器类都不是线程安全的,也就是说,如果多个线程同时读写同一个容器对象,是不安全的。如果需要线程安全,可以使用Collections提供的synchronizedⅩⅩⅩ方法对容器对象进行同步,或者使用线程安全的专门容器类。

此外,容器类提供的迭代器都有一个特点,都会在迭代中间进行结构性变化检测,如果容器发生了结构性变化,就会抛出ConcurrentModificationException,所以不能在迭代中间直接调用容器类提供的add/remove方法,如需添加和删除,应调用迭代器的相关方法。

在解决一个特定问题时,经常需要综合使用多种容器类。比如,要统计一本书中出现次数最多的前10个单词,可以先使用HashMap统计每个单词出现的次数,再使用TopK类用PriorityQueue求前10个单词,或者使用Collections提供的sort方法。

为简单起见,容器中的元素类型往往是简单的,但需要说明的是,它们也可以是复杂的自定义类型,还可以是容器类型。比如在一个新闻应用中,表示当天的前十大新闻可以用一个List表示,形如List,而为了表示每个分类的前十大新闻,可以用一个Map表示,键为分类Category,值为List,形如Map<Category, List>,而表示每天的每个分类的前十大新闻,可以在Map中使用Map,键为日期,值也是一个Map,形如Map<Date,Map<Category, List>。

3.2、数据结构和算法

在容器类中,我们看到了如下数据结构的应用:

  1. 动态数组:ArrayList内部就是动态数组,HashMap内部的链表数组也是动态扩展的,ArrayDeque和PriorityQueue内部也都是动态扩展的数组。
  2. 链表:LinkedList是用双向链表实现的,HashMap中映射到同一个链表数组的键值对是通过单向链表链接起来的,LinkedHashMap中每个元素还加入到了一个双向链表中以维护插入或访问顺序。
  3. 哈希表:HashMap是用哈希表实现的,HashSet、LinkedHashSet和LinkedHashMap基于HashMap,内部当然也是哈希表。
  4. 排序二叉树:TreeMap是用红黑树(基于排序二叉树)实现的,TreeSet内部使用TreeMap,当然也是红黑树,红黑树能保持元素的顺序且综合性能很高。
  5. 堆:PriorityQueue是用堆实现的,堆逻辑上是树,物理上是动态数组,堆可以高效地解决一些其他数据结构难以解决的问题。
  6. 循环数组:ArrayDeque是用循环数组实现的,通过对头尾变量的维护,实现了高效的队列操作。
  7. 位向量:EnumSet和BitSet是用位向量实现的,对于只有两种状态,且需要进行集合运算的数据,使用位向量进行表示、位运算进行处理,精简且高效。

每种数据结构中往往包含一定的算法策略,这种策略往往是一种折中,比如:

  • 动态扩展算法:动态数组的扩展策略,一般是指数级扩展的,是在两方面进行平衡,一方面是希望减少内存消耗,另一方面希望减少内存分配、移动和复制的开销。
  • 哈希算法:哈希表中键映射到链表数组索引的算法,算法要快,同时要尽量随机和均匀。
  • 排序二叉树的平衡算法:排序二叉树的平衡非常重要,红黑树是一种平衡算法, AVL树是另一种平衡算法。平衡算法一方面要保证尽量平衡,另一方面要尽量减少综合开销。

Collections实现了一些通用算法,比如二分查找、排序、翻转列表顺序、随机化重排等,在实现大部分算法时,Collections也都根据容器大小和是否实现了RandomAccess接口采用了不同的实现方式。

3.3、设计思维和模式

在容器类中,我们也看到了Java的多种语言机制和设计思维的运用:

  1. 封装:封装就是提供简单接口,并隐藏实现细节,这是程序设计的最重要思维。在容器类中,很多类、方法和变量都是私有的,比如迭代器方法,基本都是通过私有内部类或匿名内部类实现的。
  2. 继承和多态:继承可以复用代码,便于按父类统一处理,但继承是一把双刃剑。在容器类中,Collection是父接口,List/Set/Queue继承自Collection,通过Collection接口可以统一处理多种类型的集合对象。容器类定义了很多抽象容器类,具体类通过继承它们以复用代码,每个抽象容器类都有详细的文档说明,描述其实现机制,以及子类应该如何重写方法。容器类的设计展示了接口继承、类继承,以及抽象类的恰当应用。
  3. 组合:一般而言,组合应该优先于继承,我们看到HashSet通过组合的方式使用HashMap, TreeSet通过组合使用TreeMap,适配器和装饰器模式也都是通过组合实现的。
  4. 接口:面向接口编程是一种重要的思维,可降低代码间的耦合,提高代码复用程度,在容器类方法中,接受的参数和返回值往往都是接口,Collections提供的通用算法,操作的也都是接口对象,我们平时在使用容器类时,一般也只在创建对象时使用具体类,而其他地方都使用接口。
  5. 设计模式:我们在容器类中看到了迭代器、工厂方法、适配器、装饰器等多种设计模式的应用。
相关推荐
xingfujie1 小时前
第2章:服务器规划与基础环境配置
linux·运维·微服务·云原生·容器·kubernetes·负载均衡
段ヤシ.1 小时前
回顾Java知识点,面试题汇总Day7(持续更新)
java·开发语言
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【53】Interrupts 中断机制:动态中断
java·人工智能·spring
ℳ₯㎕ddzོꦿ࿐2 小时前
实战指南:使用 Docker Compose 优雅部署 MongoDB 并自动初始化用户
mongodb·docker·容器
用户298698530142 小时前
Java 操作 Word 文档:数学公式与符号的插入方法
java·后端
见青..2 小时前
JAVA安全靶场环境搭建
java·web安全·靶场·java安全
一坨阿亮2 小时前
Docker 离线部署
java·spring cloud·docker
yyyyy_abc2 小时前
docker学习笔记
运维·docker·容器
一起逃去看海吧2 小时前
Dify-01-docker安装 和 dify部署
运维·docker·容器