浅谈ArrayList和 LinkedList区别

来源:互联网 发布:控制网络连接管理 编辑:程序博客网 时间:2024/06/06 09:59

ArrayList 和 LinkedList 都是List接口的实现类,都实现了List接口相关的方法.


但是两者实现的逻辑完全不同,ArrayList 是基于动态数组数据结构实现,LinkedList 是基于双向链表数据结构实现,接下来从这两种完全不同的结构比较 ArrayList 和 LinkedList 的性能和 适用场景。


一、简单分析两者数据结构和提供的相关方法

 1.ArrayList 是基于数据实现的,源码如下:

 /**     * Default initial capacity.     */    private static final int DEFAULT_CAPACITY = 10;
/**     * The array buffer into which the elements of the ArrayList are stored.     * The capacity of the ArrayList is the length of this array buffer. Any     * empty ArrayList with elementData == EMPTY_ELEMENTDATA will be expanded to     * DEFAULT_CAPACITY when the first element is added.     */    private transient Object[] elementData;
    /**     * Constructs an empty list with an initial capacity of ten.     */    public ArrayList() {        super();        this.elementData = EMPTY_ELEMENTDATA;    }


由于数组初始化时都必须指定数组大小, 所以ArrayList 初始化时默认大小为10 (对应源码中的 DEFAULT_CAPACITY =10),也可以手动指定 初始化大小,如 new ArrayList<>(20); 在知道数据大小的时候,建议指定大小.

建议的理由:大家都知道数组初始化时指定了大小,然后根据初始化大小在内存分配一段连续的空间,以后再也不能修改数组大小,ArrayList便是基于动态数据的实现(就是改变了引用变量的指向),比如不断向ArrayList 里面添加元素,当ArrayList 的size不够时变开始进行扩容,扩容肯定损耗性能,如果指定大小可避免.扩容源码如下:

    /**     * Increases the capacity to ensure that it can hold at least the     * number of elements specified by the minimum capacity argument.     *     * @param minCapacity the desired minimum capacity     */    private void grow(int minCapacity) {        // overflow-conscious code        int oldCapacity = elementData.length;        int newCapacity = oldCapacity + (oldCapacity >> 1);        if (newCapacity - minCapacity < 0)            newCapacity = minCapacity;        if (newCapacity - MAX_ARRAY_SIZE > 0)            newCapacity = hugeCapacity(minCapacity);        // minCapacity is usually close to size, so this is a win:        elementData = Arrays.copyOf(elementData, newCapacity);    }

从上面源码可以看出,每次扩容后长度为原来的1.5倍,就是每次增加50%(oldCapacity >>1), 即重新创建一个1.5倍原来数据长度的新数据,然后将老的数据内容拷贝到新的里面,所以如果一开始没有设置大小或者设置得过小,后期频繁的插入,会导致经常需要扩容,这对性能有一定的影响,还有一个经常也被人吐槽的就是,当Size很大的时候,再扩容一次,就会造成很多内存的浪费

例如:list 的size 已经到达1000万了,再插入一个数据时,发现不够了,便开始扩容,list 大小变为1500万,可是在这之后不插入数据或者很少的数据,这样就会造成几乎500万大小的内存浪费。

ArrayList 提供了一个方法trimToSize(),这个方法可以多余的内存去掉,如果在知道数据量很大的时候,可以用一下这个方法,源码如下:

    /**     * Trims the capacity of this <tt>ArrayList</tt> instance to be the     * list's current size.  An application can use this operation to minimize     * the storage of an <tt>ArrayList</tt> instance.     */    public void trimToSize() {        modCount++;        if (size < elementData.length) {            elementData = Arrays.copyOf(elementData, size);        }    }

接下来看一下ArrayList 的几个主要方法 ,indexOf,set,get add(E e),add(int index,E element),remove,根据时间复杂度来,indexOf,add(int index,E element),remove 归为一类,都是o(n),get,set add(E e) 归为一类,都是o(1),相关源码如下:

    /**这个一看就是o(n),遍历查询比较     * Returns the index of the first occurrence of the specified element     * in this list, or -1 if this list does not contain the element.     * More formally, returns the lowest index <tt>i</tt> such that     * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,     * or -1 if there is no such index.     */    public int indexOf(Object o) {        if (o == null) {            for (int i = 0; i < size; i++)                if (elementData[i]==null)                    return i;        } else {            for (int i = 0; i < size; i++)                if (o.equals(elementData[i]))                    return i;        }        return -1;    }
add(int index,E element) 在指定位置插入后,需要将后面的数据都后移一位,导致时间复杂度为0(n)
    /**     * Inserts the specified element at the specified position in this     * list. Shifts the element currently at that position (if any) and     * any subsequent elements to the right (adds one to their indices).     *     * @param index index at which the specified element is to be inserted     * @param element element to be inserted     * @throws IndexOutOfBoundsException {@inheritDoc}     */    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++;    }
