【java容器的刻意练习】【八】ArrayList与LinkedList的遍历

我们使用容器经常会用到遍历,而之前几篇文章都没有提到这一点。所以,今天把这块内容补一下。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, 
        RandomAccess, 
        Cloneable, 
        java.io.Serializable

ArrayList 集成 AbstractList 抽象类。AbstractList 中提供了两个迭代器的实现类,默认实现了迭代器接口,实现了对元素的遍历,它们就是Itr 和其子类 ListItr。
ArrayList 实现 List 接口,能对它进行队列操作。
ArrayList 实现 RandomAccess 接口,
ArrayList 实现了 Cloneable 接口,即覆盖了函数clone(),能克隆。
ArrayList 实现 java.io.Serializable 接口,这意味着 LinkedList 支持序列化,能通过序列化去传输。

先看看 ArrayList 的三种遍历

        // 先创建一个100w元素的ArrayList
        for (int i=0; i < 1000000; i++){
            arrayList.add(i);
        }

        long start, end;

        // foreach循环
        start = System.currentTimeMillis();
        for (Integer item : arrayList) {
            //System.out.println(item);
        }
        end = System.currentTimeMillis();
        System.out.println("foreach cost time = " + (end-start) + "ms");

        // 普通for循环,通过get函数获取元素
        start = System.currentTimeMillis();
        for (int i=0; i < arrayList.size(); i++) {
            arrayList.get(i);
        }
        end = System.currentTimeMillis();
        System.out.println("for cost time = " + (end-start) + "ms");

        // 迭代器循环
        start = System.currentTimeMillis();
        Iterator<Integer> it = arrayList.iterator();
        while (it.hasNext()) {
            it.next();
        }
        end = System.currentTimeMillis();
        System.out.println("Iterator cost time = " + (end-start) + "ms");

foreach cost time = 15ms
for cost time = 7ms
Iterator cost time = 29ms

对同一个100w元素的ArrayList进行三种不同的循环,我们发现,最快的居然是通过get函数读取元素。

那么,foreach循环是什么东西,怎么会比迭代器快的呢?

使用foreach结构的类对象必须实现了Iterable接口,Java的Collection继承自此接口,List实现了Collection,Collection 这个接口仅包含一个函数,源码如下:

public interface Iterable<T> {
 
    /**
     * Returns an iterator over a set of elements of type T.
     *
     * @return an Iterator.
     */
    Iterator<T> iterator();

}

iterator()用于返回一个Iterator,原来,JAVA中的增强for循环底层是通过迭代器模式来实现的。

那为什么遍历ArrayList使用迭代器就这么慢呢?

我们看下public interface Iterator<E>接口的实现:

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

    /**
     * An optimized version of AbstractList.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;

        // prevent creating a synthetic constructor
        Itr() {}

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

        @SuppressWarnings("unchecked")
        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 = i + 1;
            return (E) elementData[lastRet = i];
        }

原来,ArrayList的迭代器的next()函数也是通过索引快速访问到元素的。如果数组比较小,那么不管用哪种遍历性能都差不多。如果数组比较大,那么,我们现在知道,用普通循环+get读取元素来遍历ArrayList,会比迭代器快一倍。这是因为get做了内联优化处理,节省大量的函数调用进出时间。

所以,如果有面试官问你,ArrayLsit哪种遍历最快?为什么?你会回答了吗?

接下来,我们来看看LinedList的定义。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, 
    Deque<E>, 
    Cloneable, 
    java.io.Serializable

LinkedList 集成AbstractSequentialList抽象类,内部使用listIterator迭代器来实现重要的方法
LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。

先看 LinkedList 的遍历。先插入100w个元素,然后用foreach、迭代器、removeFirst 这三种方法遍历(循环get不需要测试了,简直慢到难以忍受)。

        LinkedList linkedList = new LinkedList();
        for (int i=0; i<1000000; i++) {
            linkedList.add(i);
        }

        long start, end;

        start = System.currentTimeMillis();
        for (Object item : linkedList) {
            //System.out.println(item);
        }
        end = System.currentTimeMillis();

        System.out.println("foreach cost time = " + (end-start) + "ms");

        start = System.currentTimeMillis();
        Iterator<Integer> it = linkedList.iterator();
        while (it.hasNext()) {
            it.next();
        }
        end = System.currentTimeMillis();
        System.out.println("Iterator cost time = " + (end-start) + "ms");

        start = System.currentTimeMillis();
        while (linkedList.size() != 0) {
            linkedList.removeFirst();
        }
        end = System.currentTimeMillis();
        System.out.println("removeFirst cost time = " + (end-start) + "ms");

运行代码得到结果如下:

foreach cost time = 12ms
Iterator cost time = 11ms
removeFirst cost time = 24ms

印证了之前我们分析的结论,foreach就是Iterator,所以它们的效率都差不多。不过从代码精简方面来看,当然是foreach更简单。

而某些文章提到用removeFirst会遍历更快,但是在笔者的环境下不成立。

为什么呢?我们先看看next()的实现:

    private class ListItr implements ListIterator<E> {

        private Node<E> lastReturned;
        private Node<E> next;
        private int nextIndex;
        private int expectedModCount = modCount;

        public E next() {
            checkForComodification();
            if (!hasNext())
                throw new NoSuchElementException();

             
            lastReturned = next;
            next = next.next;
            nextIndex++;
            return lastReturned.item;
        }

真相大白!原来LinkedList为了用迭代器顺序遍历,用了lastReturned保存最近一次返回的节点,next下一次返回的节点,nextIndex保存下一个节点的索引。

再将元素增大一倍,就是200万个元素。

foreach cost time = 20ms
Iterator cost time = 21ms
removeFirst cost time = 37ms

removeFirst还是比迭代器多10多毫秒。证明这个多10ms应该是多了节点删除操作的开销。

ok,那么今天我们的结论就是

  • ArrayList推荐用for循环get元素遍历最快,不仅是连续内存读取,而且还有内联优化节省函数调用开销。
  • LinkedList推荐用foreach遍历元素最快,因为内部实现是迭代器。
  • 相同的数据量,ArrayList最快的遍历比LinkedList最快遍历快一倍。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容