Java Spliterator 以及 Iterator 分析,基于 ArrayList

了解 ArrayList 中的 ArrayListSpliterator 和 Iterator

Posted by fyypumpkin on February 8, 2019

正文

我们在使用集合的时候,经常会使用到集合中的迭代器,今天我们就来讲讲集合中的迭代器。

在 ArrayList 源码中,有一个方法叫做 iterator,这个方法会给我们返回一个迭代器,我们就可以使用这个迭代器对集合中的元素进行访问。

{
    List<String> list = new ArrayList();
    list.add("one");
    list.iterator().next();
}

进入到 iterator 方法中,很简单的一行代码

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

其中 Itr 是一个内部类,我在这个类中加了注释,这个类就是集合中迭代器最重要的部分

   private class Itr implements Iterator<E> {
//        下一次要访问的指针
        int cursor;       // index of next element to return
//        最后被访问的元素下标(在 remove 的时候,必须先被访问过才能 remove)
        int lastRet = -1; // index of last element returned; -1 if no such
//        modCount,在使用迭代器过程中,该值的改变会导致异常
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
//            引用外部的 elementData, elementData 不是 volatile 的
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        // 在迭代器中要使用迭代器中的 remove 方法
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
//                通过持有的外部引用调用外部的 remove 方法,移除后,继续讲 lastRet 设置为 -1
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
//            这是一个遍历的方法,会从 cursor 位置开始不断调用 consumer
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            // 这里会检查 遍历中的 modCount 和期望的是否一致,不一致会抛出异常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

迭代器并不难,代码量也很小,上面的注释已经很详细了,就不再过多的解释了,下面来看一下 java 1.8 最新加入的分割迭代 Spliterator,怎么理解这个呢,我上一段代码

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        list.add("6");
        list.add("7");
        list.add("8");
        list.add("9");

        Spliterator<String> spliterator = list.spliterator();

        Spliterator<String> stringSpliterator = spliterator.trySplit();

        spliterator.forEachRemaining(System.out::println);
        
        System.out.println();

        stringSpliterator.forEachRemaining(System.out::println);
    }

看一下输出

5
6
7
8
9

1
2
3
4

可以看到,通过 Spliterator 将数据分割成多分,这样的好处就是可以并行的去处理集合中的数据,看一下 ArrayList 中的源码是怎么样的

static final class ArrayListSpliterator<E> implements Spliterator<E> {

        private final ArrayList<E> list;
        // todo 起始下标
        private int index; // current index, modified on advance/split
//        todo 结束下标 (如果值小于 0,则会在使用的时候,用 list 的 size 来填充)
        private int fence; // -1 until used; then one past last index
        private int expectedModCount; // initialized when fence set

        /** Create new spliterator covering the given  range */
        ArrayListSpliterator(ArrayList<E> list, int origin, int fence,
                             int expectedModCount) {
            this.list = list; // OK if null unless traversed
            this.index = origin;
            this.fence = fence;
            this.expectedModCount = expectedModCount;
        }

        private int getFence() { // initialize fence to size on first use
            int hi; // (a specialized variant appears in method forEach)
            ArrayList<E> lst;
//            todo 初始化 fence ,-1 的时候会使用 list 的 size 来填充
            if ((hi = fence) < 0) {
//                todo 当前 fence 小于 0, 如果 list 是空的,返回 0,否则返回 list 的 size
                if ((lst = list) == null)
                    hi = fence = 0;
                else {
                    expectedModCount = lst.modCount;
                    hi = fence = lst.size;
                }
            }
            return hi;
        }

        public ArrayListSpliterator<E> trySplit() {
//            todo hi == fence, lo == index, mid = (lo + hi) / 2
            int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
//            todo 如果 lo >= mid,说明待拆分的 list 太小了,返回 null (这里要注意空指针),这里可以看出来,他是向前拆分的,怎么说呢,我画一个图
//            todo split1 = 1 2 3 4 5 6 7 8 9
//            todo split2 = split1.trySplit()
//            todo split1 = 5 6 7 8 9 [4 - 9)
//            todo aplit2 = 1 2 3 4 [0 - 4)
            return (lo >= mid) ? null : // divide range in half unless too small
                new ArrayListSpliterator<E>(list, lo, index = mid,
                                            expectedModCount);
        }

        public boolean tryAdvance(Consumer<? super E> action) {
            if (action == null)
                throw new NullPointerException();
            int hi = getFence(), i = index;
            if (i < hi) {
//                todo 起始下标 +1
                index = i + 1;
//                todo 返回起始下标的元素,调用 Consumer
                @SuppressWarnings("unchecked") E e = (E)list.elementData[i];
                action.accept(e);
                if (list.modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                return true;
            }
            return false;
        }

        public void forEachRemaining(Consumer<? super E> action) {
            int i, hi, mc; // hoist accesses and checks from loop
            ArrayList<E> lst; Object[] a;
            if (action == null)
                throw new NullPointerException();
            if ((lst = list) != null && (a = lst.elementData) != null) {
                if ((hi = fence) < 0) {
                    mc = lst.modCount;
                    hi = lst.size;
                }
                else
                    mc = expectedModCount;
                if ((i = index) >= 0 && (index = hi) <= a.length) {
                    for (; i < hi; ++i) {
                        @SuppressWarnings("unchecked") E e = (E) a[i];
                        action.accept(e);
                    }
                    if (lst.modCount == mc)
                        return;
                }
            }
            throw new ConcurrentModificationException();
        }

        public long estimateSize() {
            return (long) (getFence() - index);
        }

        public int characteristics() {
            return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
        }
    }

源码也很短,是一个静态的内部类

我在源码中 trySplit 方法谢了一段注释,这段注释刚好是解释我在上面举得例子,上面为什么这么输出,不懂的可以理解一下这个注释,实际上就是一种分割,控制下标

本文首次发布于 fyypumpkin Blog, 作者 @fyypumpkin ,转载请保留原文链接.