一.三种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
把数据做个记录
ArrayList | 100 | 1,000 | 10,000 | 100,000 | 1,000,000 |
---|---|---|---|---|---|
for | 0.0 | 0.0 | 1.0 | 1.0 | 8.0 |
foreach | 0.0 | 0.0 | 2.0 | 2.0 | 12.0 |
iterator | 0.0 | 0.0 | 1.0 | 1.0 | 11.0 |
LinkedList | 100 | 1,000 | 10,000 | 100,000 | 1,000,000 |
---|---|---|---|---|---|
for | 0.0 | 1.0 | 49.0 | 4954.0 | 很久 |
foreach | 0.0 | 0.0 | 1.0 | 7.0 | 22.0 |
iterator | 0.0 | 0.0 | 1.0 | 1.0 | 20.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方法,会抛出异常,不推荐使用。
做一件短期没有回报的事很容易使人陷入迷茫的境遇,但是,能坚持下来的一定是个成功者!!!