remove 同理,需要将删除之后的数据前移一位

    /**     * Removes the element at the specified position in this list.     * Shifts any subsequent elements to the left (subtracts one from their     * indices).     *     * @param index the index of the element to be removed     * @return the element that was removed from the list     * @throws IndexOutOfBoundsException {@inheritDoc}     */    public E remove(int index) {        rangeCheck(index);        modCount++;        E oldValue = elementData(index);        int numMoved = size - index - 1;        if (numMoved > 0)            System.arraycopy(elementData, index+1, elementData, index,                             numMoved);        elementData[--size] = null; // clear to let GC do its work        return oldValue;    }
get() 和set() 方法都是 根据下标去取数,所以复杂度为O(1),而 add(E e) 是在数组最后插入,不需要移动,源码如下:

 public E set(int index, E e) {            rangeCheck(index);            checkForComodification();            E oldValue = ArrayList.this.elementData(offset + index);            ArrayList.this.elementData[offset + index] = e;            return oldValue;        }        public E get(int index) {            rangeCheck(index);            checkForComodification();            return ArrayList.this.elementData(offset + index);        }
    /**     * Appends the specified element to the end of this list.     *     * @param e element to be appended to this list     * @return <tt>true</tt> (as specified by {@link Collection#add})     */    public boolean add(E e) {        ensureCapacityInternal(size + 1);  // Increments modCount!!        elementData[size++] = e;        return true;    }


2. 接下来看LinkedList ,LinkedList 是一个基于链表的,每一个节点都存放本身的内容item,一个指向前节点的指针prev,指向后节点的指针next。

    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;        }    }
从这上面可以看出是一个双向的,每个节点都会保存前一个节点prev和后一个节点信息next,这样就会导致浪费空间,据说网页初始化页面刚开始用的是LinkedList存储数据,后来改为ArrayList,节省了2G内存,只是据说,据说,

但是,但是存在肯定有他的合理性,这里先不讨论,我们继续往下看LinkedList其他方法,LinkedList提供的方法还比较多,大体过一下源码:

 1.linkFirst 就是在开始节点插入一个节点,first节点就变为当前新接点,再判断当前list是否 为空,为空将last指向这个新节点,否则将原来的第一节点的prev指向现在的第一节点,新节点的prev 为null

    /**     * Links e as first element.     */    private void linkFirst(E e) {        final Node<E> f = first;        final Node<E> newNode = new Node<>(null, e, f);        first = newNode;        if (f == null)            last = newNode;        else            f.prev = newNode;        size++;        modCount++;    }
linkLast在最后添加一个节点,先将last 指向新节点,再先判断是否为空,如果为空,就把firs指向这个节点,否则就把原来的last的next指向新节点,把新节点作为新的last节点

    /**     * Links e as last element.     */    void linkLast(E e) {        final Node<E> l = last;        final Node<E> newNode = new Node<>(l, e, null);        last = newNode;        if (l == null)            first = newNode;        else            l.next = newNode;        size++;        modCount++;    }
indexOf 是通过循环遍历去比较,时间复杂度为 O(n),这个 和 ArrayList 的indexOf 大体是一样的,

    /**     * Returns the index of the first occurrence of the specified element     * in this list, or -1 if this list does not contain the element.     * More formally, returns the lowest index {@code i} such that     * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,     * or -1 if there is no such index.     *     * @param o element to search for     * @return the index of the first occurrence of the specified element in     *         this list, or -1 if this list does not contain the element     */    public int indexOf(Object o) {        int index = 0;        if (o == null) {            for (Node<E> x = first; x != null; x = x.next) {                if (x.item == null)                    return index;                index++;            }        } else {            for (Node<E> x = first; x != null; x = x.next) {                if (o.equals(x.item))                    return index;                index++;            }        }        return -1;    }
接下来这个方法比较核心了,在指定位置插入节点和删除节点,get(int index),这些方法都会调用node(int index),这个方法就是获取指定位置,这里获取定位是,还做了一点的优化,就是比较获取的位置index是在前半部分,还是在后半部分,前半部分就从头开始遍历,后半部分就从尾开始遍历,不可避免的是时间复杂度还是 O(n).

很多的观点说 频繁的插入删除,用Linklist比比ArrayList快,确实是这样吗?其实这种说法是完全不对的。例如频繁的插入: Linklist 需要重头遍历一遍定位,而ArrayList 需要将后面的前移,到底谁快还不一定呢,需要具体情况具体分析了。

    /**     * Returns the (non-null) Node at the specified element index.     */    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 还提供了很多 类似队列和栈的操作方法,如果需求实现需要类似 栈或者队列时,这个是LinkedList比ArrayList的巨大优势,源码比较多,贴一部分如下:

    /**     * Retrieves, but does not remove, the head (first element) of this list.     *     * @return the head of this list, or {@code null} if this list is empty     * @since 1.5     */    public E peek() {        final Node<E> f = first;        return (f == null) ? null : f.item;    }    /**     * Retrieves, but does not remove, the head (first element) of this list.     *     * @return the head of this list     * @throws NoSuchElementException if this list is empty     * @since 1.5     */    public E element() {        return getFirst();    }    /**     * Retrieves and removes the head (first element) of this list.     *     * @return the head of this list, or {@code null} if this list is empty     * @since 1.5     */    public E poll() {        final Node<E> f = first;        return (f == null) ? null : unlinkFirst(f);    }    /**     * Retrieves and removes the head (first element) of this list.     *     * @return the head of this list     * @throws NoSuchElementException if this list is empty     * @since 1.5     */    public E remove() {        return removeFirst();    }


