ArrayList和LinkedList的三种遍历方式

一.三种List遍历性能比较

List的遍历方式多样,那该怎么去选择呢?怎么去衡量,性能是爹,当然是性能说了算啊,下面就写个简易的程序简单测试一下。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        final int n = 100;
        ArrayList<Integer> arrayList = new ArrayList<Integer>(n);
        LinkedList<Integer> linkedList = new LinkedList<Integer>();
        for (int i = 0; i < n; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }

        double begin, end, time;
        int temp;

        begin = System.currentTimeMillis();
        for (int i = 0; i < n; i++) {
            temp = arrayList.get(i);
        }
        end = System.currentTimeMillis();
        time = end - begin;
        System.out.println("ArrayList--大小" + n + "--for:" + time);

        begin = System.currentTimeMillis();
        for (Integer integer : arrayList)
            temp = integer;
        end = System.currentTimeMillis();
        time = end - begin;
        System.out.println("ArrayList--大小" + n + "--foreach:" + time);

        begin = System.currentTimeMillis();
        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext())
            temp = iterator.next();
        end = System.currentTimeMillis();
        time = end - begin;
        System.out.println("ArrayList--大小" + n + "--iterator:" + time);

        //--------------------------------------------------------//

        System.out.println("--------------------------------------");
        begin = System.currentTimeMillis();
        for (int i = 0; i < n; i++) {
            temp = linkedList.get(i);
        }
        end = System.currentTimeMillis();
        time = end - begin;
        System.out.println("LinkedList--大小" + n + "--for:" + time);

        begin = System.currentTimeMillis();
        for (Integer integer : linkedList)
            temp = integer;
        end = System.currentTimeMillis();
        time = end - begin;
        System.out.println("LinkedList--大小" + n + "--foreach:" + time);

        begin = System.currentTimeMillis();
        Iterator<Integer> iterator1 = linkedList.iterator();
        while (iterator1.hasNext())
            temp = iterator1.next();
        end = System.currentTimeMillis();
        time = end - begin;
        System.out.println("LinkedList--大小" + n + "--iterator:" + time);

    }
}

输出结果:

1000条数据
ArrayList–大小1000–for:0.0
ArrayList–大小1000–foreach:1.0
ArrayList–大小1000–iterator:0.0
--------------------------------------
LinkedList–大小1000–for:2.0
LinkedList–大小1000–foreach:1.0
LinkedList–大小1000–iterator:0.0

100000条数据
ArrayList–大小100000–for:2.0
ArrayList–大小100000–foreach:2.0
ArrayList–大小100000–iterator:5.0
·--------------------------------------
LinkedList–大小100000–for:4951.0
LinkedList–大小100000–foreach:6.0
LinkedList–大小100000–iterator:1.0

把数据做个记录

ArrayList1001,00010,000100,0001,000,000
for0.00.01.01.08.0
foreach0.00.02.02.012.0
iterator0.00.01.01.011.0
LinkedList1001,00010,000100,0001,000,000
for0.01.049.04954.0很久
foreach0.00.01.07.022.0
iterator0.00.01.01.020.0

以上数据没有实验多次取平均值,有一些误差,但是,整体上可以看出在数据量比较小时,不同遍历方式的性能差别不大,但是在数据量比较大时,对于ArrayList来说,3钟遍历方式差距不是很大,其中for循环的效率最高,因为采用直接的下标运算,而对于LinkedList来说,迭代器效率最高,因为其相当于维护一个当前状态指针,遍历只需要扫描一遍双向链表即可,而for效率最低,因为需要扫描n遍链表,不难看出,两种List中,foreach的效率都比迭代器略低,因为其底层就是由迭代器实现的,只不过为了方便书写,做了简单的封装。

二.三种遍历List的方式底层分析

遍历List需要输出List的全部信息,其时间的消耗占比还是很大的,所以,选择正确的遍历方式非常重要,Java提供了三种遍历List的方式,下面我们来一一认识他们。

2.1 for循环

for循环是通过get(i)实现的,对于ArrayList来说,其实就是下标访问,时间复杂度为O(n),对于ArrayList来说,其底层是双向链表,每次都要从链表头或者表尾去遍历,具体是从头开始还是从尾开始取决于更靠近谁,所以其复杂度是指数级别的。
如果对ArrayList和LinkedList底层原理还不是很熟悉的可以查看这篇文章ArrayList和LinkedList的底层分析和比较

书写格式

        for (int i = 0; i < n; i++) {
            temp = arrayList.get(i);
        }

2.2 Iterator

首先分析一下ArrayList中迭代器的源码

    public Iterator<E> iterator() {
        return new Itr();//通过内部类实现
    }
    private class Itr implements Iterator<E> {
		int cursor;       // 下一个要访问元素的下标
        int lastRet = -1; // 上一个访问元素的下标
        int expectedModCount = modCount;

		public boolean hasNext() {//判断是否还有元素
            return cursor != size;
        }
        
		//返回下一个元素
		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];
        }

//遍历过程中删除元素
		public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);//调用ArrayList的remove方法删除的上一个访问过的元素
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;//重新设置modCount,因为ArrayList.remove修改了这个值
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
}

从源码可知,ArrayList的迭代器方式遍历本质上也是通过下标访问,只不过具体步骤封装起来麻烦了一点。迭代器里面有个remove方法,能够动态的修改modCount,所以在遍历时删除元素也不会抛出异常,需要注意的一点是其删除的元素是上一个访问过的元素。

