在面试的时候经常被问到LinkedList和ArrayList有什么区别?
正常的回答是:ArrayList是基于数组实现的, LinkedList是基于链表实现的。
好一点的如下面的回答:
- LinkedList 在插入和删除数据时效率更高;
- ArrayList 在查找某个 index 的数据时效率更高;
- LinkedList 实现了 List 和 Deque 接口,一般称为双向链表;
- ArrayList 实现了List 接口,称为动态数组;
- LinkedList 比 ArrayList 需要更多的内存;
接着面试官问:它们都在什么场景下使用吗?
答:因为ArrayList是基于数组实现的,所以在遍历的时候,ArrayList的效率是要比LinkedList高的, LinkedList是基于链表实现的,所以在进行新增/删除元素的时候, LinkedList的效率是要比ArrayList 高的。
是这样么,没错,我以前也是这么说的,那时候天真的以为我回答得非常的好,好尴尬哦
但是LinkedList在插入和删除数据时效率更高,真的是这样吗?聪明的孩子都知道这么说就不是了。
当然不是,显然是在一定的条件下才成立,为了验证这一点,接下来我们根据测试以及源码分析一下这个问题。
插入元素谁快谁慢
ArrayList源码:
ArrayList 新增元素有两种情况,一种是直接将元素添加到数组末尾,一种是将元素插入到指定位置。
添加到数组末尾的源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;// 向末尾追加一个元素,显然时间复杂度为O(1)
return true;
}
插入到指定位置的源码:
public void add(int index, E element) {
rangeCheckForAdd(index); //1 先检查插入的位置是否在合理的范围之内
ensureCapacityInternal(size + 1); // Increments modCount!!//2 然后判断是否需要扩容
System.arraycopy(elementData, index, elementData, index + 1,
size - index);//3 再把该位置以后的元素复制到新添加元素的位置之后
elementData[index] = element;//4 最后通过索引将元素添加到指定的位置
size++;
}
LinkedList源码:
LinkedList新增元素也有两种情况,一种是直接将元素添加到队尾,一种是将元素插入到指定位置。
添加到队尾的源码:
public boolean add(E e) {
linkLast(e); // 向末尾追加元素
return true;
}
void linkLast(E e) {
final Node<E> l = last; //先将队尾的节点 last 存放到临时变量 l 中
final Node<E> newNode = new Node<>(l, e, null);
last = newNode; //然后生成新的 Node 节点,并赋给 last
if (l == null)//如果 l 为 null,说明是第一次添加
first = newNode; //所以 first 为新的节点
else
l.next = newNode; //否则将新的节点赋给之前last的next。
size++;
modCount++;
}
添加到指定位置的源码:
public void add(int index, E element) {
checkPositionIndex(index);//先检查插入的位置是否在合理的范围之内
//判断插入的位置是否是队尾,如果是,添加到队尾;否则执行linkBefore()方法。
if (index == size)
linkLast(element);
else
//linkBefore方法里面还有一个node方法:
linkBefore(element, node(index));
}
/**
* Returns the (non-null) Node at the specified element index.
*/
//查找指定位置上的元素,这一步是需要遍历LinkedList
Node<E> node(int index) {
// assert isElementIndex(index);
// 如果index小于size/2,就从first开始遍历,即从头往后找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {// 如果index大于等于size/2,就从last开始遍历,即从尾向前找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
//从上面可以看出如果插入的位置越靠近LinkedList的中间位置,遍历所花费的时间就越多。
}
/**
* Inserts element e before non-null Node succ.
*/
//找到指定位置上的元素succ之后,就开始执行linkBefore()方法了
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev; //先将succ的前一个节点prev存放到临时变量pred中
final Node<E> newNode = new Node<>(pred, e, succ);//生成新的Node节点(newNode)
//将succ的前一个节点变更为newNode
succ.prev = newNode;
//如果pred为null,说明插入的是队头,所以first为新节点,否则将pred的后一个节点变更为 newNode
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
从头部开始插入元素:
public static void main(String[] args) throws ParseException {
List<Integer> list = new ArrayList<Integer>();
LinkedList<Integer> linkedList = new LinkedList<Integer>();
long start = System.currentTimeMillis(); // 开始时间
for (int i = 0; i < 50000; ++i) {
list.add(0,1);
}
long end = System.currentTimeMillis(); // 结束时间
System.out.println("ArrayList添加数据时间: "+(end-start)+"ms");
long start2 = System.currentTimeMillis(); // 开始时间
for (int i = 0; i < 50000; ++i) {
linkedList.add(0, i);
}
long end2 = System.currentTimeMillis(); // 结束时间
System.out.println("LinkedList添加数据时间: "+(end2-start2)+"ms");
}
ArrayList添加数据时间: 329ms
LinkedList添加数据时间: 5ms
从时间消耗看到ArrayList比LinkedList要花费的时间多,从源码中ArrayList要对头部以后的元素进行复制的操作。
从中间开始插入元素:
public static void main(String[] args) throws ParseException {
List<Integer> list = new ArrayList<Integer>();
LinkedList<Integer> linkedList = new LinkedList<Integer>();
long start = System.currentTimeMillis(); // 开始时间
for (int i = 0; i < 100000; ++i) {
list.add(list.size()/5,1);
}
long end = System.currentTimeMillis(); // 结束时间
System.out.println("ArrayList添加数据时间: "+(end-start)+"ms");
long start2 = System.currentTimeMillis(); // 开始时间
for (int i = 0; i < 100000; ++i) {
linkedList.add(linkedList.size()/5, i);
}
long end2 = System.currentTimeMillis(); // 结束时间
System.out.println("LinkedList添加数据时间: "+(end2-start2)+"ms");
}
ArrayList添加数据时间: 962ms
LinkedList添加数据时间: 1622ms
从时间消耗看到ArrayList比LinkedList要花费的时间少,从源码中LinkedList需要进行遍历的操作。
从尾部开始插入元素:
public static void main(String[] args) throws ParseException {
List<Integer> list = new ArrayList<Integer>();
LinkedList<Integer> linkedList = new LinkedList<Integer>();
long start = System.currentTimeMillis(); // 开始时间
for (int i = 0; i < 1000000; ++i) {
list.add(i);
}
long end = System.currentTimeMillis(); // 结束时间
System.out.println("ArrayList添加数据时间: "+(end-start)+"ms");
long start2 = System.currentTimeMillis(); // 开始时间
for (int i = 0; i < 1000000; ++i) {
linkedList.add(i);
}
long end2 = System.currentTimeMillis(); // 结束时间
System.out.println("LinkedList添加数据时间: "+(end2-start2)+"ms");
}
ArrayList添加数据时间: 17ms
LinkedList添加数据时间: 22ms
从时间消耗看到ArrayList比LinkedList要花费的时间少,数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列,ArrayList添加元素需要进行数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。
删除元素谁快谁慢
ArrayList源码:
ArrayList删除元素有两种方式:
remove(Object o):根据对象删除元素。
remove(int index):根据索引删除元素。
public E remove(int index) {
// 首先检查下标是否越界,如果下标越界,将抛出异常
rangeCheck(index);
// 当前列表的修改次数加1
// 这个参数是用于迭代器fail-fast机制的
// 当在遍历列表时,如果对列表进行删除操作时,将会抛出异常
modCount++;]
// 根据下标获取对应的值
E oldValue = elementData(index);
// 计算要删除位置的元素后还有几个元素,用于后面的操作
int numMoved = size - index - 1;
// 如果要删除的元素不是最后一个元素
if (numMoved > 0)
// 这是jdk的一个本地方法,用于将一个数组从指定位置复制到目标数组的指定位置
// 其中numMoved就是要复制的个数,也就是被删除元素后面的元素个数
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 把列表大小减1,并把最后一个元素置空,让垃圾收集器把它回收
// 这里如果不置空,它将会保存着一个引用,那么垃圾收集器将无法回收它,可能会造成内存泄漏
elementData[--size] = null; // clear to let GC do its work
//将被删除的值返回
return oldValue;
}
public boolean remove(Object o) {
// 如果对象为空(ArrayList允许元素为空)
if (o == null) {
//循环查找ArrayList中是否有null元素,如果有则返回true
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {//如果指定元素不为空,则循环
for (int index = 0; index < size; index++)
//将指定元素与ArrayList中的元素进行equals比较
if (o.equals(elementData[index])) {
//如果相等,则调用fastRemove移除并返回true;
fastRemove(index);
return true;
}
}
return false;
}
这里重点是fastRemove这个方法,这个是ArrayList的私有移除方法,方法源码:
private void fastRemove(int index) {
modCount++;
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
}
fastRemove()方法跳过边界检查并且不会返回值,因为这里的index是根据元素得出的,所以不可能越界,因为上一级返回了是否成功的标志,这里也不需要在返回值操作。
remove方法可以指定索引移除值,也可以按指定对象进行移除。移除某个值后,其后所有的值都会往前移动一位。
LinkedList源码:
LinkedList删除元素有3种方式
remove():无参方法会直接删除链表的第一个节点
remove(Object o):删除传入的对象
remove(int index):删除第index个链表节点
remove()源码:
//删除第一个节点
public E remove() {
return removeFirst();
}
public E removeFirst() {
//first指向链表中的第一个结点
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
//主要方法是这个
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//获取当前节点的元素
final E element = f.item;
//将next引用指向下一个节点
final Node<E> next = f.next;
//将删除的元素置空,垃圾回收器会自动清除内容
f.item = null;
//将删除的内容
f.next = null; // help GC
//因为删除当前节点元素(第一个节点),所以此处将下一个节点赋值给第一个
first = next;
//判断如果没有下一个节点,那么最后一个节点就不存在null
if (next == null)
last = null;
else
//此处的next.prev即第一个结点,要删除,所以赋值为null
next.prev = null;
//链表大小减一
size--;
//修改记录加一
modCount++;
//将要删除的值返回
return element;
}
删除第一个节点就不需要遍历了,只需要把第二个节点更新为第一个节点即可
而c removeLast:删除最后一个节点,删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点就行。
remove(Object o)源码:
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
//当有节点的值为null时
if (x.item == null) {
//调用unlink方法删除节点
unlink(x);
return true;
}
}
} else {//如果传入对象不为null同样遍历全表
for (Node<E> x = first; x != null; x = x.next) {
//调用对象的equals方法判断有没有与传入对象相等的节点
if (o.equals(x.item)) {
//如果有调用unlink方法删除节点
unlink(x);
return true;
}
}
}
//其他情况返回false
return false;
}
remove(int index)源码:
public E remove(int index) {
//判断传入第index个节点是否存在
checkElementIndex(index);
//如果存在调用node(index) 方法(前后半段遍历,和新增元素操作一样)找到节点Node,然后调用unlink(Node)解除节点的前后引用,同时更新前节点的后引用和后节点的前引用:
return unlink(node(index));
}
上面调用unlink()方法的 步骤解析:
E unlink(Node<E> x) {
// assert x != null;
//创建三个临时变量存储传入节点的信息
//element 存储节点的对应对象
//node<E> next 存储下一个节点的地址
//node<E> prev 存储上一个节点的地址
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//如果上一个节点为null(prev=null)说明传入节点为头节点
if (prev == null) {
//此时将下一个节点的地址赋值给first
first = next;
} else {
//其他则将上一个节点的next指向传入节点的下一个节点
prev.next = next;
//传入节点的prev赋值为null
x.prev = null;
}
//如果传入节点之后没有节点(next=null)说明传入节点为尾节点
if (next == null) {
//此时上一个节点赋值给尾节点
last = prev;
} else {
//其他则将上一个节点的地址赋值给下一个节点的prev
next.prev = prev;
// 将传入节点的next赋值为null
x.next = null;
}
//最后将传入节点的值赋值为null
x.item = null;
//链表中的节点个数减1
size--;
//修改记录加一
modCount++;
//将要删除的值返回
return element;
}
LinkedList在删除比较靠前和比较靠后的元素时非常高效,但如果删除的是中间位置的元素,效率就比较低。
从头部开始删除元素:
public static void main(String[] args) throws ParseException {
LinkedList<String> linkedList = new LinkedList<String>();
ArrayList<String> arrayList = new ArrayList<String>();
for (int i=0;i<50000;i++) {
arrayList.add(i + "最帅的人");
linkedList.add(i + "最帅的人");
}
long timeStart = System.currentTimeMillis();
for (int i=0;i<50000;i++) {
linkedList.removeFirst();
}
long timeEnd = System.currentTimeMillis();
System.out.println("LinkedList删除花费的时间 " + (timeEnd - timeStart));
timeStart = System.currentTimeMillis();
for (int i=0;i<50000;i++) {
arrayList.remove(0);
}
timeEnd = System.currentTimeMillis();
System.out.println("ArrayList删除花费的时间 " + (timeEnd - timeStart));
}
LinkedList 从集合头部位置删除元素花费的时间 4
ArrayList 从集合头部位置删除元素花费的时间 430
从集合头部删除元素时,ArrayList花费的时间比LinkedList要多很多。
从中间开始删除元素:
public static void main(String[] args) throws ParseException {
LinkedList<String> linkedList = new LinkedList<String>();
ArrayList<String> arrayList = new ArrayList<String>();
for (int i=0;i<50000;i++) {
arrayList.add(i + "最帅的人");
linkedList.add(i + "最帅的人");
}
long timeStart = System.currentTimeMillis();
for (int i=0;i<50000;i++) {
linkedList.remove(linkedList.size()/2);
}
long timeEnd = System.currentTimeMillis();
System.out.println("LinkedList删除花费的时间 " + (timeEnd - timeStart));
timeStart = System.currentTimeMillis();
for (int i=0;i<50000;i++) {
arrayList.remove(arrayList.size()/2);
}
timeEnd = System.currentTimeMillis();
System.out.println("ArrayList删除花费的时间 " + (timeEnd - timeStart));
}
LinkedList删除花费的时间 1623
ArrayList删除花费的时间 130
从集合中间位置删除元素时,ArrayList花费的时间要比LinkedList少很多。
从尾部开始删除元素:
public static void main(String[] args) throws ParseException {
LinkedList<String> linkedList = new LinkedList<String>();
ArrayList<String> arrayList = new ArrayList<String>();
for (int i=0;i<50000;i++) {
arrayList.add(i + "最帅的人");
linkedList.add(i + "最帅的人");
}
long timeStart = System.currentTimeMillis();
for (int i=0;i<50000;i++) {
linkedList.removeLast();
}
long timeEnd = System.currentTimeMillis();
System.out.println("LinkedList删除花费的时间 " + (timeEnd - timeStart));
timeStart = System.currentTimeMillis();
for (int i=0;i<50000;i++) {
arrayList.remove(arrayList.size()-1);
}
timeEnd = System.currentTimeMillis();
System.out.println("ArrayList删除花费的时间 " + (timeEnd - timeStart));
}
LinkedList删除花费的时间 7
ArrayList删除花费的时间 3
从集合尾部删除元素时,ArrayList花费的时间要比LinkedList少一点。
总结
在集合里面插入元素速度比对结果:
- 头部插入:LinkedList 快
- 中间、尾部插入:ArrayList 快
- 头部删除:LinkedList 快
- 中间、尾部插入:ArrayList快
数据量大的话使用ArrayList,查询速度快,而且插入和删除效率也比较高,数据量小主要进行插入、删除的建议使用LinkedList。
因此,面试的时候如果面试官问到可以把上面所说的讲一遍,基本没有多大的问题了。
本文详细分析了LinkedList和ArrayList在插入、删除元素时的效率差异,揭示了头部、中部和尾部操作的性能对比,并根据数据量大小和操作类型给出了实际应用场景的建议。
813

被折叠的 条评论
为什么被折叠?