二、简单分析ArrayList 和LinkedList 的性能

上面已经简单介绍了ArrayList 和LinkedList 的结构和提供的方法,接下来看一下两者的使用性能.

1. 首先比较一下随机查询,测试代码如下:

public class Test1 {static List<Integer> arraylist=new ArrayList<Integer>();      static List<Integer> linkedlist=new LinkedList<Integer>();    static Integer num= 100000;      public static void main(String[] args) {          for(int i=0;i<num;i++){          arraylist.add(i);          linkedlist.add(i);          }          System.out.println("arrayList 消耗时间:"+getData(arraylist));          System.out.println("linkedList 消耗时间:"+getData(linkedlist));             }          public static long getData(List<Integer> list) {        long time=System.currentTimeMillis();        int temp;        for(int i=0;i<list.size();i++){           temp = list.get(i);        }          return System.currentTimeMillis()-time;      }}
测试结果如下:

从结果可以看出,如果 是随机查询比较多的,绝对不建议用LinkedList ,LinkedList 是从头或者从尾部一个一个的去遍历比较,然后获取,这样肯定慢.

2.接下来比较插入,代码如下:

public class Test1 {static List<Integer> arraylist = new ArrayList<Integer>();static List<Integer> linkedlist = new LinkedList<Integer>();static Integer num = 100000; //list的大小static Integer position = 1; //插入的位置public static void main(String[] args) {for (int i = 0; i < num; i++) {arraylist.add(i);linkedlist.add(i);}System.out.println("array insert time:" + insertData(arraylist));System.out.println("linked insert time:" + insertData(linkedlist));}public static long insertData(List<Integer> list) {long time = System.currentTimeMillis();for (int i = 100; i < 100000; i++) {list.add(position, i);}return System.currentTimeMillis() - time;}}
当positon =1时,结果 ArrayList 消耗的时间 > LinkedList 消耗的时间:


当position =10000时,结果 ArrayList 消耗的时间 > LinkedList 消耗的时间:


当position= 20000,结果 ArrayList 消耗的时间 < LinkedList 消耗的时间:


当postion= 30000时,结果 ArrayList 消耗的时间 < LinkedList 消耗的时间:


........

当postion= 80000时,结果 ArrayList 消耗的时间 < LinkedList 消耗的时间:



当postion= 100000时,结果 ArrayList 消耗的时间 < LinkedList 消耗的时间:



结论:当list 数据量比较大时,测试用的10万数据量,当在集合的前部 20%左右插入数据(具体还是要看数据量,如果数据量100万估计就10%,意思就是:数据量越大,比例越小),LinkedList 比ArrayList 快(想想也知道,ArrayList 需要拷贝的比较多,LinkedList 遍历数据比较少),当在集合中后部插入时,ArrayList 性能更好.当数量比较小时比如就几百几十的,那就没有必要比较了。


3.比较一下remove 的性能比较:

public class Test1 {static List<Integer> arraylist = new ArrayList<Integer>();static List<Integer> linkedlist = new LinkedList<Integer>();static Integer num = 100000; //list的大小static int position = 1; //插入的位置public static void main(String[] args) {for (int i = 0; i < num; i++) {arraylist.add(i);linkedlist.add(i);}System.out.println("array insert time:" + removeData(arraylist));System.out.println("linked insert time:" + removeData(linkedlist));}public static long removeData(List<Integer> list) {long time = System.currentTimeMillis();for (int i = 100; i < 10000; i++) {list.remove(position);}return System.currentTimeMillis() - time;}}

当postion=1时,结果 ArrayList 消耗的时间 > LinkedList 消耗的时间:



当postion=10000时,结果 ArrayList 消耗的时间 > LinkedList 消耗的时间:



当postion=20000时,结果 ArrayList 消耗的时间 < LinkedList 消耗的时间:



当postion=30000时,结果 ArrayList 消耗的时间 < LinkedList 消耗的时间:



当postion=90000时,结果 ArrayList 消耗的时间 < LinkedList 消耗的时间:



结论和插入的类似,即当在集合的前部 20%左右插入数据(具体还是要看数据量),LinkedList 比ArrayList 快(想想也知道,ArrayList 需要拷贝的比较多,LinkedList 遍历数据比较少),当在集合中后部插入时,ArrayList 性能更好。

此外,如果需要在头部不断的插入数据,LinkedList 的优势是巨大的。

如果需要类似队列的功能是,LinkedList也比ArrayList 更好,所以具体使用哪个,还是看具体的应用场景.