zoukankan      html  css  js  c++  java
  • 说说JDK中的List-ArrayList、Vector、LinkedList

    为方便开发人员,JDK提供了一套主要数据结构的实现,比如List、Map等。今儿说说List接口。

    List接口的一些列实现中,最常用最重要的就是这三个:ArrayList、Vector、LinkedList。

    JDK中这三个类的定义:

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

    从这三个类定义就可以看出一些信息:

    • 三个都直接实现了AbstractList这个抽象类
    • ArrayList和Vector都实现了RandomAccess接口,而LinkedList没有,这是什么意思呢?在JDK中,RandomAccess接口是一个空接口,所以它没有实际意义,就是一个标记,标记这个类支持快速随机访问,所以,arrayList和vector是支持随机访问的,但是LinkedList不支持
    • serializbale接口表名,他们都支持序列化

    下面详细说说这三个List实现。

    这三个里面,ArrayList和Vector使用了数组的实现,相当于封装了对数组的操作。这也正是他们能够支持快速随机访问的原因,多说一句,JDK中所有基于数组实现的数据结构都能够支持快速随机访问。

    ArrayList和Vector的实现上几乎都使用了相同的算法,他们的主要区别就是ArrayList没有对任何一个方法做同步,所以不是线程安全的;而Vector中大部分方法都做了线程同步,是线程安全的。

    LinkedList使用的是双向循环链表的数据结构。由于是基于链表的,所以是没法实现随机访问的,只能顺序访问,这也正式它没有实现RandomAccess接口的原因。

    正式由于ArrayList、Vector和LinkedList所采用的数据结构不同,注定他们适用的是完全不同的场景。

    通过阅读这几个类的源码,我们可以看到他们实现的不同。ArrayList和Vector基本一样,我们就拿ArrayList和LinkedList做对比。

    在末尾增加一个元素

    ArrayList中的add方法实现如下:

    1     public boolean add(E e) {
    2         ensureCapacityInternal(size + 1);  // Increments modCount!!
    3         elementData[size++] = e;
    4         return true;
    5     }

    这个方法做两件事情,首先确保数组空间足够大,然后在数组末尾增加元素并且通过后++使得完成size+1。

    从这个代码可以看出,如果数组空间足够大,那么只是数组的add操作就是O(1)的性能,非常高效。

    在看看ensureCapacityInternal这个方法的实现:

     1     private void ensureCapacityInternal(int minCapacity) {
     2         modCount++;
     3         // overflow-conscious code
     4         if (minCapacity - elementData.length > 0)
     5             grow(minCapacity);
     6     }
     7 
     8     private void grow(int minCapacity) {
     9         // overflow-conscious code
    10         int oldCapacity = elementData.length;
    11         int newCapacity = oldCapacity + (oldCapacity >> 1);
    12         if (newCapacity - minCapacity < 0)
    13             newCapacity = minCapacity;
    14         if (newCapacity - MAX_ARRAY_SIZE > 0)
    15             newCapacity = hugeCapacity(minCapacity);
    16         // minCapacity is usually close to size, so this is a win:
    17         elementData = Arrays.copyOf(elementData, newCapacity);
    18     }

    可以看出,如果数组空间不够,那么这个方法就会做数组扩容和数组复制操作,看第11行,JDK利用移位运算符进行扩容计算,>>1右移一位表示除2,所以newCapacity就是扩容为原来的1.5倍。

    PS:这里的代码都是JDK1.7中的实现,JDK1.7对1.6的很多代码做了优化,比如上面这段扩容代码,在JDK1.6中第11行是直接除2,显然,移位运算要更高效。

    在看看LinkedList中的add方法:

     1     public boolean add(E e) {
     2         linkLast(e);
     3         return true;
     4     }
     5 
     6     void linkLast(E e) {
     7         final Node<E> l = last;
     8         final Node<E> newNode = new Node<>(l, e, null);
     9         last = newNode;
    10         if (l == null)
    11             first = newNode;
    12         else
    13             l.next = newNode;
    14         size++;
    15         modCount++;
    16     }
    1         Node(Node<E> prev, E element, Node<E> next) {
    2             this.item = element;
    3             this.next = next;
    4             this.prev = prev;
    5         }

    从这段add代码可以看出,LinkedList由于使用了链表,所以不需要进行扩容,直接把元素加到链表最后,把新元素的前驱指向之前的last元素,并把last元素指向新元素就ok。这也是一个O(1)的性能。

    测试一下:

     1     public static void main(String[] args) {
     2         // TODO Auto-generated method stub
     3         long begin = System.currentTimeMillis();
     4         
     5 //        List<Object> list = new ArrayList<Object>();
     6         List<Object> list = new LinkedList<Object>();
     7         Object obj = new Object();
     8         for(int i=0; i<50000; i++){
     9             list.add(obj);
    10         }
    11         
    12         long end = System.currentTimeMillis();
    13         long time = end - begin;
    14         System.out.println(time+"");
    15 
    16     }

    分别对ArrayList和LinkedList做末尾add操作,循环50000次,ArrayList耗时6ms,而LinkedList耗时8ms,这是由于LinkedList在add时候需要更多的对象创建和赋值操作。

    在任意位置插入元素

    ArrayList中的实现如下:

    1     public void add(int index, E element) {
    2         rangeCheckForAdd(index);
    3 
    4         ensureCapacityInternal(size + 1);  // Increments modCount!!
    5         System.arraycopy(elementData, index, elementData, index + 1,
    6                          size - index);
    7         elementData[index] = element;
    8         size++;
    9     }

    这段代码,首先先检查数组容量,容量不够先扩容,然后把index之后的数组往后挪一个,最后在index位置放上新元素。由于数组是一块连续内存空间,所以在任意位置插入,都会导致这个其后数组后挪一位的情况,需要做一次数组复制操作,很明显,如果有大量的随机插入,那么这个数组复制操作开销会很大,而且插入的越靠前,数组复制开销越大。

    LinkedList中的实现:

     1     public void add(int index, E element) {
     2         checkPositionIndex(index);
     3 
     4         if (index == size)
     5             linkLast(element);
     6         else
     7             linkBefore(element, node(index));
     8     }
     9 
    10     void linkBefore(E e, Node<E> succ) {
    11         // assert succ != null;
    12         final Node<E> pred = succ.prev;
    13         final Node<E> newNode = new Node<>(pred, e, succ);
    14         succ.prev = newNode;
    15         if (pred == null)
    16             first = newNode;
    17         else
    18             pred.next = newNode;
    19         size++;
    20         modCount++;
    21     }

    这段代码,取到原先index处节点的前驱,变成新节点的前驱 ,同时把原先index变成新节点的后驱,这样就完成了新节点的插入。这个就是链表的优势,不存在数据复制操作,性能和在最后插入是一样的。

    测试一种极端情况,每次都在最前端插入元素:

     1     public static void main(String[] args) {
     2         // TODO Auto-generated method stub
     3         long begin = System.currentTimeMillis();
     4         
     5 //        List<Object> list = new ArrayList<Object>();
     6         List<Object> list = new LinkedList<Object>();
     7         Object obj = new Object();
     8         for(int i=0; i<50000; i++){
     9             list.add(0,obj);
    10         }
    11         
    12         long end = System.currentTimeMillis();
    13         long time = end - begin;
    14         System.out.println(time+"");
    15 
    16     }

    测试结果是:ArrayList耗时1400ms,而LinkedList只耗时12ms。可以看出,在随机插入的时候,两者的性能差异就很明显了。

    小结一下,从上面的源码剖析和测试结果可以看出这三种List实现的一些典型适用场景,如果经常对数组做随机插入操作,特别是插入的比较靠前,那么LinkedList的性能优势就非常明显,而如果都只是末尾插入,则ArrayList更占据优势,如果需要线程安全,则非Vector莫属。

  • 相关阅读:
    控制结构(Scala)
    《基于Spark的大数据访存行为跨层分析工具》学习笔记
    函数式对象(Scala)
    心脏病预测(SVM模型)
    类、对象、基础类型、操作(Scala)
    ElementUI对话框(dialog)提取为子组件
    ElementUI+命名视图实现复杂顶部和左侧导航栏
    ElementUI 复杂顶部和左侧导航栏实现
    Vue页面手动刷新,导航栏激活项还原到初始状态问题解决方案
    elementUI动态数据表格(带分页)
  • 原文地址:https://www.cnblogs.com/lingiu/p/3252135.html
Copyright © 2011-2022 走看看