JDK源码阅读(六) : 容器——ArrayList

本文详述了ArrayList和LinkedList两种Java集合容器,探讨它们的构造、增删改查操作,尤其是ArrayList的fast-fail机制。ArrayList适用于随机访问,不适合频繁插入删除,而LinkedList适合插入删除但访问较慢。此外,还提到了规避并发修改异常的方法和CopyOnWriteArrayList的使用场景。
摘要由CSDN通过智能技术生成

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++;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值