Loading

java集合源码分析(二):List与AbstractList

概述

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<E>,这个方法允许我们通过传入的匿名实现类的方法去对集合中的每一个类做一些处理以后再放回去;
  • 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<E> 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<E> extends AbstractList<E> {
    // 把外部类AbstractList作为成员变量
    private final AbstractList<E> l;
    // 表示视图的起始位置(偏移量)
    private final int offset;
    // SubList视图的长度
    private int size;

    SubList(AbstractList<E> 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<E> extends SubList<E> implements RandomAccess {
    RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
        super(list, fromIndex, toIndex);
    }

    public List<E> 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<E> {
    // 迭代器认为后备列表应该具有的modCount值。如果违反了此期望,则迭代器已检测到并发修改。
    int expectedModCount = modCount;
    
    // 检查是否发生并发操作
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

结合代码,我们就不难理解这个 fast-fail 机制是怎么实现的了:

AbstractList 提供了一个成员变量用于记录对集合结构性修改的次数,如果子类希望实现并发修改错误的检查,就需要结构性操作的方法里让modCount+1。这样。在获取迭代器以后,迭代器内部会获取当前的modCount赋值给expectedModCount

当使用迭代器迭代的时候,每一次迭代都会检测modCountexpectedModCount是否相等。如果不相等,说明迭代器创建以后,集合结构被修改了,这个时候再去进行迭代可能会出现错误(比如少遍历一个,多遍历一个),因此检测到后会直接抛出 ConcurrentModificationException异常。

ListItr 继承了 Itr ,因此他们都有一样的 fast-fail机制。

值得一提的是,对于启用了 fast-fail 机制的实现类,只有使用迭代器才能边遍历边删除,原因也是因为并发修改检测:

2.Itr 迭代器

现在,回到 Itr 的代码上:

private class Itr implements Iterator<E> {
    // 后续调用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();
        }
    }
	
    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=1lastRet=-1i=1
  • 接着让lastRet=i,也就是当前指针指向新的当前节点 a,现在 lastRet=0cursor=1`,完成了对第一个节点 a 的迭代;
  • 重复上述过程,把节点中的每一个元素都处理完。

现在我们知道了迭代的方式,cursorlastRet 的作用,也就不难理解 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<E> {
    // 可以自己设置迭代的开始位置
    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<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;
}

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;
}

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<E> it = listIterator(fromIndex);
    // 从fromIndex的下一个开始,删到toIndex
    for (int i=0, n=toIndex-fromIndex; i<n; i++) {
        it.next();
        it.remove();
    }
}

六、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<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());
}

从源码也可以看出,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<String> 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。

posted @ 2020-12-01 19:24  Createsequence  阅读(229)  评论(0编辑  收藏  举报