java集合类概述以及迭代器思想

130 阅读5分钟

概述

java的集合类在工作中非常的常用,可是大部分人可能工作个5年都只用过 ArrayList,HashMap 也从来不懂里面的原理到底是什么样的。这次就来整体的梳理一下java集合类的相关知识点。

知识图谱

先来看看大概有哪些常用的类需要了解 集合类.png java的集合类可以大体的分为 Collection 和 Map . 可以笼统的理解为两个都是存放数据的容器。只不过Collection存放的是用户自定义的数据

public interface Collection<E> extends Iterable<E> {
}

而Map里面存放的都是 Entry, 然后 Entry包含了两个用户自定义的 key和value。

Collection

Collection是一个接口,继承于 Iterable接口。Iterable接口属于迭代器接口,其设计思想是每个集合的数据结构是不同的,用户是不知道如何正确的遍历该容器,所以抽象出一个Iterator接口,然后作为实现容器需要在容器内部自己写一个Iterator实现类完成遍历

image.png Collection接口只提供 add和remove操作 ,没有提供 get 操作。因为不同结构的集合get方式不能统一,只能交给更具体的子类来定义get方法。例如线性结构的 List 就可以提供 RandomAccess,而非线性结构 Set就做不到。下图显示了List,Set,Queue在接口层面对于Collection的扩展 在这里插入图片描述

Map

Map 往下并没有什么扩展的接口直接就是实现类了。 可以理解为Map是一种特殊的Collection,里面存放的都是Entry。

Iterator 遍历器

我们先来看看 Iterator 的接口定义

public interface Iterator<E> {
    # 是否有下一个
    boolean hasNext();
    # 返回下一个数据
    E next();
    # 删除当前数据
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}

相信我们在学习java的时候就听过不要在for循环的时候删除元素,需要用 Iterator 来删除,但是知道其中原理的人并不是很多。 我们拿ArrayList来举例子.看看为什么不能在for循环的时候删除元素。先看代码,我进行了简化

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    # 这个属性在父类AbstractList中,我拿出来好看
    protected transient int modCount = 0;
    # 装数据的容器
    transient Object[] elementData;

    public Iterator<E> iterator() {
        return new Itr();
    }



    # 就像我上文中说的,每个容器都有自己的迭代器实现
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public E next() {
            checkForComodification();
            int i = cursor;
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }


        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

}

这里有几个关键的变量

  1. modCount 修改次数
  2. cursor 当前迭代器的指针
  3. lastRet 当前迭代器指针的前一个位置
  4. expectedModCount 期望的修改次数

我们看来看这些变量是怎么工作的: 假设现在有一个容器里面装着 A B C D四个数据。

  1. 我创建了一个迭代器。这时候迭代器的 cursor 指向 A 的坐标, 当我调用了 next方法的时候 cursor和lastRet往前挪动了一格,然后把lastRet指向的A返回给用户。
  2. 我调用了集合的删除方法删除了A元素,看代码可知删除元素后,整个数组会移动,所以现在容器内部结构变成 B C D
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}
  1. 原本我已经取出A,然后在B的位置,但是我删除了A之后,再想取B的时候其实取出了C。

image.png

这就造成了数据的错误,但是如果这个操作删除的是B之后的数据,那么数据又不会错误。

这种行为叫做 结果不可预期行为或者未决行为

未决行为的解决

我们已经知道了在迭代器遍历的时候,如果调用了集合的删除元素方法,可能造成不可预期的错误。有的人可能会想,那么在删除的时候把指针往后挪一格是不是就不会出错了呢?答案是没错的。在迭代器中删除元素之后就会把指针再指回去,所以并不是遍历不能删除元素,而是不能删除元素的时候迭代器感知不到,这点很重要,因为在 ArrayBlockingQueue中的容器删除元素的方法里面主动的调用了所有的迭代器。那么就可以在遍历的时候调用容器的删除方法。

public void remove() {
    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

ArrayBlockingQueue 是在容器的层面解决这个问题,我们来看看在迭代器的角度如何解决这个未决行为。 我们再来回头看看迭代器的几个参数

  1. modCount 修改次数
  2. cursor 当前迭代器的指针
  3. lastRet 当前迭代器指针的前一个位置
  4. expectedModCount 期望的修改次数 当创建迭代器的时候 expectedModCount = modCount, 迭代器记录了当前容器的修改次数。然后在容器操作数据的时候都会 modCount++。当迭代器执行 next方法遍历的时候,都会 checkForComodification 判断我创建的时候的 modCount和我现在正在遍历的容器的modCount是否相等,如果不相等,那么就说明已经在我不知道的时候进行了修改,不论这个修改会不会产生影响,我都直接报错,这就是 Fail-fast 的思想