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

概述

上一篇文章基本介绍了 List 接口的上层结构,也就是 Iterable 接口,Collection 接口以及实现了 Collection 接口的抽象类的基本状况,如今在前文的基础上,咱们将继续向实现前进,进一步的探索 List 接口与其抽象实现类 AbstractList 的源码,了解他是如何在三大实现类与 Collection 接口之间实现承上启下的做用的。java

1、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 接口的默认实现,换成了根据顺序的分割。

2、AbstractList 抽象类

AbstractList 类是一个继承了 AbstractCollection 类而且实现了 List 接口的抽象类,它至关于在 AbstractCollection 后的第二层方法模板。是对 List 接口的初步实现,同时也是 Collection 的进一步实现。安全

1.不支持的实现

能够直接经过下标操做的set()add()remove()都是 List 引入的新接口,这些都 AbstractList 都不支持,要使用必须由子类重写。并发

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,对应获取他们的方法是:dom

  • iterator():获取 Itr 迭代器类;
  • listIterator():获取 ListItr 迭代器类。这是个多态方法,能够选择是否从指定下标开始,默认从下标为0的元素开始迭代;

视图类 SubList 和 RandomAccessSubList:函数

  • subList():获取视图类,会自动根据实现类是否继承 RandomAccess 而返回 SubList 或 RandomAccessSubList。

这些内部类一样被一些其余的方法所依赖,因此要全面的了解 AbstractList 方法的实现,就须要先了解这些内部类的做用和实现原理。测试

3、subList方法与内部类

subList()算是一个比较经常使用的方法了,在 List 接口的规定中,这个方法应该返回一个当前集合的一部分的视图:this

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 的操做转变为了对 “视图的操做”。

咱们先看看 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()方法时能够分辨这三者。

4、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这个成员变量只是用于记录最近一次操做的节点是哪一个,跟方向性是无关。

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

6、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

7、总结

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。

相关文章
相关标签/搜索