概述
List 应该接口是 Collection 最常被使用的接口了。其下的实现类皆为有序列表,其中主要分为 Vector,ArrayList,LinkedList 三个实现类,其中 Vecotr 又拥有子类 Stack。
从线程安全来说,List 下拥有线程安全的集合类 Vector;从数据结构来说,List 下拥有基于数组实现的 Vector 与 ArrayList,和基于链表实现的 LinkedList。
本篇文章暂不讨论具体的实现类,而将基于 List 接口与其抽象类 AbstractList,了解 List 接口是如何承上启下,进一步从 Collection 抽象到具体的。
这是关于 java 集合类源码的第二篇文章。往期文章:
java集合源码分析(一):Collection 与 AbstractCollection
一、List 接口
List 接口的方法
List 接口继承了 Collection 接口,在 Collection 接口的基础上增加了一些方法。相对于 Collection 接口,我们可以很明显的看到,List 中增加了非常多根据下标操作集合的方法,我们可以简单粗暴的分辨一个方法的抽象方法到底来自 Collection 还是 List:参数里有下标就是来自 List,没有就是来自 Collection。
可以说,List 接口在 Collection 的基础上,进一步明确了 List 集合运允许根据下标快速存取的特性。
1.新增的方法
get():根据下标获取指定元素;
replaceAll():参数一个函数式接口UnaryOperator
sort():对集合中的数据进行排序。参数是 Comparator super E>,这个参数让我们传入一个比较的匿名方法,用于数组排序;
set():用指定的元素替换集合中指定位置的元素;
indexOf():返回指定元素在此列表中首次出现的索引;如果此列表不包含该元素,则返回-1;
lastIndexOf():返回指定元素在此列表中最后一次出现的索引,否则返回-1;
listIterator():这个是个多态的方法。无参的 listIterator()用于获取迭代器,而有参的 listIterator()可以传入下标,从集合的指定位置开始获取迭代器。指定的索引指示首次调用next将返回的第一个元素。
subList():返回此列表中指定的两个指定下标之间的集合的视图。注意,这里说的是视图,因而对视图的操作会影响到集合,反之亦然。
2.同名的新方法
add():添加元素。List 中的 add() 参数的(int,E),而 Collection 中的 add() 参数是 E,因此 List 集合中同时存在指定下标和不指定下标两种添加方式;
remove():删除指定下标的元素。注意,List 的 remove() 参数是 int ,而 Collection 中的 ``remove()` 参数是 Objce,也就是说,List 中同时存在根据元素是否相等和根据元素下标删除元素两种方式。
3.重写的方法
spliterator():List 接口重写了 Collection 接口的默认实现,换成了根据顺序的分割。
二、AbstractList 抽象类
AbstractList 类是一个继承了 AbstractCollection 类并且实现了 List 接口的抽象类,它相当于在 AbstractCollection 后的第二层方法模板。是对 List 接口的初步实现,同时也是 Collection 的进一步实现。
我们可以根据 JavaDoc 简单的了解一下它:
此类提供List接口的基本实现,以最大程度地减少实现由“随机访问”数据存储(例如数组)支持的此接口所需的工作。 对于顺序访问数据(例如链表),应优先使用AbstractSequentialList代替此类。
要实现不可修改的列表,程序员只需要扩展此类并为get(int)和size()方法提供实现即可。
要实现可修改的列表,程序员必须另外重写set(int, E)方法(否则将抛出UnsupportedOperationException )。 如果列表是可变大小的,则程序员必须另外重写add(int, E)和remove(int)方法。
不像其他的抽象集合实现,程序员不必提供迭代器实现;
迭代器和列表迭代器由此类在“随机访问”方法之上实现: get(int) , set(int, E) , add(int, E)和remove(int) 。
1.不支持的实现与抽象方法
可以直接通过下标操作的set(),add(),remove()都是 List 引入的新接口,这些都 AbstractList 都不支持,要使用必须由子类重写。
get()由于不能确定子类是链表还是数组,所以此时get()仍然强制要求子类去实现。
abstract public E get(int index);
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
2.内部类们
跟 AbstractCollection 类不同,AbstractList 拥有几个特别的内部类,他们分别的迭代器类:Itr 和 ListItr,对应获取他们的方法是:
iterator():获取 Itr 迭代器类;
listIterator():获取 ListItr 迭代器类。这是个多态方法,可以选择是否从指定下标开始,默认从下标为0的元素开始迭代;
视图类 SubList 和 RandomAccessSubList:
subList():获取视图类,会自动根据实现类是否继承 RandomAccess 而返回 SubList 或 RandomAccessSubList。
这些内部类同样被一些其他的方法所依赖,所以要全面的了解 AbstractList 方法的实现,就需要先了解这些内部类的作用和实现原理。
三、subList方法与内部类
subList()算是一个比较常用的方法了,在 List 接口的规定中,这个方法应该返回一个当前集合的一部分的视图:
public List subList(int fromIndex, int toIndex) {
// 是否是实现了RandomAccess接口的类
return (this instanceof RandomAccess ?
// 是就返回一个可以随机访问的内部类RandomAccessSubList
new RandomAccessSubList<>(this, fromIndex, toIndex) :
// 否则返回一个普通内部类SubList
new SubList<>(this, fromIndex, toIndex));
}
这里涉及到 RandomAccessSubList 和 SubList 这个内部类,其中,RandomAccessSubList 类是 SubList 类的子类,但是实现了 RandomAccess 接口。
1.SubList 内部类
我们可以简单的把 SubList 和 AbstractList 理解为装饰器模式的一种实现,就像 SynchronizedList 和 List 接口的实现类一样。SubList 内部类通过对 AbstractList 的方法进行了再一次的封装,把对 AbstractList 的操作转变为了对 “视图的操作”。
通过对原有的 AbstractList 进行包装,将原本对 AbstractList 操作的方法改为了对 SubList 的操作的方法,是适配器模式思想的一种体现。
我们先看看 SubList 这个类的成员变量和构造方法:
class SubList extends AbstractList {
// 把外部类AbstractList作为成员变量
private final AbstractList l;
// 表示视图的起始位置(偏移量)
private final int offset;
// SubList视图的长度
private int size;
SubList(AbstractList list, int fromIndex, int toIndex) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > list.size())
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
// 获取外部类的引用
// 这也是为什么操作视图或者外部类都会影响对方的原因,因为都操作内存中的同一个实例
l = list;
// 获取当前视图在外部类中的起始下标
offset = fromIndex;
// 当前视图的长度就是外部类截取的视图长度
size = toIndex - fromIndex;
this.modCount = l.modCount;
}
}
我们可以参考图片理解一下:
image-20201126114026855
然后 subList 里面的方法就很好理解了:
public E set(int index, E element) {
// 检查下标是否越界
rangeCheck(index);
// 判断是存在并发修改
checkForComodification();
// 把元素添加到偏移量+视图下标的位置
return l.set(index+offset, element);
}
其他方法都差不多,这里便不再多费笔墨了。
2.RandomAccessSubList 内部类
然后是 SubList 的子类 RandomAccessSubList:
class RandomAccessSubList extends SubList implements RandomAccess {
RandomAccessSubList(AbstractList list, int fromIndex, int toIndex) {
super(list, fromIndex, toIndex);
}
public List subList(int fromIndex, int toIndex) {
return new RandomAccessSubList<>(this, fromIndex, toIndex);
}
}
我们可以看见,他实际上还是 SubList,但是实现了 RandomAccess 接口。关于这个接口,其实只是一个标记,实现了该接口的类可以实现快速随机访问(下标),通过 for 循环+下标取值会比用迭代器更快。
Vector 和 ArrayList 都实现了这个接口,而 LinkedList 没有。专门做此实现也是为了在实现类调用的 subList()方法时可以分辨这三者。
四、iterator方法与内部类
在 AbstractList 里面,为我们提供了 Itr 和 ListItr 两种迭代器。
迭代器是 AbstractList 中很重要的一块内容,他是对整个接口体系的顶层接口,也就是 Iterable 接口中的 iterator() 方法的实现,源码中的很多涉及遍历的方法,都离不开内部实现的迭代器类。
1.迭代器的 fast-fail 机制
我们知道,AbstractList 默认是不提供线程安全的保证的,但是为了尽可能的避免并发修改对迭代带来的影响,JDK 引入一种 fast-fail 的机制,即如果检测的发生并发修改,就立刻抛出异常,而不是让可能出错的参数被使用从而引发不可预知的错误。
对此,AbstractList 提供了一个成员变量 modCount,JavaDoc 是这么描述它的:
已对该列表进行结构修改的次数。
结构修改是指更改列表大小或以其他方式干扰列表的方式,即正在进行的迭代可能会产生错误的结果。该字段由iterator和listIterator方法返回的迭代器和列表迭代器实现使用。如果此字段的值意外更改,则迭代器(或列表迭代器)将抛出ConcurrentModificationException,以响应下一个,移除,上一个,设置或添加操作。
面对迭代期间的并发修改,这提供了快速失败的行为,而不是不确定的行为。
子类对此字段的使用是可选的。如果子类希望提供快速失败的迭代器(和列表迭代器),则只需在其add(int,E)和remove(int)方法(以及任何其他覆盖该方法导致结构化的方法)中递增此字段即可)。
一次调用add(int,E)或remove(int)不得在此字段中添加不超过一个,否则迭代器(和列表迭代器)将抛出虚假的ConcurrentModificationExceptions。
如果实现不希望提供快速失败迭代器,则可以忽略此字段。
这个时候我们再回去看看迭代器类 Itr 的一部分代码,可以看到:
private class Itr implements Iterator {
// 迭代器认为后备列表应该具有的modCount值。如果违反了此期望,则迭代器已检测到并发修改。
int expectedModCount = modCount;
// 检查是否发生并发操作
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
结合代码,我们就不难理解这个 fast-fail 机制是怎么实现的了:
AbstractList 提供了一个成员变量用于记录对集合结构性修改的次数,如果子类希望实现并发修改错误的检查,就需要结构性操作的方法里让modCount+1。这样。在获取迭代器以后,迭代器内部会获取当前的modCount赋值给expectedModCount。
当使用迭代器迭代的时候,每一次迭代都会检测modCount和expectedModCount是否相等。如果不相等,说明迭代器创建以后,集合结构被修改了,这个时候再去进行迭代可能会出现错误(比如少遍历一个,多遍历一个),因此检测到后会直接抛出 ConcurrentModificationException异常。
ListItr 继承了 Itr ,因此他们都有一样的 fast-fail机制。
值得一提的是,对于启用了 fast-fail 机制的实现类,只有使用迭代器才能边遍历边删除,原因也是因为并发修改检测:
2.Itr 迭代器
现在,回到 Itr 的代码上:
private class Itr implements Iterator {
// 后续调用next返回的元素索引
int cursor = 0;
// 最近一次调用返回的元素的索引。如果通过调用remove删除了此元素,则重置为-1。
int lastRet = -1;
// 迭代器认为后备列表应该具有的modCount值。如果违反了此期望,则迭代器已检测到并发修改。
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}./*欢迎加入java交流Q君样:909038429一起吹水聊天
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
迭代方法
除了并发修改检测外,迭代器迭代的方式也出乎意料。我们可以看看 hasNext()方法:
public E next() {
// 检验是否发生并发修改
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
这个逻辑其实跟链表的遍历是一样的,只不过指针变成了数组的下标。以链表的方式去理解:
我们把循环里调用next()之后的节点叫做下一个节点,反正称为当前节点。假如现在有 a,b,c 三个元素:
当初始化的时候,指向最后一次操作的的节点的指针 lastRet=-1,即当前节点不存在,当前游标 cursor=0,即指向下一个节点 a;
当开始迭代的时候,把游标的值赋给临时指针 i,然后通过游标获取并返回下一个节点 a,再把游标指向 a 的下一个节点 b,此时 cursor=1,lastRet=-1,i=1;
接着让lastRet=i,也就是当前指针指向新的当前节点 a,现在 lastRet=0,cursor=1`,完成了对第一个节点 a 的迭代;
重复上述过程,把节点中的每一个元素都处理完。
现在我们知道了迭代的方式,cursor和 lastRet 的作用,也就不难理解 remove()方法了:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 调用删除方法
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
// 因为删除了当前第i个节点,所以i+1个节点就会变成第i个节点,
// 调用next()以后cursor会+1,因此如果不让cursor-1,就会,next()以后跳过原本的第i+1个节点
// 拿上面的例子来说,你要删除abc,但是在删除a以后会跳过b直接删除c
cursor--;
// 最近一个操作的节点被删除了,故重置为-1
lastRet = -1;
// 因为调用了外部类的remove方法,所以会改变modCount值,迭代器里也要获取最新的modCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
至于hasNext()方法没啥好说的,如果 cursor已经跟集合的长度一样长了,说明就已经迭代到底了。
2.ListItr 迭代器
ListItr 继承了 Itr 类,并且实现了 ListIterator 接口。其中,ListIterator 接口又继承了 Iterator 接口。他们的类关系图是这样的:
ListIterator 的类关系图
ListIterator 接口在 Iterator 接口的基础上,主要提供了六个新的抽象方法:
hasPrevious():是否有前驱节点;
previous():向前迭代;
nextIndex():获取下一个元素的索引;
previousIndex():返回上一个元素的索引;
set():替换元素;
add():添加元素;
可以看出来,实现了 ListIterator 的 ListItr 类要比 Itr 更加强大,不但可以向后迭代,还能向前迭代,还可以在迭代过程中更新或者添加节点。
private class ListItr extends Itr implements ListIterator {
// 可以自己设置迭代的开始位置
ListItr(int index) {
cursor = index;
}
// 下一节点是否就是第一个节点
public boolean hasPrevious() {
return cursor != 0;
}
public E previous() {
// 检查并发修改
checkForComodification();
try {
// 让游标指向当前节点
int i = cursor - 1;
// 使用AbstractList的get方法获取当前节点
E previous = get(i);
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
// 获取下一节点的下标
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();
}
}
}
这里比较不好理解的是下一节点还有当前节点这个概念,其实可以这么理解:cursor游标指定的必定是下一次 next()操作要得到的节点,因此cursor在操作前或者操作后指向的必定就是下一节点,因此相对下一节点,cursor其实就是当前节点,相对下一节点来说就是上一节点。
也就是说,假如现在有 a,b,c 三个元素,现在的 cursor 为2,也就是指向 b。调用 next()以后游标就会指向 c,而调用previous()以后游标又会指回 b。
至于lastRet这个成员变量只是用于记录最近一次操作的节点是哪个,跟方向性是无关。
五、AbstractList 实现的方法
1.add
注意,现在现在 AbstractList 的 add(int index, E e)仍然还不被支持,add(E e)只是定义了通过 add(int index, E e)把元素添加到队尾的逻辑。
// 不指定下标的add,默认逻辑为添加到队尾
public boolean add(E e) {
add(size(), e);
return true;
}
关于 AbstractList 和 AbstractCollection 中 add()方法之间的关系是这样的:
add方法的实现逻辑
AbstractList 这里的 add(E e)就非常有模板方模式提到的“抽象类规定算法骨架”这个感觉了。AbstractCollection 接口提供了 add(E e)的初步实现(尽管只是抛异常),然后到了 AbstractList 中就完善了 add(E e)方法的逻辑——通过调用 add(int index,E e)方法把元素插到队尾,但是具体的 add(int index,E e)怎么实现再交给子类决定。
2.indexOf/LastIndexOf
public int indexOf(Object o) {
ListIterator 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;
}./*欢迎加入java交流Q君样:909038429一起吹水聊天
public int lastIndexOf(Object o) {
ListIterator 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;
}
3.addAll
这里的addAll来自于List 集合的 addAll。参数是需要合并的集合跟起始下标:
public boolean addAll(int index, Collection extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) {
add(index++, e);
modified = true;
}
return modified;
}
这里的 rangeCheckForAdd()方法是一个检查下标是否越界的方法:
private void rangeCheckForAdd(int index) {
// 不得小于0或者大于集合长度
if (index < 0 || index > size())
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
4.removeRange
这个方法是 AbstractList 私有的方法,一般被子类用于删除一段多个元素,实现上借助了 ListIter 迭代器。
protected void removeRange(int fromIndex, int toIndex) {
ListIterator it = listIterator(fromIndex);
// 从fromIndex的下一个开始,删到toIndex
for (int i=0, n=toIndex-fromIndex; i
六、AbstractList 重写的方法
1.equals
equals()方法比较特殊,他是来自于 Collection 和 List 接口中的抽象方法,在 AbstractList 得中实现,但是实际上也是对 Object 中方法的重写。考虑到 equals()情况特殊,所以我们也认为它是一个重写的方法。
我们可以先看看 JavaDoc 是怎么说的:
比较指定对象与此列表是否相等。当且仅当指定对象也是一个列表,并且两个列表具有相同的大小,并且两个列表中所有对应的元素对相等时,才返回true
然后再看看源码是什么样的:
public boolean equals(Object o) {
// 是否同一个集合
if (o == this)
return true;
// 是否实现了List接口
if (!(o instanceof List))
return false;
// 获取集合的迭代器并同时遍历
ListIterator 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());
}
从源码也可以看出,AbstractList 的 equals() 是要求两个集合绝对相等的:顺序相等,并且相同位置的元素也要相等。
2.hashCode
hashCode() 和 equals()情况相同。AbstractList 重新定义了 hashCode()
public int hashCode() {
int hashCode = 1;
for (E e : this)
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}
新的计算方式会获取集合中每一个元素的 hashCode 去计算集合的 hashCode,这可能是考虑到原本情况下,同一个集合哪怕装入的元素不同也会获得相同的 hashCode,可能会引起不必要的麻烦,因此重写了次方法。
我们可以写个测试看看:
List list1 = new ArrayList<>();
list1.add("a");
System.out.println(list1.hashCode()); // 128
list1.add("c");
System.out.println(list1.hashCode()); // 4067
七、总结
List 接口继承了 Collection 接口,新增方法的特点主要体现在可以通过下标去操作节点,可以说大部分下标可以作为参数的方法都是 List 中添加的方法。
AbstractList 是实现了 List 的抽象类,他实现了 List 接口中的大部分方法,同时他继承了 AbstractCollection ,沿用了一些 AbstractCollection 中的实现。这两个抽象类可以看成是模板方法模式的一种体现。
他提供了下标版的 add(),remove(),set()的空实现。
AbstractList 内部提供两个迭代器,Itr 和 ListItr,Itr 实现了 Iterator接口,实现了基本的迭代删除,而 ListItr 实现了ListIterator,在前者的基础上增加了迭代中添加修改,以及反向迭代的相关方法,并且可以从指定的位置开始创建迭代器。
AbstractList 的 SubList 可以看成 AbstractList 的包装类,他在实例化的时候会把外部类实例的引用赋值给成员变量,同名的操作方法还仍然是调用 AbstractList 的,但是基于下标的调用会在默认参数的基础上加上步长,以实现对“视图”的操作,这是适配器模式思想的一种体现。
AbstractList 引入了并发修改下 fast-fail 的机制,在内部维护一个成员变量 modelCount,默认为零,每次结构性修改都会让其+1。在迭代过程中会默认检查 modelCount是否符合预期值,否则抛出异常。值得注意的是,这个需要实现类的配合,在实现 add()等方法的时候要让 modelCount+1。对于一些实现类,在迭代中删除可能会抛出 ConcurrentModificationExceptions,就是这方面的问题。
AbstractList 重写了 hashCode()方法,不再直接获取实例的 HashCode 值,而遍历集合,根据每一个元素的 HashCode 计算集合的 HashCode,这样保证了内容不同的相同集合不会得到相同的 HashCode。
最新2020整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友请加Q君样:909038429
/./*欢迎加入java交流Q君样:909038429一起吹水聊天