有关不同实现类的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相近,而且对程序员来说可控性更高,所以不失为一个好的选择。

posted @ 2020-03-05 00:28  WDZRMPCBIT  阅读(435)  评论(0编辑  收藏  举报