下面再看一下LinkedList的源码

 public boolean hasNext() {
            return nextIndex < size;
        }

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

            lastReturned = next;//更新上次返回的指针
            next = next.next;
            nextIndex++;
            return lastReturned.item;
        }

通过内部类ListItr的源码可知,LinkedList的迭代器维护一个当前访问的数据的指针,找下一个元素把指针指向next即可,所以速度很快。

书写格式

        Iterator<Integer> iterator = linkedList.iterator();
        while(iterator.hasNext())
            temp=iterator.next();

2.3 for each

先写一个建议简易代码,然后反编译查看其底层原理

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList=new ArrayList<>();
        arrayList.add(1);
        int temp;
        for(Integer integer:arrayList)
        {
            temp=integer;
        }
    }
}

反编译

D:\Document\java\algorithm\src>javac Main.java
D:\Document\java\algorithm\src>javap -c Main.class
Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class java/util/ArrayList
       3: dup
       4: invokespecial #9                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: iconst_1
      10: invokestatic  #10                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      13: invokevirtual #16                 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      16: pop
      17: aload_1
      18: invokevirtual #20                 // Method java/util/ArrayList.iterator:()Ljava/util/Iterator;
      21: astore_3
      22: aload_3
      23: invokeinterface #24,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
      28: ifeq          51
      31: aload_3
      32: invokeinterface #30,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      37: checkcast     #11                 // class java/lang/Integer
      40: astore        4
      42: aload         4
      44: invokevirtual #34                 // Method java/lang/Integer.intValue:()I
      47: istore_2
      48: goto          22
      51: return
}

什么底层语言看不懂,问题不大,我们把class文件拖入idea可以更方便的反编译

import java.util.ArrayList;
import java.util.Iterator;

public class Main {
    public Main() {
    }

    public static void main(String[] var0) {
        ArrayList var1 = new ArrayList();
        var1.add(1);

        int var2;
        Integer var4;
        for(Iterator var3 = var1.iterator(); var3.hasNext(); var2 = var4) {
            var4 = (Integer)var3.next();
        }

    }
}

通过反编译,我们可以看到,foreach底层其实也是通过迭代器实现的,只不过用foeach更为方便。

三.遍历中能删除元素么

刚刚讨论了三种遍历方式的性能比较,对于遍历方式的选择大家心中也有了一杆明明白白的称,但是,在开发过程中,往往在遍历时需要删除元素,那遍历时,可以删除元素么?

3.1普通的for循环

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList=new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(2);
        arrayList.add(4);
        for(int i=0;i<arrayList.size();i++)
        {
            if(arrayList.get(i)==2)
            {
                arrayList.remove(i);
                //i--;下标没有-1会导致一些相同的元素没有被删除掉
                //下标-1后正确输出1 4
            }
        }
        for(int i=0;i<arrayList.size();i++)
        {
            System.out.println(arrayList.get(i));
        }
    }
}

输出结果为1 2 4;但是预期输出结果为1 4;出现这种情况是因为删除后直接把后面的元素前移了,而i如果没有-1,那么就会漏掉一些元素的比较。LinkedList结果也和上面一样,需要把i-1才能得到正确结果。

3.2 迭代器Iterator

import java.util.ArrayList;
import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList=new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(2);
        arrayList.add(4);

        Iterator<Integer> iterator = arrayList.iterator();
        while(iterator.hasNext())
        {
            if (iterator.next()==2)
                iterator.remove();
        }

        for(int i=0;i<arrayList.size();i++)
        {
            System.out.println(arrayList.get(i));
        }

    }
}


输出1 4,符合我们的预测,推荐使用。需要注意的是,删除用的是iterator的remove方法,由上期《ArrayList和LinkedList的底层分析和比较》 源码分析可以知道,迭代器的remove方法是先调用list的remove方法删除元素并且修改modCount,然后remove方法里会更新modCount实现,如果我们直接调用list的remove方法去删除元素,会导致迭代器里面记录的modCount和预期值不同而抛出ConcurrentModificationException异常。

3.3 for each

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList=new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        arrayList.add(4);

    for(Integer integer:arrayList)
    {
        if(integer==2)
            arrayList.remove(integer);
    }

        for(int i=0;i<arrayList.size();i++)
        {
            System.out.println(arrayList.get(i));
        }

    }
}

会抛出ConcurrentModificationException异常,至于为什么会抛出异常,反编译一下就可以知道原理了

import java.util.ArrayList;
import java.util.Iterator;

public class Main {
    public Main() {
    }

    public static void main(String[] var0) {
        ArrayList var1 = new ArrayList();
        var1.add(1);
        var1.add(2);
        var1.add(3);
        var1.add(4);
        Iterator var2 = var1.iterator();

        while(var2.hasNext()) {
            Integer var3 = (Integer)var2.next();
            if (var3 == 2) {
                var1.remove(var3);//相当于调用了直接调用了List的remove方法
            }
        }

        for(int var4 = 0; var4 < var1.size(); ++var4) {
            System.out.println(var1.get(var4));
        }

    }
}

反编译可以知道,foreach删除元素时,调用了List的remove方法,直接导致itetator记录的modCount和预期值不同,于是抛出异常。

总结一下,iterator遍历时删除元素没有那么多限制,推荐使用,简单for循环遍历时需要把脚标-1,不然会导致一些元素没有比较,foreach内部用iterator遍历,但是调用list的remove方法,会抛出异常,不推荐使用。

做一件短期没有回报的事很容易使人陷入迷茫的境遇,但是,能坚持下来的一定是个成功者!!!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值