1. List家族
List是集合类容器的上层接口,List家族有很多我们常用的容器,这些容器都实现了List接口,使用较多的是ArrayList和LinkedList,本文主要讨论这两种容器。List容器中存储的元素可以重复。
2. ArrayList
ArrayList不是多线程版本的容器,因此不需要做同步操作synchronized,保证了操作的效率,如果我们需要对单个ArrayList容器做多线程操作,可以在外部进行同步控制。而List家族另一个古老的容器类Vector则是多线程版本的,现在使用不多。
2.1 继承关系和成员属性
ArrayList继承自一个抽象类AbstractList(抽象类命名通常以Abstract开始),该类主要提供一些基础操作。其次,实现了List, RandomAccess, Cloneable, java.io.Serializable等必要的接口。(以jdk11 LTS为例)
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
//默认容器的最大容量为10
private static final int DEFAULT_CAPACITY = 10;
//空数组,静态成员,多个空的容器实例可以共享该空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//空数组,静态成员,默认最大容量为10
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//元素将存储于elementData数组中,容器的最大容量就是这个数组的长度,长度可扩充。
transient Object[] elementData; // 非private,以简化嵌套类访问
//容器中包含的元素数量
private int size;
elementData数组使用transient修饰,不可用于序列化。简单来说,transient修饰符修饰的变量只可存在于内存中,不能持久化到硬盘中。比如,如果存储的是用户的银行卡密码,考虑安全性,不希望在网络操作中被序列化传输。
2.2 构造实例
该类提供了三种构造器:
- 传入起始容量的构造器。如果我们可以大概确定要存的元素数量,则可以在构造实例时就指定容器容量,避免扩容操作带来的开销;
- 空参构造器。使用该构造器构造实例时,只分配一个空数组,当添加元素时,分配指定大小的数组,这是一种延时分配存储空间的策略,避免我们在创建了容器后忘记使用而导致的资源浪费。
- 传入一个可能包含元素的容器,并将该容器中所有的元素都复制到ArrayList的elementData数组中。
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//创建指定大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
//空数组,但是默认最大容量为10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
//将该容器转换成数组
Object[] a = c.toArray();
//如果数组长度大于0
if ((size = a.length) != 0) {
//如果传入的容器是ArrayList类型的
if (c.getClass() == ArrayList.class) {
//直接将elementData指向该数组
elementData = a;
} else {
//如果传入的容器不是ArrayList类型,则将该数组中的所有元素复制到一个新建的Object数组中,
//elementData指向该数组
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 如果数组长度为0,则直接指向可共享的空数组
elementData = EMPTY_ELEMENTDATA;
}
}
2.3 增删改查
2.3.1 增
①add()方法在数组实际存储的最后一个元素后面添加一个元素
public boolean add(E e) {
//容器被修改的次数加1
modCount++;
//在size的位置放上要添加的元素,代码见下文
add(e, elementData, size);
return true;
}
在size的位置放上要添加的元素。
private void add(E e, Object[] elementData, int s) {
//如果数组的容量不够了
if (s == elementData.length)
//需要扩容,代码见下文
elementData = grow();
//如果容量够的话,直接在s的位置放上这个元素
elementData[s] = e;
//容器中存储的元素数量加1
size = s + 1;
}
扩容操作,一般会扩大为原容量的1.5倍。
private Object[] grow() {
//最少需要size+1的容量
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
//创建一个新的数组,至少需要minCapacity的容量,然后将原数组中的元素全部复制到新数组中,再将elementData指向新数组
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));//代码见下文
}
//返回一个大于等于最低需求容量的容量值
private int newCapacity(int minCapacity) {
// overflow-conscious code
//原来的数组容量
int oldCapacity = elementData.length;
//将原来的数组容量扩大至1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩大1.5倍后,新的容量小于最低需求容量
if (newCapacity - minCapacity <= 0) {
//如果当前存储元素的数组是默认的空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//返回默认的容量10和最低需求容量的最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // 如果最低需求容量溢出了,则抛出OOM
throw new OutOfMemoryError();
//正常情况下,返回最低需求容量
return minCapacity;
}
//如果新的容量大于最低需求容量,并且新容量不超过设置的最大数组长度(Integer.MAX_VALUE-8),则返回新容量
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);//否则,将根据情况返回容量值,代码见下文
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 溢出则抛出OOM
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) //如果最小容量大于设置的最大数组长度,则返回int的最大值
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE; //否则返回设置的最大数组长度
}
②add(int, E) 在指定位置放上一个元素。
public void add(int index, E element) {
//检查指定的位置是否越界,超出了则抛出异常
rangeCheckForAdd(index);
//容器修改次数加1
modCount++;
final int s;
Object[] elementData;
//如果数组的容量等于实际存储的元素数量,则需要扩容
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
//将原数组中index开始的元素都往后移动一位
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
//在index的位置上放上新的元素
elementData[index] = element;
//数组中元素的数量加1
size = s + 1;
}
③addAll(Collection<>) 将给定容器中的所有元素都加入到本容器的尾部。
public boolean addAll(Collection<? extends E> c) {
//先将容器转换成数组
Object[] a = c.toArray();
modCount++;
//取得该容器的数组的长度(容量)
int numNew = a.length;
//如果为空,则返回false,表明没有添加任何元素
if (numNew == 0)
return false;
Object[] elementData;
final int s;
//如果要加入的元素数大于本容器数组的可用容量
if (numNew > (elementData = this.elementData).length - (s = size))
//扩容
elementData = grow(s + numNew);
System.arraycopy(a, 0, elementData, s, numNew);
size = s + numNew;
return true;
}
2.3.2 删
①remove(int) 删除指定位置上的元素。
public E remove(int index) {
//首先检查是否越界
Objects.checkIndex(index, size);
//使用一个局部变量指向容器实例的数组
final Object[] es = elementData;
//取出index位置上的元素
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
//删除index位置上的元素,代码见下文
fastRemove(es, index);
//返回要删除的元素
return oldValue;
}
快速删除给定位置上的元素(无越界检查和返回元素)
private void fastRemove(Object[] es, int i) {
//修改次数加1
modCount++;
//新的元素数
final int newSize;
//如果不是删除最后一位上的元素
if ((newSize = size - 1) > i)
//将i+1位置开始的全部元素前移一位
System.arraycopy(es, i + 1, es, i, newSize - i);
//最后一位置为null
es[size = newSize] = null;
}
②remove(Object) 删除容器数组中的一个具体元素,该元素equals传入的参数,只删除第一次出现位置上的元素。
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
found: {
//对于传入的参数为null的情况,单独考虑,避免出现空指针异常
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
//第一次出现时就退出循环
break found;
} else {
for (; i < size; i++)
//调用元素的equals方法来判断两个元素是否相同
if (o.equals(es[i]))
break found;
}
//如果没有跳出found,则说明没有找到,返回false
return false;
}
//删除该位置上的元素
fastRemove(es, i);
return true;
}
③removeAll(Collection<>) 从本容器中批量删除与给定容器中所有元素相同的元素。
public boolean removeAll(Collection<?> c) {
//批量删除,代码见下文
return batchRemove(c, false, 0, size);
}
批量删除。
//从本容器from的位置开始搜索,直到end位置
boolean batchRemove(Collection<?> c, boolean complement,
final int from, final int end) {
//c为空,则抛出异常
Objects.requireNonNull(c);
final Object[] es = elementData;
//用两个指针r和w,r从本容器的from位置开始搜索,到end退出
int r;
for (r = from;; r++) {
if (r == end)
return false;
//r指向第一个要删除的元素的位置
if (c.contains(es[r]) != complement)
break;
}
//w指向已经遍历过的需要保留的最后一个元素的后一位
int w = r++;
try {
for (Object e; r < end; r++)
//如果当前位置不是要删除的位置,则将其放到w位置上,然后w后移一位
if (c.contains(e = es[r]) == complement)
es[w++] = e;
//这样w前面的元素全部都是要保留下来的元素
} catch (Throwable ex) {
//当c.contains()出现异常,将r开始的元素全部复制到w位置及其后面,这样可以保持了已经做出的改变和尚未遍历的部分
System.arraycopy(es, r, es, w, end - r);
w += end - r;
throw ex;
} finally {
//修改次数为已经删除的元素数
modCount += end - w;
//将后面空出来的位置都置为null
shiftTailOverGap(es, w, end);
}
return true;
}
④removeIf(Predicate<>, int, int) 给定一个检测器Predicate,从本容器数组的i位置开始查找,一直到end为止,如果满足条件,则删除该位置上的元素。
//jdk11
boolean removeIf(Predicate<? super E> filter, int i, final int end) {
Objects.requireNonNull(filter);
int expectedModCount = modCount;
final Object[] es = elementData;
//找到第一个满足条件的位置,i指向该位置
for (; i < end && !filter.test(elementAt(es, i)); i++)
;
//如果i没有越界
if (i < end) {
//beg指向第一个满足条件的位置
final int beg = i;
//nBits(int n)将会新建出一个位图bitmap,deathRow数组指向这个位图
//对于匹配成功的元素的偏移量(相对于beg),将会在该位图中的对应位置做出标记
final long[] deathRow = nBits(end - beg);
//由于beg位置就是匹配成功的位置,所以deathRow[0]的第0位被置为1
deathRow[0] = 1L;
//开始第一轮循环,将满足条件的位置都在deathRow数组中进行位标记
for (i = beg + 1; i < end; i++)
if (filter.test(elementAt(es, i)))
//标记方式,代码见下文
setBit(deathRow, i - beg);
//如果modCount发生变化,则说明存在多线程操作且没有同步措施
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
modCount++;
//w指向起始位置beg,w始终指向已知的需要保留的最后一个元素的后一位
int w = beg;
//开始第二轮循环,如果该位置上的元素没有被标记过,则将其移动到w位置上,w后移一位
for (i = beg; i < end; i++)
//判断规则,代码见下文
if (isClear(deathRow, i - beg))
es[w++] = es[i];
//最后将尾部空出来的位置都置为null,size削减
shiftTailOverGap(es, w, end);
return true;
} else {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
return false;
}
}
nBits方法创建一个long数组用作位图,每个long元素的每一位都可作为标记。因为long类型是64位,所以每64个元素就可以标记在1个long值上。返回创建好的long数组。 i>>6 相当于除以64。
private static long[] nBits(int n) {
return new long[((n - 1) >> 6) + 1];
}
setBit方法将位图数组中对应位置的第 i 位标记为1。这里的 i 是相对于beg的偏移量,而beg则是第1个满足条件的位置。
private static void setBit(long[] bits, int i) {
bits[i >> 6] |= 1L << i;
}
首先按照上文所述的标记规则,将 i 除以64,得到long数组的索引。1L左移了 i 位,意味着long数组的这个位置上元素的第 i 位设置为1。
比如,当i=0,bits[0]的第0位被置为1;当i=1,bits[0]的第1位被置为1;依次重复下去;当i=63,bits[0]的第63位被置为1。
当i=64,i>>6 = 1,所以应该在bits[1]上做标记。
那么1L << 64 是否会溢出呢?
答案是不会。1L左移64位会得到1L自身。可以理解为,在左移操作前,要先对左移的次数取模(依据前面元素的类型宽度),也就是说要先将64mod64,得到的是0。所以1L左移0位,还是1L。
值得注意的另外一种情况是,如果先将1L左移63位,再左移1位的时候,将会溢出。
private static boolean isClear(long[] bits, int i) {
return (bits[i >> 6] & (1L << i)) == 0;
}
isClear方法按照前面设定的规则,判断偏移量为 i 的位置是否为0,如果是0,则表示是clear的,没有被标记过,该位置应该保留。
2.3.3 改
①set(int, E)方法将给定索引位置上的元素值修改为其他元素值,最后返回索引位置的旧值。
public E set(int index, E element) {
Objects.checkIndex(index, size);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
2.3.4 查
①get(int) 获取到给定索引位置的元素。
public E get(int index) {
//首先判断是否越界
Objects.checkIndex(index, size);
//返回数组中对应位置的元素
return elementData(index);
}
②indexOf(Object) 方法获取容器数组中,与给定元素相等的第一个元素所在的索引位置。
public int indexOf(Object o) {
//代码见下文
return indexOfRange(o, 0, size);
}
indexOfRange方法,如果没有找到,则返回-1。
int indexOfRange(Object o, int start, int end) {
Object[] es = elementData;
//首先考虑传入的元素为null的情况,避免出现空指针异常
if (o == null) {
for (int i = start; i < end; i++) {
if (es[i] == null) {
return i;
}
}
} else {
//如果传入的元素非空,则遍历整个数组,调用参数的equals方法来判断是否相同,返回第一个相同的元素的索引
for (int i = start; i < end; i++) {
if (o.equals(es[i])) {
return i;
}
}
}
return -1;
}
那么,当我们要判断容器中是否存储了某个元素时,只需调用indexOf方法,判断返回值是否为-1即可。本类中的contains方法就是这么做的。
此外,还有lastIndexOf(Object) 方法,可以获取最后一个相等的元素的索引。
2.4 迭代器
iterator() 方法返回本容器的迭代器
public Iterator<E> iterator() {
return new Itr();
}
Itr类是ArrayList的内部类。初始时刻,迭代器指向第一个元素的上一位置,cursor指向下一个位置。
private class Itr implements Iterator<E> {
//cursor指向下一个要返回的元素的索引,默认值为0
int cursor;
//lastRet指向上一次返回的元素的索引,初始值设为-1
int lastRet = -1;
//期待的修改次数,被初始化为迭代器实例创造时的容器修改次数
int expectedModCount = modCount;
// prevent creating a synthetic constructor
Itr() {}
}
hasNext() 判断迭代器是否有下一个元素,通过判断游标cursor是否到达了size位置,如果到达了size,则说明没有下一个可以返回的元素了。
public boolean hasNext() {
return cursor != size;
}
next() 返回迭代器下一位置的元素值,即cursor所指的元素值。
public E next() {
//验证线程安全
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
//cursor加1
cursor = i + 1;
//返回原cursor所指位置的元素值,由于是使用Object数组存储的,所以要重新强制转换成泛型参数
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
//判断迭代器使用期间容器是否被修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
remove方法将移除迭代器所指位置的元素值,即lastRet位置的元素,同时cursor会前移一位。
public void remove() {
//迭代器指向-1时,不可以做remove操作
if (lastRet < 0)
throw new IllegalStateException();
//验证线程安全问题
checkForComodification();
try {
//移除lastRet位置的元素
ArrayList.this.remove(lastRet);
//cursor前移到lastRet位置
cursor = lastRet;
//lastRet置为1
lastRet = -1;
//自身修改完容器,要更新expectedModCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
值得注意的是,使用了一次remove操作后,lastRet被置为-1,这也就意味着我们不可以立刻再次执行remove操作,这是因为如果容器中仅剩1个元素时,且迭代器指向这个元素,做了remove操作后,就没有元素了,不可以再删除。
2.5 fast-fail机制
(1)什么是fast-fail机制?
fail-fast 机制是java集合中的一种错误检测机制。在java.util包下的容器List、Map和Set等,都使用了fail-fast机制。
在系统设计中,快速失效系统(fail-fast) 是一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。这种设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障。快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。
(2)jdk源代码中如何体现fast-fail机制?
根据2.4节迭代器的实现代码,我们可以看到起在调用next()、remove()方法时都调用了checkForComodification()方法,检查expectedModCount是否等于modCount,如果不等,则会抛出ConcurrentModificationException异常,从而产生了fast-fail机制。
当某个线程对容器进行了修改,modCount的值就会发生变化,而expectedModCount在迭代器实例被创建时初始化为当前时刻modCount的值,而后只在本迭代器对容器修改时,才会更新该值。那么如果在使用迭代器期间,其他线程修改了容器,就会导致这两个值不同,从而抛出线程安全异常。
事实上,单线程和多线程都会导致fast-fail问题,只要在本线程操纵迭代器的过程中,容器发生了修改,不管是本线程修改的(使用容器类自身的remove方法等)还是其他线程修改的,都会导致expectedModCount不等于modCount。
如果是在使用迭代器遍历过程中,使用迭代器的remove方法,会将expectedModCount更新成modCount,不会发生fast-fail。
另外,在foreach循环中不能使用元素的add或remove方法。原因是,List的foreach循环是基于while循环和迭代器实现的。在迭代器遍历过程中,使用了类自身的remove方法,将导致expectedModCount不等于modCount。
(3)特殊情况
上面说到,迭代器的remove方法会将expectedmodCount值设置为了modCount值,如果检测完成后,才发生其他线程修改容器的操作,而后expectedmodCount又更新为modCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
(4)如何规避fast-fail?
如果是在多线程环境下使用fail-fast机制的集合,建议使用java.util.concurrent包下的类取代java.util包下的类。
比如,使用CopyOnWriteArrayList类来替代ArrayList。CopyOnWriteArrayList是一种线程安全的List容器类,执行写时复制策略。当进行读操作时,不会加锁,因为读操作不会改变容器的内容,也就不会发生线程安全问题。而在写的时候,不直接往当前容器中添加元素,而是先将当前容器数组copy一份,再往新数组中添加元素,添加完成后,再将原容器的数组引用指向新数组。CopyOnWriteArrayList中add和remove等写方法是需要加锁的,避免copy出多个副本出来。
CopyOnWrite的迭代器是只读的,不支持增删改,因为这个迭代器遍历的仅仅是一个容器数组的快照。该快照不发生变化,只在copy出的新数组中进行写操作,所以不会发生fast-fail。
局限性:CopyOnWrite容器仅适用于写操作非常少的场景,而且只能保证数据的最终一致性,不能保证数据的实时一致性。新写入的元素,是不可以立刻被迭代器遍历到的。因为迭代器只在旧的快照上进行遍历。
2.6 其他方法
①trimToSize() 将容器的数组长度减少至其实际存储的元素数,以节省空间。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA //如果长度为0,则使用共享的空数组
: Arrays.copyOf(elementData, size); //否则,拷贝出一个新数组
}
}
②sort方法对容器中的元素,按照给定的排序规则(由Comparator定义),进行排序。
public void sort(Comparator<? super E> c) {
//记下当前容器被修改的次数
final int expectedModCount = modCount;
//对数组进行排序
Arrays.sort((E[]) elementData, 0, size, c);
//如果在排序其间,容器被其他线程修改了,则抛出异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
modCount++;
}
Comparator的匿名实现类需要实现Comparator接口中的compare方法和equals方法。
具体的排序实现方式,本节不作讨论。
3. LinkedList
LinkedList是对List接口和Deque(双端队列)接口的双链表实现。
3.1 继承关系和成员属性
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//size记录了链表的元素数量
transient int size = 0;
//first指针指向了链表的第一个节点,LinkedList的节点结构采用了内部类Node<E>,实现方式见下文
transient Node<E> first;
//last指针指向了链表的最后一个节点
transient Node<E> last;
}
private static class Node<E> {
E item;//数据域
Node<E> next;//前驱
Node<E> prev;//后继
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
3.2 构造器
①空参构造器,初始时刻链表为空。
public LinkedList() {
}
②利用现有的容器实例来构造LinkedList。将给定容器中的所有元素都添加到新构造的LinkedList的双链表中。
public LinkedList(Collection<? extends E> c) {
//首先调用空参构造器,以构造父类对象,并且初始化实例属性
this();
//将容器c的内容添加到本容器的链表的尾部,按照该容器的迭代器遍历次序添加,后文介绍代码实现方式
addAll(c);
}
3.3 增删改查
3.3.1 增
①add(E) 在双链表尾部添加一个元素。
public boolean add(E e) {
//在链表尾部添加元素
linkLast(e);
return true;
}
void linkLast(E e) {
//取得链表尾指针
final Node<E> l = last;
//构造一个新节点,前驱是尾指针,后继为null,数据域为该元素值
final Node<E> newNode = new Node<>(l, e, null);
//尾指针后移一位
last = newNode;
//如果尾指针也是null,则说明这是首节点
if (l == null)
first = newNode;
else
//否则,设置前驱节点的后继指针指向新节点
l.next = newNode;
//元素数加1
size++;
//修改次数加1
modCount++;
}
②add(int, E) 在链表的某个索引位置添加元素。
public void add(int index, E element) {
checkPositionIndex(index);
//如果正好是在链表尾部添加,则直接调用linkLast方法
if (index == size)
linkLast(element);
//否则,调用linkBefore方法,在index位置上添加
else
linkBefore(element, node(index));
}
node(index)方法获取到给定索引位置的节点指针。
Node<E> node(int index) {
// assert isElementIndex(index);
//首先判断index是在链表的前半部分还是后半部分,如果是在前半部分,则使用头指针来遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
//退出循环时,x指向了index位置的节点
return x;
} else {//如果是在后半部分,则使用尾指针来遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
linkBefore方法在某个节点位置上插入一个节点。由此,我们可以学习到jdk源码中是如何实现双链表的插入和删除操作的。
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//取得插入位置的前驱指针
final Node<E> pred = succ.prev;
//构造新的节点,设置前驱指针,后继指针指向插入位置
final Node<E> newNode = new Node<>(pred, e, succ);
//设置后继节点的前驱
succ.prev = newNode;
//如果前驱为null,则设置新节点为首节点
if (pred == null)
first = newNode;
else
//否则将前驱节点的后继设置为新节点
pred.next = newNode;
size++;
modCount++;
}
③addAll(int, Collection<>) 方法在给定索引位置上插入给定容器中的所有元素。如果添加成功,则返回true。
public boolean addAll(int index, Collection<? extends E> c) {
//检查添加位置是否越界
checkPositionIndex(index);
//将容器转换成数组
Object[] a = c.toArray();
//获取要添加的数组的长度
int numNew = a.length;
//如果长度为0,则不用添加
if (numNew == 0)
return false;
//前驱和后继指针
Node<E> pred, succ;
//如果插入位置为链表尾部,则新节点的前驱设置为尾指针,后继为null
if (index == size) {
succ = null;
pred = last;
} else {
//否则,调用node方法获取到该索引位置的节点指针,用作新节点的后继
succ = node(index);
//获取前驱指针
pred = succ.prev;
}
//遍历数组
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//依次为数组中的每个元素构造节点,但只设置前驱
Node<E> newNode = new Node<>(pred, e, null);
//考虑首节点的情况
if (pred == null)
first = newNode;
else
//设置各个节点的后继
pred.next = newNode;
//更新前驱节点
pred = newNode;
}
//处理剩余的节点
//如果初始插入位置为链表尾部,则重新设置尾指针
if (succ == null) {
last = pred;
} else {//否则,设置当前节点的后继和后继节点的前驱
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
3.3.2 删
①remove(Object) 删除某个元素,该元素等于给定的参数。
public boolean remove(Object o) {
//考虑参数为null的情况,避免空指针异常
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
//移除x指针指向的节点,代码见下文
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
unlink方法移除给定指针指向的节点,并返回被移除的节点的数据域。
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
//取得前驱指针
final Node<E> next = x.next;
//取得后继指针
final Node<E> prev = x.prev;
//如果要移除的是头结点
if (prev == null) {
//头指针更新为下一个节点
first = next;
} else {
//前驱的后继指向当前节点的后继
prev.next = next;
x.prev = null;
}
//如果要移除的是尾节点
if (next == null) {
//尾指针更新为前一个节点
last = prev;
} else {
//后继的前驱指向当前节点的前驱
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
3.3.3 改查
改查方法几乎都基于上述方法,不再介绍。
3.4 迭代器
LinkedList的迭代器实现与ArrayList类似,仅增加了链表的特性。
private class ListItr implements ListIterator<E> {
//上一次返回的节点指针,默认为null
private Node<E> lastReturned;
private Node<E> next;
//下一个节点索引
private int nextIndex;
private int expectedModCount = modCount;
//按照给定的索引,取得迭代器
ListItr(int index) {
// assert isPositionIndex(index);
//如果给定的index在size处,则没有下一个节点,next为空;否则,next指向index位置的节点
next = (index == size) ? null : node(index);
nextIndex = index;
}
//当nextIndex到了size,则没有下一个节点
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
//返回next指针指向的节点数据,更新lastReturned
lastReturned = next;
//next指针后移
next = next.next;
nextIndex++;
return lastReturned.item;
}
//是否有前节点的实现,也是类似
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
//移除的是lastReturned指向的节点
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
//lastReturned置为null,意味着一次remove之后不可以立刻再次remove
lastReturned = null;
expectedModCount++;
}
}