有关不同实现类的List的三种遍历方式的探讨
我们知道,List的类型有ArrayList和LinkedList两种,而曾经的Vector已经被废弃。
而作为最常用的操作之一,List的顺序遍历也有三种方式:借助角标的传统遍历、使用内置迭代器和显式迭代器。
下面,将首先给出两种种不同类型实现的实验结果,之后,将会通过分析JAVA中List的各种实现,来探讨造成实验结果的原因。
1.随机数据的生成
package temp; import java.io.*; import java.util.Random; public class Datamaker { final static int MAXL = 10; final static int MAXN = 30000000; public static String getRandomString(int length){ String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random=new Random(); StringBuffer sb=new StringBuffer(); for(int i=0;i<length;i++){ int number=random.nextInt(62); sb.append(str.charAt(number)); } return sb.toString(); } public static void main(String[] args) throws IOException { String fileName = "src//temp//data.txt"; BufferedWriter bw = new BufferedWriter(new FileWriter(fileName)); for(int i = 0; i < MAXN; i++) bw.write(getRandomString((int) (Math.random() * MAXL) + 1) + "\n"); System.out.println("Generate finish.\n"); bw.flush(); bw.close(); } }
如上所示,我们生成3 * 10 ^ 7组测试用例,测试用例中的每行由长度为1到10的随机字符串组成。
实际上,由于字符串的存储速度与List的实现方式无关,为了减少生成数据的时间,于是采用了较短的字符串长度。
生成的数据保存在目录下的Data.txt文件中。
2.测试用代码
package temp; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.*; public class Temp { static List<String> S = new ArrayList<>(); public static void printTime(String type, String method, long time) { System.out.println(method + " of " + type + " : " + time + "ms"); } public static void main(String[] args) throws IOException { String fileName = "src//temp//data.txt"; try(FileReader reader = new FileReader(fileName); BufferedReader br = new BufferedReader(reader) ) { String line; while((line = br.readLine()) != null) { S.add(line); } } catch (IOException e) { System.out.println("Illegal input file or its path."); } System.out.println("Read finish."); Iterator<String> it; String str; long beginTime = 0, endTime = 0; //Test List begin List<String> testList = new ArrayList<>(); it = S.iterator(); while(it.hasNext()) testList.add(it.next()); System.out.println("Copy finish."); beginTime = System.currentTimeMillis(); int Size = testList.size(); for(int i = 0; i < Size; i++) { str = testList.get(i); } endTime = System.currentTimeMillis(); printTime(" List", "Traversal with scripts", endTime - beginTime); beginTime = System.currentTimeMillis(); for(String s : testList) { str = s; } endTime = System.currentTimeMillis(); printTime("List", "Traversal with implicit iterator", endTime - beginTime); beginTime = System.currentTimeMillis(); it = testList.iterator(); while(it.hasNext()) { str = it.next(); } endTime = System.currentTimeMillis(); printTime("List", "Traversal with explicit iterator", endTime - beginTime); //Test List end } }
以上代码以ArrayList实现为例,分别测试三种遍历方式的运行时间,单位为ms。
为了尽量减少无关变量的影响,每个循环中都执行相同的赋值操作,同时均使用相同的Data.txt。
LinkedList实现类的代码与此类似,下不赘述。
3.实验结果
ArrayList:
Read finish.
Copy finish.
Traversal with scripts of List : 96ms
Traversal with implicit iterator of List : 104ms
Traversal with explicit iterator of List : 98ms
LinkedList:
Read finish.
Copy finish.
Traversal with scripts of List : 10001ms
Traversal with implicit iterator of List : 162ms
Traversal with explicit iterator of List : 151ms
这里由于LinkedList的传统遍历运行时间太长,我就给截断了。
可以看到,在两种实现类中,内置迭代器的运行速度都要快于显式迭代器。
而在ArrayList中,传统遍历速度又优于其他两种遍历方式。
4.ArrayList分析
private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private transient Object[] elementData; private int size; public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); }
上述为ArrayList的底层实现,可以看到其本质上就是一个数组。
当加入新元素后会产生溢出时,add方法会新建一个大小为原数组的大小的1.5倍的新数组。
所以get方法的实现就及其简单,直接用偏移寻址即可。
5.LinkedList分析
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } }
由于LinkedList的成员几乎完全由Node来实现,所以直接来分析Node这个类。
可以看到,正如在DS中学过的链表一样,Node的next和prev分别指向该节点的前驱和后继。
而get本质上是遍历一个链表来寻找符合的元素,因此LinkedList的get效率慢到无法忍受,也不足为奇了。
6.迭代器分析
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 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的测试中,能够达到与直接调用get相近的时间。
7.总结
基于上述分析,我们可以得到如下结论:
ArrayList基于动态数组的实现,它长于随机访问元素,但是在中间插入和移除元素时较慢。
LinkedList基于链表实现,在List中间进行插入和删除的代价较低,提供了优化的顺序访问。LinkedList在随机访问方面相对比较慢,但是它的特性集较ArrayList更大。
迭代器的时间开销与get相近,而且对程序员来说可控性更高,所以不失为一个好的选择。