ArrayList与LinkedList的选择

Posted FreeFly辉

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArrayList与LinkedList的选择相关的知识,希望对你有一定的参考价值。

一、区别

以下源码均来自 jdk 1.8

1、arraylist源码分析

arraylist构造方法
如下:
public ArrayList(int initialCapacity) {  //指定初始化大小
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else { //很明显数组大小不可能小于 0 
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

public ArrayList() { //初始化一个空数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) { //初始化一个默认数据的数组
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

以上可以看出,创建一个ArrayList对象时只是初始化一个空数组,或指定大小的数组。
接下来看add()方法:

public boolean add(E e) { //添加一个泛型数据(其实泛型可以说是语法糖,
//当你将class文件反编译回来会发现就是用的object类接受的,只是编译时根据指定的泛型进行了强转)
    ensureCapacityInternal(size + 1);  //根据当前数值加一来判定是否要扩容
    elementData[size++] = e; 
    return true;
}

其中ensureCapacityInternal,只是简单的判断是否要扩容,其扩容时是加多少数据,就扩容多大(相对于add()和addAll()),因为不会预留空间,每次添加都会扩容。扩容方法中用的时是Arrays.copyOf(elementData, newCapacity); 方法。最终就是

System.arraycopy(original, 0, copy, 0,
                 Math.min(original.length, newLength));

相信大家对这个复制方法不陌生。值得一提的是,arraylist每次扩容只扩容添加了多少个元素,就扩容多大(这里对比addAll()),所以一旦数组开始扩容,那么每次添加数据都会涉及扩容。

get方法
public E get(int index) {
    rangeCheck(index); //检查是否越界
    return elementData(index);//获取值方法
}		
E elementData(int index) {
    return (E) elementData[index]; //elementData是存数据的数组,简单的根据数组下标获取数据
}
遍历就看一下forEach
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) { //关键部分
        action.accept(elementData[i]); //根据下标调用传入的函数式接口
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

可以看出其实就算是forEach对与arraylist也只是普通的for循环。所以不要对arraylist纠结普通for循环和foreach的遍历速度,只是代码结构看着不一样。

至于remove和indexOf()就不粘贴代码了,可自行查看(用idea无需下载源码包)
remove就是将对应下标 置空,并未对数组进行缩容。 indexOf就是遍历数组然后调用equals方法比较。

1、linkedlist源码分析

linkedlist构造方法
public LinkedList() { //没啥特殊的,相对arraylist少了指定初始化大小的构造方法,因为链表本就没大小
}
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
linkedlist的add方法构造方法

在此先看一下linkedlist中的一个内部类 Node:

private static class Node<E> {
    E item; //存放add进来的数据对象
    Node<E> next; //指向下一个数据向,如果是最后添加的,则此next为null
    Node<E> prev;//指向前一个数据向,如果是第一个添加的,则此prev为null

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next; //与prev共同作用形成衔接成一条双向链
        this.prev = prev;
    }
}
public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
    final Node<E> l = last; //这里的last是linkedlist对象的属性,用于存放最后一个添加的对象
    final Node<E> newNode = new Node<>(l, e, null);//构建node对象,存放上一个值引用
    last = newNode; //last重新赋值为当前最后添加的对象
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++; //每次添加,将size数值加一
    modCount++;
}

可以看出在尾部追加数据是比arraylist快速的,因为不涉及扩容

get方法
public E get(int index) {
    checkElementIndex(index); //下标越界检查,没啥特殊的,只是用了与size判断比较
    return node(index).item; //根据下标找出来的 Node对象 .item属性返回,因为item属性本就存的数据项
}
Node<E> node(int index) {
    if (index < (size >> 1)) { //如果下标在 size大小前半部分,就遍历前半部分(右移 1相当于除以2)
        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;
    }
}

可以看出,随机查找的速度远比不上arraylist,因为数组根据下标查找,无需遍历

遍历方法(注意 linkedlist遍历和arraylist遍历区别很大)
default void forEach(Consumer<? super T> action) { //此方法继承自Iterable接口
    Objects.requireNonNull(action);
    for (T t : this) { //增强for循环,(语法糖,编译成class文件时会自动转成 Iterator形式)
        action.accept(t);
    }
}
形式如下
LinkedList<Object> objects = new LinkedList<>();
Iterator<Object> iterator = objects.iterator();
while (iterator.hasNext()){
    iterator.next();
}

iterator代码自行查看,因为牵扯的类较多,大致意思就是用一个int型的cursor记录当前位置,每next()一次,cursor++,用一个对象属性Node类型 lastReturned记录下一个要返回的值,一旦调用的next(),就返回该属性的item属性,以此来遍历,所以iterator是不可逆的,也是无法根据索引来移除的(因此iterator接口定义了remove方法)。

remove和indexOf方法不在粘贴代码
remove(),就是从0开始遍历链表累加,直至累加值等于传入的索引,将该node前一节点的next指向当前node的next,当前node的next指向当前node的prev; indexOf()一样的遍历,equals。

二、总结

1、添加:明显在尾部和头部追加 linkedlist比arraylist要快许多。(因为arraylist多了扩容,
移位复制操作。)。
指定位置添加:速度不好说,有人说linkedlist快,因为不用切断数组,复制数组操作。事实上linkedlist多了个循环操作。所以都有额外操作,看的就是for循环和数组赋值哪个快了。(个人觉得应该还是linkedlist快些)

2、查找:查找就不用说了,没啥可比性。arraylist完胜linkedlist,因为arraylist根据下标直接取值,linkedlist需要遍历。

3、删除:同插入式添加,一个涉及到复制,一个涉及到遍历。说不定谁快。

4、遍历:arraylist无论式普通for还是foreach都差不多,因为都是根据大小遍历下标取值。
linkedlist打死也别用普通for循环,因为你用普通for循环,然后根据下标去get(),在其get()方法中又要去for一次。这就是双重循环了。如果都是foreach,速度差别不大。

使用场景

如果提前知道大致数据大小,一定选择arraylist,并在构造方法中指定大小。 如果不知道数据大小,且查找比例占操作少写,那就是linkedlist了。
举例

1、前台请求过来的分页查询涉及到VO转换,就用arraylist,因为分页查询可以知道要返回的集合大小,所以就用arraylist,构造方法中指定大小为分页大小即可(只是尾部添加操作,相对linkedlist只能说不慢,而是提高了空间利用率,因为无需头引用,尾引用)。

2、前台根据条件填充下拉框数据请求,一般下拉框都不进行分页(除非数据量很大),此时用linkedlist较好,因为你不知道要追加多少条数据。考录到添加速度,linkedlist好些。因为arraylist是单次扩容,并不会预留空间,因此每次添加都要扩容。

3、如果是自己解析原生sql,非分页查询就用linkedlist(事实上都框架给你封装好了,这里只是为了举例)
事实上普通web开发涉及不到啥linkedlist和arraylist的特点,因此很多人就全按arraylist的来。

以上是关于ArrayList与LinkedList的选择的主要内容,如果未能解决你的问题,请参考以下文章

Linkedlist与arraylist的比较[重复]

Java中arraylist和linkedlist源代码分析与性能比較

从内存分配的角度来看 ArrayList 与 LinkedList

ArrayList与LinkedList的区别 ?

ArrayList与LinkedList的区别 ?

源码面经Java源码系列-ArrayList与LinkedList