集合在我们的日常开发中所使用的次数简直太多了,你已经把它们都用的熟透了,但是作为一名合格的程序员,你不仅要了解它的基本用法,你还要了解它的源码;存在即合理,你还要了解它是如何设计和实现的,你还要了解它的衍生过程。
这篇博客就来详细介绍一下 Collection 这个庞大集合框架的家族体系和成员,让你了解它的设计与实现。
是时候祭出这张神图了首先来介绍的就是列表爷爷辈儿的接口- Iterator
01、Iterable 接口
JavaDoc 解释:实现此接口允许对象成为 for-each 循环的目标
也就是说,实现了此接口,就能使用 for-each 进行循环遍历,for-each 是增强型的 for 循环,是 java 提供的一种语法糖,它的基本遍历方式如下:
List<Object> list = new ArrayList(); for (Object obj: list){}
除了实现此接口的对象外,数组也可以用 for-each 循环遍历,如下:
Object[] list = new Object[10]; for (Object obj: list){}
那么,为什么实现了此接口的对象可以进行 for-each 遍历呢?上述的循环遍历经过反编译后如下:
// 数组 Object[] list = new Object[10]; Object[] var14 = list; int var15 = list.length; for (int var16 = 0; var16 < var15; ++var16) { Object var10000 = var14[var16]; }
// 对象 List<Object> list = new ArrayList(); Object var15; for (Iterator var14 = list.iterator(); var14.hasNext(); var15 = var14.next()) { ; }
也就是说,for-each 循环经过反编译后,会自动创建 iterator 来实现对数组和对象的循环遍历。
其他遍历方式
jdk1.8 之前Iterator
只有 iterator 一个方法,就是
Iterator<T> iterator();
实现次接口的方法能够创建一个轻量级的迭代器,用于安全的遍历元素,移除元素,添加元素。
为什么说是安全的遍历元素,移除元素,添加元素?请参考for 、foreach 、iterator 三种遍历方式的比较[1]
也可以使用迭代器的方式进行遍历
for(Iterator it = coll.iterator(); it.hasNext(); ){ System.out.println(it.next()); }
本篇先不探讨相关 jdk1.8 的新特性
02、Collection 接口
Collection 位于继承体系的顶级接口,它的上面只有 Iterable 接口,但是 Iterable 和集合的继承体系并无直接关系,我们一说集合框架的顶级接口其实就是指的是 Collection 接口,一说映射的顶级接口就指的是 Map 接口,而 Iterable 接口更多扮演辅助的作用。
Collection 接口更多的是制定标准的作用。它主要指定的标准有
- Collection 中的一些集合允许重复元素,而其他的则不允许。一些集合有序而一些集合无序。JDK 没有直接提供这个接口的实现,它提供了更多实现特性的接口,比如 List、Set,不包括 Map,因为 Map 不是 Collection 的实现。
- 无序集合(包含重复元素)应直接实现这个接口
- 一般 Collection 的实现类应该提供两个标准的构造器,一个无参构造器,用于创建一个空集合;和一个持有单个 Collection 类型参数的构造器。例如
// ArrayList.java public ArrayList() {...} public ArrayList(Collection<? extends E> c) {...} // LinkedList.java public LinkedList() {} public LinkedList(Collection<? extends E> c) {...} // LinkedHashSet.java public LinkedHashSet() {...} public LinkedHashSet(Collection<? extends E> c) {...} // HashSet.java public HashSet() {...} public HashSet(Collection<? extends E> c) {...} // TreeSet.java public TreeSet() {...} public TreeSet(Collection<? extends E> c) {...}
个人理解添加 Collection 类型的构造函数其实就是为了集合的复制和集合的相互转化。
- Collection 接口包含一些改变集合结构的"破坏性"方法,如果实现的集合不支持此种方法,请抛出UnsupportedOperationException 异常。
- 一些 collection 的实现对元素有一些限制。例如,一些实现类禁止空元素,一些则在元素类型上有一些限制。试图添加不合格的元素会引发未经检查的异常。特别是空指针异常和类型转换异常。尝试查询不合格元素的 存在可能会抛出异常,或者可能返回 false。一些实现将展现前者的行为,一些实现将展现后者的行为。更进一步来说,尝试将一个不符合条件的元素进行操作,不会使操作完成,将不合格的元素插入集合中可能 会导致错误,有一些例外可能会取得成功,这取决于实现类。
- 对于线程安全性来讲,Collection 没有提供线程安全的方法,这完全交由子类自己去实现。
- 对集合执行递归遍历的某些集合操作可能会失败,并且集合直接间接包含自身的自引用实例会出现异常。这包括 clone(), equals(), hashCode() 和 toString() 实现可以可选地处理自引用场景,但是大多数当前实现不这样做。
List 接口
List 也被称为列表或者有序序列,它继承了 Collection 接口,提供了很多与 Collection 相同的方法,同时也是 ArrayList、LinkedList 等的父类。List 定义了一些列表的标准,它的具体特性如下:
- 使用该接口可以有序的控制每个元素的插入次序,使用者也可以通过索引访问元素,并寻找 list 中的元素。
- 与 set 不同,list 允许重复元素。list 列表中的元素保证插入的次序是因为其存储在 list 中的元素都满足 e1.equals(e2),并且允许多个空元素。
- List 除了使用 Iterator 作为迭代器之外,还提供了一种特殊的迭代器
ListIterator
,是 List 接口所独有的。ListIterator 除了允许正常的操作外,增加了元素的插入和替换,还允许双向访问,提供了一种方法来获得从列表中的指定位置开始的序列迭代器
ListIterator<E> listIterator(); // 从指定位置处开始 ListIterator<E> listIterator(int index);
- List 接口提供了两种方法寻找指定的对象,从性能的角度来看,应谨慎使用这些方法。它们将执行昂贵的线性搜索
int indexOf(Object o); int lastIndexOf(Object o);
- 虽然列表允许将自己包含为元素,但建议及其谨慎使用,equals 和 hashCode 方法不再这样的列表中很好的定义。
- 某些列表的实现对它们可能包含的元素有些限制,例如,一些实现允许空元素,一些实现对他们的元素有严格的类型限制。试图添加不合规定的元素会抛出未经检查的异常,比如 NullPointerException 和 ClassCastException。
Set 接口
Set 接口位于与 List 接口同级的层次上,它同时也继承了 Collection 接口。
- Set 属于集合框架的一个子类,它不允许重复元素。也就是说,它包含在内的每两个元素的比较都不能满足 e1.equals(e2),所以最多只允许一个空元素。
- Set 接口提供了额外的规定,超出了从 Collection 接口继承的那些。它对 add,equals,hashCode 方法提供了额外的标准。其他从 Collection 继承下来的方法在这仍然用起来很方便。
- 对于构造函数的约定,也就能理解,所有的构造函数必须构建一个不包含重复元素的集合。
- 注意:如果将可变元素用于 set 对象的集合,则必须非常小心。
- 一些 set 的实现对元素有严格的控制。例如,一些实现禁止空元素,一些实现对元素类型有严格对限制。尝试添加一些不合法的元素会抛出未经检查的异常。特别是 NullPointerException 或者 ClassCastException 。尝试查询不合法的元素也会抛出异常,或者可能仅仅返回 false。一些将展示前者的行为一些将展示后者的行为。大致上来说,尝试对不合格的元素进行操作,其完成的操作不会导致将不合格的元素插入到集合中。在实现中可以选择是当插入不合法元素时抛出异常还是仅仅只返回 false。
Queue 接口
Queue(队列) 是和 List、Set 接口并列的 Collection 的三大接口之一。Queue 的设计用来在处理之前保持元素的访问次序。除了 Collection 基础的操作之外,队列提供了额外的插入,读取,检查操作。这些当中每一个方法都会有两种形式:如果失败就抛出异常,其他方式返回特殊的值(null 或者 false),后者组成的插入操作被设计成用来严格控制队列容量的实现;在大部分的实现中,插入操作不能直接失败。
Queue 接口提供了几种功能相似但细节不同的方法:
add & offer() , remove() & poll(), element() & peek()
前者失败都会抛出异常,后者会返回特殊的值。
- 队列通常(但不一定)以 FIFO(先进先出)方式对元素进行排序。PriorityQueues 就是一个特例。它的元素的顺序是遵从提供的比较器,或者元素的自然排序,以及对元素进行排序的 LIFO 队列(或堆栈)(后进先出)不论使用顺序如何,调用 remove() 或者 poll() 都会移除队列的头元素。在 FIFO 队列中,所有新添加的元素都会插入到队列的末尾。
- Offer 方法会在允许的情况下插入一个元素,否则返回 false。这与 Collection.add() 方法不同,后者只能通过抛出未经检查的异常来添加元素。offer 方法被设计成,添加失败是一个正常的情况,而不是通过异常的形式出现。
- Remove() 和 poll() 方法移除并返回队列的头元素。remove() 和 poll() 方法不同的地方在于:remove() 方法在队列为 null 时抛出异常而 poll() 方法则返回 null。
- Element() 方法和 peek() 方法返回,但不移除队列的头元素。
- Queue 的实现不允许插入 null 元素,即使一些实现像是 LinkedList,没有限制插入空元素。即使实现允许,null 元素也不应该插入到队列中,因为 null 也被 poll 方法用作特殊的返回值。以指示队列包含输任何的返回值。