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的选择的主要内容,如果未能解决你的问题,请参考以下文章
Java中arraylist和linkedlist源代码分析与性能比較