JCF相关基础类接口/抽象类源码阅读
本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习
本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则你会不知道我在说什么
简介
JCF(Java Collections Framework)为Java开发者提供了通用的容器,而每个可以直接拿来用的容器又实现了很多接口,并且有些接口方法还有default实现,为了更好地掌握JCF的整体结构,甚至以后需要到自己开发容器的时候,对应该实现哪些接口可以做到了然于胸。
对于接口我按照源码注释进行二次解释,而抽象类,会稍微深入到源码去理解里面方法的执行。
代码分析
Iterator接口
Iterator是一个集合上的迭代器,用来替代 Enumeration(旧API,可以忽略) 进行遍历、迭代。迭代不一定是顺序的,对于List来说可能是顺序的,但是对于Set来说就不一定了。
public interface Iterator<E> {
// 查询是否有下一个元素
boolean hasNext();
// 获取当前,并将指针指向下一个元素
E next();
// 移除上一次调用next得到的元素
default void remove() {
throw new UnsupportedOperationException("remove");
}
// 遍历从当前指针开始的剩余元素
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
注释里说的“指针”其实是假想出来的。举个例子,比如ArrayList,其新生成的迭代器相当于一个指针,初始指向下标为0的位置,此时hasNext询问“指针”指向的位置有没有元素,next的行为则是返回当前指针指向的元素,然后把“指针”指向下一个元素的位置。
因此使用迭代器遍历是这样的:
ArrayList<String> arr = new ArrayList<>();
Iterator<String> c = arr.iterator();
while (c.hashNext()) {
String s = c.next();
// ...
}
另外,迭代器还支持在遍历的过程中删除当前指针指向的元素,比如你像下面这么干,就会删除ArrayList的第一个元素:
ArrayList<String> arr = new ArrayList<>();
Iterator<String> c = arr.iterator();
arr.next();
arr.remove();
当然,remove得子类去根据集合是否支持,不支持的话直接抛异常。
Iterable接口
public interface Iterable<T> {
// 返回集合的迭代器
Iterator<T> iterator();
// 提供函数调用式的遍历方式
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
// 返回集合的spliterator
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
实现这个接口主要是可以让对象支持for-each语法,比如ArrayList就实现了这个接口,因此相比使用迭代器,你可以写出更简洁的遍历方式:
ArrayList<String> arr = new ArrayList<>();
for (String s : arr) {
// ...
}
这种简洁的遍历方式是java的语法糖,实际上它还是依靠iterator来实现的,也就是会被编译成之前使用迭代器的那个样子(可能会与实际的编译情况有差别,不细究):
ArrayList<String> arr = new ArrayList<>();
Iterator<String> c = arr.iterator();
while (c.hashNext()) {
String s = c.next();
// ...
}
最后的spliterator函数,返回一个Spliterator对象,这个是在java8引入的,与stream、并行计算有关,暂时不讲,以后会单独介绍。
Collection接口
Collection接口实现了Iterable接口,通过注释的方式来说明每个方法的作用。流相关函数忽略。
public interface Collection<E> extends Iterable<E> {
// 集合元素个数,如果元素个数超过Integer.MAX_VALUE,则返回Integer.MAX_VALUE
int size();
// 集合是否为空
boolean isEmpty();
// 是否包含o,具体是通过equals方法来对比
boolean contains(Object o);
Iterator<E> iterator();
// 返回一个包含所有元素的新数组,也就是说数组是new出来的(元素不是new出来的),不是集合的底层数组
Object[] toArray();
// 返回一个包含所有元素的新数组,类型为T。如果数组长度小于集合大小那么会new一个数组,否则返回a
<T> T[] toArray(T[] a);
// 新增一个元素
boolean add(E e);
// 移除一个元素
boolean remove(Object o);
// 是否包含c中的所有元素
boolean containsAll(Collection<?> c);
// 添加c中的所有元素
boolean addAll(Collection<? extends E> c);
// 移除c中的所有元素(如果元素不在集合中的话不产生影响)
boolean removeAll(Collection<?> c);
// 移除所有满足filter的元素,返回是否有元素被移除
default boolean removeIf(Predicate<? super E> filter);
// 移除所有不在c中的元素,与removeAll的功能恰好相反
boolean retainAll(Collection<?> c);
// 移除集合中所有元素
void clear();
// 流相关
default Spliterator<E> spliterator();
default Stream<E> stream();
default Stream<E> parallelStream();
}
AbstractCollection抽象类
AbstractCollection实现了Collection接口,目的是实现一些通用的方法,这样一来想要实现Collection的类可以考虑继承AbstractCollection,省去功夫重新实现其中的一些方法,比如isEmpty显而易见可以实现成size()==0,如果子类有更加高效的实现再考虑重写AbstractCollection提供的默认实现。
下面来看一下AbstractCollection实现了的方法:
-
isEmptry
public boolean isEmpty() { return size() == 0; }
-
contains
// 循环遍历比较o和集合中的元素,如果equals则返回true 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; }
-
toArray
// 循环遍历集合,将元素放到数组r。集合大小可能会在遍历时由于并发增删发生变化,从而影响最终数组的大小 public Object[] toArray() { Object[] r = new Object[size()]; Iterator<E> it = iterator(); for (int i = 0; i < r.length; i++) { // 如果没有下一个元素了,那么直接truncate if (!it.hasNext()) return Arrays.copyOf(r, i); r[i] = it.next(); } // 如果集合大小大于原来的大小,那么调用finishToArray继续加入元素到数组中 return it.hasNext() ? finishToArray(r, it) : r; } // 使用iterator将剩下的元素加入数组中 private static <T> T[] finishToArray(T[] r, Iterator<?> it) { int len = r.length; int i = len; while (it.hasNext()) { // 数组需要扩容 if (i == len) { // 一般情况下扩展数组大小为原先大小的两倍+1(具体查看ArraysSupport.newLength) len = ArraysSupport.newLength(len, 1, /* minimum growth */ (len >> 1) + 1 /* preferred growth */); r = Arrays.copyOf(r, len); } r[i++] = (T)it.next(); } // 返回数组并且删除扩容后多余的长度 return (i == len) ? r : Arrays.copyOf(r, i); }
-
add
public boolean add(E e) { // 实现为抛出异常的原因: // 1. 默认集合为immutable的,如果要支持add,重写这个方法即可 // 2. AbstractCollection没有默认的存储元素的底层数据结构,而且迭代器不支持add操作,因此add方法留给子类实现 throw new UnsupportedOperationException(); }
-
remove
// 类似contains方法,只是多了一句it.remove public boolean remove(Object o) { Iterator<E> it = iterator(); if (o==null) { while (it.hasNext()) { if (it.next()==null) { it.remove(); return true; } } } else { while (it.hasNext()) { if (o.equals(it.next())) { it.remove(); return true; } } } return false; }
还有几个xxxAll的批操作方法就不展示了,大同小异。
AbstratctCollection基于迭代器实现了一些常用的删除、包含等操作,总体来说比较简单。
List接口
List接口实现Collection接口,代表的是 有序的集合(sequence),每个元素都对应一个下标,允许存储重复的元素(这是官方推荐的做法),支持随机访问、插入、新增。在Collection的Iterator的基础上,List提供了列表迭代器ListIterator,除了Iterator接口提供的操作外,还允许基于位置的元素插入、替换以及双向遍历。
先看一下新增的迭代器ListIterator
public interface ListIterator<E> extends Iterator<E> {
// 就是Iterator中的hashNext、next、remove
boolean hasNext();
E next();
void remove();
// 询问当前指针的上一个位置是否有元素
boolean hasPrevious();
// 将指针-1,并返回指针指向的位置
E previous();
// 当前指针的指向的下标
int nextIndex();
// 当前指针指向的下标-1
int previousIndex();
void set(E e);
void add(E e);
}
下面是新增的方法,太简单的就不注释了,猜也能猜出来功能是什么。
// 通过迭代器的set方法,将每个位置上的元素替换为用户指定的元素
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
// 通过toArray转换成数组并用工具类Arrays.sort排序,然后再把排序后的数组重新设到集合上
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
// 其实就是firstIndexOf
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator<E> listIterator();
// 从index开始的列表迭代器,即“指针”初始位置为index而不是-1
ListIterator<E> listIterator(int index);
// 返回列表的一个“view”。对view做结构化的修改以及非结构的修改都会影响原始的列表
// 如果在生成了view之后对原列表做结构化的修改,并且不是通过view作的这个修改,那么这个行为是未定义的(由子类定义),比如在ArrayList会抛ConcurrentModificationException
// 结构化修改:比如subList(0, 10).clear()会使得原始列表的前10个元素被删除
// 非结构化修改:比如subList(3, 5).set(0, obj)会使得原列表下标为3的元素被设置为obj
List<E> subList(int fromIndex, int toIndex);
List还添加了一堆静态方法of的重载,来方便地生成immutable List,比如
static <E> List<E> of(E... elements);
AbstractList抽象类
AbstractList继承于AbstractColletion。类似AbstractColletion,AbstractList实现了List的一些方法。AbstractList还提供了默认的Iterator、ListIterator实现。
Itr(Iterator的实现)
只需要牢记,Iterator用于向后(单向)遍历,指针指向下一个获取的元素,因此是先获取指针指向的元素,然后指针+1。
private class Itr implements Iterator<E> {
// iterator的“指针”,实际上是当前遍历到的下标
int cursor = 0;
// 上次指针修改前的值
int lastRet = -1;
// 记录创建iterator时列表的结构化修改次数
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
// 获取当前指针指向的元素
int i = cursor;
E next = get(i);
lastRet = i;
// 指针+1
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException(e);
}
}
// 删除上次next对应的元素
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 移除指针修改前指向的元素
AbstractList.this.remove(lastRet);
// 指针-1
if (lastRet < cursor)
cursor--;
// 重设lastRet,指示该位置的元素已经被移除,不能二次移除
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
// 检查列表是否被结构化修改了,而且不是被this iterator修改的,那么抛出异常避免进一步出错
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
比较简单,基本上是把Iterator接口定义好的功能用代码翻译一下而已,看注释就行。
需要注意的点:
-
checkForComodification,虽然抛出的异常名是“并发修改异常”,看,但分析代码可以知道,在iterator被创建后,只能用这个iterator来修改集合的结构(比如remove),否则,无论是当前线程还是其他线程修改了集合的结构后,这个iterator不能再被使用,否则会抛异常。
-
不能连续remove元素,remove相当于在逻辑上留下了一个“空洞”(lastRet=-1),即非法元素,那肯定不能remove一个“空洞”。如果要实现从当前元素开始删除后面所有的元素可以写成:
while (ite.hasNext()) { it.next(); it.remove(); }
ListItr(ListIterator的实现)
ListItr实现了ListIterator,继承于Itr。只需要牢记ListIterator用于双向遍历,向后遍历的行为与Iterator一样,向前遍历时,指针先-1,然后获取指针指向的元素。
private class ListItr extends Itr implements ListIterator<E> {
// 指针从index开始
ListItr(int index) {
cursor = index;
}
// 如果指针=0,那么前一个元素就是-1,前面就没有元素了,返回false
public boolean hasPrevious() {
return cursor != 0;
}
// 获取上一个元素
public E previous() {
checkForComodification();
try {
// 指针-1
int i = cursor - 1;
// 获取上一个元素
E previous = get(i);
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException(e);
}
}
// 获取下一个元素下标
public int nextIndex() {
return cursor;
}
// 获取上一个元素下标
public int previousIndex() {
return cursor-1;
}
// 设置当前指针指向的元素
public void set(E e) {
// 如果是非法位置,则抛异常
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.set(lastRet, e);
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// 将元素插入当前指针指向的位置,当前指针指向的元素及其后面的元素全体“后移”
public void add(E e) {
checkForComodification();
try {
int i = cursor;
AbstractList.this.add(i, e);
// 注意这两行
lastRet = -1;
cursor = i + 1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
需要注意的点:
- 之前说过,使用remove删除完元素之后,当前指针可以视为指向“空洞”,不可以进行操作,包括set。因此lastRet=-1的时候进行set的话会抛异常
- add之后,指针还是指向原来的元素,因此cursor=i+1,并且lastRet=-1设置“空洞”,不能进行set和remove。
回归AbstractList,看一下它实现了List接口的方法:
// 在尾部添加一个元素
public boolean add(E e) {
add(size(), e);
return true;
}
// 往后找o第一次出现的下标
public int indexOf(Object o) {
ListIterator<E> it = listIterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return it.previousIndex();
} else {
while (it.hasNext())
if (o.equals(it.next()))
return it.previousIndex();
}
return -1;
}
// 往前找o第一次出现的下标
public int lastIndexOf(Object o) {
ListIterator<E> it = listIterator(size());
if (o==null) {
while (it.hasPrevious())
if (it.previous()==null)
return it.nextIndex();
} else {
while (it.hasPrevious())
if (o.equals(it.previous()))
return it.nextIndex();
}
return -1;
}
// 删除所有元素
public void clear() {
removeRange(0, size());
}
// 移除[fromIndex, toIndex)内的元素
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<E> it = listIterator(fromIndex);
for (int i=0, n=toIndex-fromIndex; i<n; i++) {
it.next();
it.remove();
}
}
// 获取迭代器
public Iterator<E> iterator() {
return new Itr();
}
// 获取列表迭代器
public ListIterator<E> listIterator() {
return listIterator(0);
}
// 获取列表的子视图
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size());
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
// 重写equals规则
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;
ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return !(e1.hasNext() || e2.hasNext());
}
值得分析的方法只有两个:subList
和equals
- equals:对Object.equals的重载。当两个列表的每个元素都相等的时候,列表视为相等
- subList:这个方法根据列表是否支持随机访问,分别返回SubList或RandomAccessSubList。这两个类都继承自AbstractList,以 代理模式,代理
this
对象,封装了对列表某个范围的数据访问,官方文档称之为原列表的一个“视图”,对该视图的任何结构与非结构上的修改都相当于对原列表进行修改,目的是方便我们像使用普通List那样来操作列表的子列表。
SubList
SubList是AbstractList的静态内部类,继承于AbstractList。看一下成员变量:
private static class SubList<E> extends AbstractList<E> {
// 被代理对象
private final AbstractList<E> root;
// 由于SubList也是一个List,因此subList可以链式调用,parent记录调用subList的SubList对象
private final SubList<E> parent;
// 记录该子列表在原列表的偏移量,比如list.subList(3, 5),那么生成的SubList.offset=3
private final int offset;
// 子列表大小
protected int size;
}
下面分析SubList的方法,由于SubList是代理类,因此SubList的方法肯定是借助被代理对象来实现的:
public R xyz(params) {
// ...
return root.xyz(params)
// ...
}
几乎每个公有方法都调用了checkForComodification,之前稍微提到过,但都不够详细或者有所缺漏,在这儿结合SubList全面剖析一下它背后的理念。
checkForComodification检查列表是否被非子孙子列表做过结构性修改(即检查modCount与parent.modCount是否相等)如果被修改过的话,再去访问该子列表是不允许的。这里“子孙”一词借用了树的概念:原列表视为根节点,其subList为第二层节点,subList.subList为第三层节点....,当对某个节点做结构性修改,只会对祖先进行同步,此时访问本节点和祖先节点都是没问题的,但除了该节点的祖先外的其他节点与本节点此时是不一致的,访问它们就会抛异常。talk is cheap, show me the code:
List<String> list = ...;
List<String> sub1 = list.subList(3, 5);
List<String> sub1_1 = sub2.subList(0, 3);
List<String> sub2 = list.subList(6, 10);
/*
此时的树:
list(root)
├── sub1
│ └── sub1_1
└── sub2
*/
list.remove(0); // 最后一行报错
sub2.remove(0); // 最后一行报错
sub1_1.remove(0); // ok的
sub1.remove(0); // ok的
// 访问sub1节点
sub1.size();
由此,我们进一步思考为什么要这样设计:回到“视图”这个概念,子节点是对父节点的视图,使用视图的初衷就是为了方便操作原列表的某个范围,因此:
- 子节点发生结构性修改后,肯定能继续访问父节点,否则如果抛异常的话,要视图干什么呢。
- 父节点发生结构性修改后,子节点(视图)可能没办法再去正确映射父节点这段范围,如果还要更新子节点的话,成本太高了,因为子节点可能不止一个,直接把子节点给废了就好了。
- 兄弟/叔父等旁系节点发生结构性修改后,与父节点发生修改同理,都是对同一个祖先的映射,相当于祖先被修改了,因此本节点要废掉。
有了前面的铺垫,很多方法基本不用再分析。只有updateSizeAndModCount
和listIterator
需要讲下:
-
updateSizeAndModCount:每次对子列表做完结构性修改后(remove、add等),维护modCount和size:
private void updateSizeAndModCount(int sizeChange) { SubList<E> slist = this; do { slist.size += sizeChange; slist.modCount = root.modCount; slist = slist.parent; } while (slist != null); }
上面说过,节点发生修改,只需要同步祖先即可,因此借助parent,往root的方向,更新一路上父节点的modCount。
-
listIterator:由于子列表只映射原列表的某个范围,因此肯定不能直接返回原列表的迭代器,SubList自己实现了一个匿名内部类,并且也是代理模式,代理原列表(父节点)的迭代器,比较简单,明白原理随便看看就行:
public ListIterator<E> listIterator(int index) { checkForComodification(); rangeCheckForAdd(index); return new ListIterator<E>() { private final ListIterator<E> i = root.listIterator(offset + index); public boolean hasNext() { return nextIndex() < size; } public E next() { if (hasNext()) return i.next(); else throw new NoSuchElementException(); } public boolean hasPrevious() { return previousIndex() >= 0; } public E previous() { if (hasPrevious()) return i.previous(); else throw new NoSuchElementException(); } public int nextIndex() { return i.nextIndex() - offset; } public int previousIndex() { return i.previousIndex() - offset; } public void remove() { i.remove(); updateSizeAndModCount(-1); } public void set(E e) { i.set(e); } public void add(E e) { i.add(e); updateSizeAndModCount(1); } }; }
RandomAccessSubList
继承于SubList,没有实现新的方法,只是多实现了一个RandomAccess接口,这个接口作为标记接口没有任何方法。这个类的目的是用于调用subList的时候保留RandomAccess接口。
说个题外话,像RandomAccess的空接口有什么用呢?用作标记。举个例子,某个函数接收参数类型为List,功能是遍历并输出元素,但是你不想采用迭代器的方式,而是使用下标的方式去遍历(因为可能下标方式是直接访问数组,而迭代器还得创建迭代器对象,调用额外的方法等,直接下标可能效率更高),但如果List是不支持随机访问的LinkedList,那么用下标方式就会很慢了,因此可以判断有没有实现RandomAccess快速判断支不支持随机访问,以选择迭代方式:
public void iter(List<String> list) {
if (list instanceof RandomAccess) {
for (int i = 0; i < list.size(); i++) {
String s = list[i];
sout(s);
}
} else {
for (String s : list) {
sout(s);
}
}
}
类似的,还有Cloneable接口、Serializable接口等,但是也有一些别的作用,比如Cloneable,如果你要实现clone方法的话,必须继承这个接口,否则调用super.clone()获取浅拷贝对象的时候(即Object.clone这个native方法)会直接抛异常。
Queue接口
顾名思义,Queue代表队列。其父接口是Collection。
虽然名义上是队列,但不一定是FIFO,也可以是优先队列,也可以是LIFO。总而言之Queue只是提供“入队”和“出队”的语义,而出入队顺序并不作规定,即在给定入队顺序下,出队顺序由子类定义。
Queue定义了两套功能相同的方法,及其应对非法操作的行为:
- add/offer入队:抛出IllegalStateException / 返回false
- remove/poll出队:抛出NoSuchElementException / 返回null
- element/peek查询队首元素:抛出NoSuchElementException / 返回null
非法操作包括对满队列入队、对空队列出队/查询队首,前一组方法(add、remove、element)直接抛出异常,后一组命名更加fancy的方法(offer、poll、peek)则返回特殊值。
由于Queue不规定出队顺序,因此如果要比较两个Queue的话(即equals、hashcode方法),一般不能基于元素来比较他们,是否相等还与出队顺序相关,因此一般使用默认的Object.equals...
Deque(/dɛk/)接口
Deque表示双端队列,其父接口是Queue。注意Deque这个单词与deck的发音一样,与de-queue区分开来,后者用来说“出队”这个操作,比如:you can "de-queue an object from the deque。
在Queue的基础上添加了一堆方法,不过都比较好理解。直接截图源码文档看一下:
Queue的方法依然保持原来的语义,last代表队尾,first为队首:
Deque还提供了用作栈的方法,比如push、pop(当然,你也可以通过定义Queue的出队顺序为LIFO来搞个栈,只是没有push、pop这些以栈操作来命名的方法)。基于这一点,相比Stack类官方推荐用Deque接口,可能因为Stack继承于Vector,而Vector早已是哥们废了:
还提供了两个移除内部元素(即队头队尾)的方法:
boolean removeFirstOccurrence(Object o);
boolean removeLastOccurrence(Object o);