通用容器类
具体容器类其实都不是从头构建的,它们都继承了一些抽象容器类。这些抽象类提供了容器接口的部分实现,方便了Java具体容器类的实现。此外,通过继承抽象类,自定义的类也可以更为容易地实现容器接口。为什么需要实现容器接口呢?至少有两个原因。
- 容器类是一个大家庭,它们之间可以方便地协作,比如很多方法的参数和返回值都是容器接口对象,实现了容器接口,就可以方便地参与这种协作。
- Java有一个类Collections,提供了很多针对容器接口的通用算法和功能,实现了容器接口,可以直接利用Collections中的算法和功能。
1、抽象容器类
抽象容器类与之前介绍的接口和具体容器类的关系如图所示。

虚线框表示接口,有Collection、List、Set、Queue、Deque和Map。有6个抽象容器类。
- AbstractCollection:实现了Collection接口,被抽象类AbstractList、AbstractSet、AbstractQueue继承,ArrayDeque也继承自AbstractCollection(图中未画出)。
- AbstractList:父类是AbstractCollection,实现了List接口,被ArrayList、Abstract-SequentialList继承。
- AbstractSequentialList:父类是AbstractList,被LinkedList继承。
- AbstractMap:实现了Map接口,被TreeMap、HashMap、EnumMap继承。
- AbstractSet:父类是AbstractCollection,实现了Set接口,被HashSet、TreeSet和EnumSet继承。
- 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以静态方法的方式提供了很多通用算法和功能,这些功能大概可以分为两类。
- 对容器接口对象进行操作。
- 查找和替换。
- 排序和调整顺序。
- 添加和修改。
- 返回一个容器接口对象。
- 适配器:将其他类型的数据转换为容器接口对象。
- 装饰器:修饰一个给定容器接口对象,增加某种性质。
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,可经过三次翻转实现:
- 翻转子列表A
a1a2...amb1b2...bn→ am...a2a1b1b2...bn - 翻转子列表B
am...a2a1b1b2...bn→ am...a2a1bn...b2b1 - 翻转整个列表
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、数据结构和算法
在容器类中,我们看到了如下数据结构的应用:
- 动态数组:ArrayList内部就是动态数组,HashMap内部的链表数组也是动态扩展的,ArrayDeque和PriorityQueue内部也都是动态扩展的数组。
- 链表:LinkedList是用双向链表实现的,HashMap中映射到同一个链表数组的键值对是通过单向链表链接起来的,LinkedHashMap中每个元素还加入到了一个双向链表中以维护插入或访问顺序。
- 哈希表:HashMap是用哈希表实现的,HashSet、LinkedHashSet和LinkedHashMap基于HashMap,内部当然也是哈希表。
- 排序二叉树:TreeMap是用红黑树(基于排序二叉树)实现的,TreeSet内部使用TreeMap,当然也是红黑树,红黑树能保持元素的顺序且综合性能很高。
- 堆:PriorityQueue是用堆实现的,堆逻辑上是树,物理上是动态数组,堆可以高效地解决一些其他数据结构难以解决的问题。
- 循环数组:ArrayDeque是用循环数组实现的,通过对头尾变量的维护,实现了高效的队列操作。
- 位向量:EnumSet和BitSet是用位向量实现的,对于只有两种状态,且需要进行集合运算的数据,使用位向量进行表示、位运算进行处理,精简且高效。
每种数据结构中往往包含一定的算法策略,这种策略往往是一种折中,比如:
- 动态扩展算法:动态数组的扩展策略,一般是指数级扩展的,是在两方面进行平衡,一方面是希望减少内存消耗,另一方面希望减少内存分配、移动和复制的开销。
- 哈希算法:哈希表中键映射到链表数组索引的算法,算法要快,同时要尽量随机和均匀。
- 排序二叉树的平衡算法:排序二叉树的平衡非常重要,红黑树是一种平衡算法, AVL树是另一种平衡算法。平衡算法一方面要保证尽量平衡,另一方面要尽量减少综合开销。
Collections实现了一些通用算法,比如二分查找、排序、翻转列表顺序、随机化重排等,在实现大部分算法时,Collections也都根据容器大小和是否实现了RandomAccess接口采用了不同的实现方式。
3.3、设计思维和模式
在容器类中,我们也看到了Java的多种语言机制和设计思维的运用:
- 封装:封装就是提供简单接口,并隐藏实现细节,这是程序设计的最重要思维。在容器类中,很多类、方法和变量都是私有的,比如迭代器方法,基本都是通过私有内部类或匿名内部类实现的。
- 继承和多态:继承可以复用代码,便于按父类统一处理,但继承是一把双刃剑。在容器类中,Collection是父接口,List/Set/Queue继承自Collection,通过Collection接口可以统一处理多种类型的集合对象。容器类定义了很多抽象容器类,具体类通过继承它们以复用代码,每个抽象容器类都有详细的文档说明,描述其实现机制,以及子类应该如何重写方法。容器类的设计展示了接口继承、类继承,以及抽象类的恰当应用。
- 组合:一般而言,组合应该优先于继承,我们看到HashSet通过组合的方式使用HashMap, TreeSet通过组合使用TreeMap,适配器和装饰器模式也都是通过组合实现的。
- 接口:面向接口编程是一种重要的思维,可降低代码间的耦合,提高代码复用程度,在容器类方法中,接受的参数和返回值往往都是接口,Collections提供的通用算法,操作的也都是接口对象,我们平时在使用容器类时,一般也只在创建对象时使用具体类,而其他地方都使用接口。
- 设计模式:我们在容器类中看到了迭代器、工厂方法、适配器、装饰器等多种设计模式的应用。