ArrayList还是LinkedList?性能可差千倍

注意 for 循环中的 linkLast() 方法,它可以把链表重新链接起来,这样就恢复了链表序列化之前的顺序。很妙,对吧?

和 ArrayList 相比,LinkedList 没有实现 RandomAccess 接口,这是因为 LinkedList 存储数据的内存地址是不连续的,所以不支持随机访问。

03、ArrayList 和 LinkedList 新增元素时究竟谁快?

====================================

前面我们已经从多个维度了解了 ArrayList 和 LinkedList 的实现原理和各自的特点。那接下来,我们就来聊聊 ArrayList 和 LinkedList 在新增元素时究竟谁快?

1)ArrayList

ArrayList 新增元素有两种情况,一种是直接将元素添加到数组末尾,一种是将元素插入到指定位置。

添加到数组末尾的源码:

public boolean add(E e) {

modCount++;

add(e, elementData, size);

return true;

}

private void add(E e, Object[] elementData, int s) {

if (s == elementData.length)

elementData = grow();

elementData[s] = e;

size = s + 1;

}

很简单,先判断是否需要扩容,然后直接通过索引将元素添加到末尾。

插入到指定位置的源码:

public void add(int index, E element) {

rangeCheckForAdd(index);

modCount++;

final int s;

Object[] elementData;

if ((s = size) == (elementData = this.elementData).length)

elementData = grow();

System.arraycopy(elementData, index,

elementData, index + 1,

s - index);

elementData[index] = element;

size = s + 1;

}

先检查插入的位置是否在合理的范围之内,然后判断是否需要扩容,再把该位置以后的元素复制到新添加元素的位置之后,最后通过索引将元素添加到指定的位置。这种情况是非常伤的,性能会比较差。

2)LinkedList

LinkedList 新增元素也有两种情况,一种是直接将元素添加到队尾,一种是将元素插入到指定位置。

添加到队尾的源码:

public boolean add(E e) {

linkLast(e);

return true;

}

void linkLast(E e) {

final LinkedList.Node l = last;

final LinkedList.Node newNode = new LinkedList.Node<>(l, e, null);

last = newNode;

if (l == null)

first = newNode;

else

l.next = newNode;

size++;

modCount++;

}

先将队尾的节点 last 存放到临时变量 l 中(不是说不建议使用 I 作为变量名吗?Java 的作者们明知故犯啊),然后生成新的 Node 节点,并赋给 last,如果 l 为 null,说明是第一次添加,所以 first 为新的节点;否则将新的节点赋给之前 last 的 next。

插入到指定位置的源码:

public void add(int index, E element) {

checkPositionIndex(index);

if (index == size)

linkLast(element);

else

linkBefore(element, node(index));

}

LinkedList.Node node(int index) {

// assert isElementIndex(index);

if (index < (size >> 1)) {

LinkedList.Node x = first;

for (int i = 0; i < index; i++)

x = x.next;

return x;

} else {

LinkedList.Node x = last;

for (int i = size - 1; i > index; i–)

x = x.prev;

return x;

}

}

void linkBefore(E e, LinkedList.Node succ) {

// assert succ != null;

final LinkedList.Node pred = succ.prev;

final LinkedList.Node newNode = new LinkedList.Node<>(pred, e, succ);

succ.prev = newNode;

if (pred == null)

first = newNode;

else

pred.next = newNode;

size++;

modCount++;

}

先检查插入的位置是否在合理的范围之内,然后判断插入的位置是否是队尾,如果是,添加到队尾;否则执行 linkBefore() 方法。

在执行 linkBefore() 方法之前,会调用 node() 方法查找指定位置上的元素,这一步是需要遍历 LinkedList 的。如果插入的位置靠前前半段,就从队头开始往后找;否则从队尾往前找。也就是说,如果插入的位置越靠近 LinkedList 的中间位置,遍历所花费的时间就越多。

找到指定位置上的元素(succ)之后,就开始执行 linkBefore() 方法了,先将 succ 的前一个节点(prev)存放到临时变量 pred 中,然后生成新的 Node 节点(newNode),并将 succ 的前一个节点变更为 newNode,如果 pred 为 null,说明插入的是队头,所以 first 为新节点;否则将 pred 的后一个节点变更为 newNode。

ArrayList还是LinkedList?性能可差千倍

经过源码分析以后,小伙伴们是不是在想:“好像 ArrayList 在新增元素的时候效率并不一定比 LinkedList 低啊!”

当两者的起始长度是一样的情况下:

  • 如果是从集合的头部新增元素,ArrayList 花费的时间应该比 LinkedList 多,因为需要对头部以后的元素进行复制。

public class ArrayListTest {

public static void addFromHeaderTest(int num) {

ArrayList list = new ArrayList(num);

int i = 0;

long timeStart = System.currentTimeMillis();

while (i < num) {

list.add(0, i + “沉默王二”);

i++;

}

long timeEnd = System.currentTimeMillis();

System.out.println(“ArrayList从集合头部位置新增元素花费的时间” + (timeEnd - timeStart));

}

}

/**

  • @author 微信搜「沉默王二」,回复关键字 PDF

*/

public class LinkedListTest {

public static void addFromHeaderTest(int num) {

LinkedList list = new LinkedList();

int i = 0;

long timeStart = System.currentTimeMillis();

while (i < num) {

list.addFirst(i + “沉默王二”);

i++;

}

long timeEnd = System.currentTimeMillis();

System.out.println(“LinkedList从集合头部位置新增元素花费的时间” + (timeEnd - timeStart));

}

}

num 为 10000,代码实测后的时间如下所示:

ArrayList从集合头部位置新增元素花费的时间595

LinkedList从集合头部位置新增元素花费的时间15

复制代码

ArrayList 花费的时间比 LinkedList 要多很多。

  • 如果是从集合的中间位置新增元素,ArrayList 花费的时间搞不好要比 LinkedList 少,因为 LinkedList 需要遍历。

public class ArrayListTest {

public static void addFromMidTest(int num) {

ArrayList list = new ArrayList(num);

int i = 0;

long timeStart = System.currentTimeMillis();

while (i < num) {

int temp = list.size();

list.add(temp / 2 + “沉默王二”);

i++;

}

long timeEnd = System.currentTimeMillis();

System.out.println(“ArrayList从集合中间位置新增元素花费的时间” + (timeEnd - timeStart));

}

}

public class LinkedListTest {

public static void addFromMidTest(int num) {

LinkedList list = new LinkedList();

int i = 0;

long timeStart = System.currentTimeMillis();

while (i < num) {

int temp = list.size();

list.add(temp / 2, i + “沉默王二”);

i++;

}

long timeEnd = System.currentTimeMillis();

System.out.println(“LinkedList从集合中间位置新增元素花费的时间” + (timeEnd - timeStart));

}

}

num 为 10000,代码实测后的时间如下所示:

ArrayList从集合中间位置新增元素花费的时间1

LinkedList从集合中间位置新增元素花费的时间101

ArrayList 花费的时间比 LinkedList 要少很多很多。

  • 如果是从集合的尾部新增元素,ArrayList 花费的时间应该比 LinkedList 少,因为数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列。

public class ArrayListTest {

public static void addFromTailTest(int num) {

ArrayList list = new ArrayList(num);

int i = 0;

long timeStart = System.currentTimeMillis();

while (i < num) {

list.add(i + “沉默王二”);

i++;

}

long timeEnd = System.currentTimeMillis();

System.out.println(“ArrayList从集合尾部位置新增元素花费的时间” + (timeEnd - timeStart));

}

}

public class LinkedListTest {

public static void addFromTailTest(int num) {

LinkedList list = new LinkedList();

int i = 0;

long timeStart = System.currentTimeMillis();

while (i < num) {

list.add(i + “沉默王二”);

i++;

}

long timeEnd = System.currentTimeMillis();

System.out.println(“LinkedList从集合尾部位置新增元素花费的时间” + (timeEnd - timeStart));

}

}

num 为 10000,代码实测后的时间如下所示:

ArrayList从集合尾部位置新增元素花费的时间69

LinkedList从集合尾部位置新增元素花费的时间193

ArrayList 花费的时间比 LinkedList 要少一些。

这样的结论和预期的是不是不太相符?ArrayList 在添加元素的时候如果不涉及到扩容,性能在两种情况下(中间位置新增元素、尾部新增元素)比 LinkedList 好很多,只有头部新增元素的时候比 LinkedList 差,因为数组复制的原因。

当然了,如果涉及到数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。

04、ArrayList 和 LinkedList 删除元素时究竟谁快?

====================================

1)ArrayList

ArrayList 删除元素的时候,有两种方式,一种是直接删除元素(remove(Object)),需要直先遍历数组,找到元素对应的索引;一种是按照索引删除元素(remove(int))。

public boolean remove(Object o) {

final Object[] es = elementData;

final int size = this.size;

int i = 0;

found: {

if (o == null) {

for (; i < size; i++)

if (es[i] == null)

break found;

} else {

for (; i < size; i++)

if (o.equals(es[i]))

break found;

}

return false;

}

fastRemove(es, i);

return true;

}

public E remove(int index) {

Objects.checkIndex(index, size);

final Object[] es = elementData;

@SuppressWarnings(“unchecked”) E oldValue = (E) es[index];

fastRemove(es, index);

return oldValue;

}

但从本质上讲,都是一样的,因为它们最后调用的都是 fastRemove(Object, int) 方法。

private void fastRemove(Object[] es, int i) {

modCount++;

final int newSize;

if ((newSize = size - 1) > i)

System.arraycopy(es, i + 1, es, i, newSize - i);

es[size = newSize] = null;

}

从源码可以看得出,只要删除的不是最后一个元素,都需要数组重组。删除的元素位置越靠前,代价就越大。

2)LinkedList

LinkedList 删除元素的时候,有四种常用的方式:

  • remove(int),删除指定位置上的元素

public E remove(int index) {

checkElementIndex(index);

return unlink(node(index));

}

先检查索引,再调用 node(int) 方法( 前后半段遍历,和新增元素操作一样)找到节点 Node,然后调用 unlink(Node) 解除节点的前后引用,同时更新前节点的后引用和后节点的前引用:

E unlink(Node x) {

// assert x != null;

final E element = x.item;

final Node next = x.next;

final Node prev = x.prev;

if (prev == null) {

first = next;

} else {

prev.next = next;

x.prev = null;

}

if (next == null) {

last = prev;

} else {

next.prev = prev;

x.next = null;

}

x.item = null;

size–;

modCount++;

return element;

}

  • remove(Object),直接删除元素

public boolean remove(Object o) {

if (o == null) {

for (LinkedList.Node x = first; x != null; x = x.next) {

if (x.item == null) {

unlink(x);

return true;

}

}

} else {

for (LinkedList.Node x = first; x != null; x = x.next) {

if (o.equals(x.item)) {

unlink(x);

return true;

}

}

}

return false;

}

也是先前后半段遍历,找到要删除的元素后调用 unlink(Node)。

  • removeFirst(),删除第一个节点

public E removeFirst() {

final LinkedList.Node f = first;

if (f == null)

throw new NoSuchElementException();

return unlinkFirst(f);

}

private E unlinkFirst(LinkedList.Node f) {

// assert f == first && f != null;

final E element = f.item;

final LinkedList.Node next = f.next;

f.item = null;

f.next = null; // help GC

first = next;

if (next == null)

last = null;

else

next.prev = null;

size–;

modCount++;

return element;

}

删除第一个节点就不需要遍历了,只需要把第二个节点更新为第一个节点即可。

  • removeLast(),删除最后一个节点

删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点即可。

可以看得出,LinkedList 在删除比较靠前和比较靠后的元素时,非常高效,但如果删除的是中间位置的元素,效率就比较低了。

这里就不再做代码测试了,感兴趣的小伙伴可以自己试试,结果和新增元素保持一致:

  • 从集合头部删除元素时,ArrayList 花费的时间比 LinkedList 多很多;

  • 从集合中间位置删除元素时,ArrayList 花费的时间比 LinkedList 少很多;

  • 从集合尾部删除元素时,ArrayList 花费的时间比 LinkedList 少一点。

我本地的统计结果如下所示,小伙伴们可以作为参考:

ArrayList从集合头部位置删除元素花费的时间380

LinkedList从集合头部位置删除元素花费的时间4

ArrayList从集合中间位置删除元素花费的时间381

LinkedList从集合中间位置删除元素花费的时间5922

ArrayList从集合尾部位置删除元素花费的时间8

LinkedList从集合尾部位置删除元素花费的时间12

05、ArrayList 和 LinkedList 遍历元素时究竟谁快?

====================================

1)ArrayList

遍历 ArrayList 找到某个元素的话,通常有两种形式:

  • get(int),根据索引找元素

public E get(int index) {

Objects.checkIndex(index, size);

return elementData(index);

}

由于 ArrayList 是由数组实现的,所以根据索引找元素非常的快,一步到位。

  • indexOf(Object),根据元素找索引

public int indexOf(Object o) {

return indexOfRange(o, 0, size);

}

int indexOfRange(Object o, int start, int end) {

Object[] es = elementData;

if (o == null) {

for (int i = start; i < end; i++) {

if (es[i] == null) {

return i;

}

}

} else {

for (int i = start; i < end; i++) {

if (o.equals(es[i])) {

return i;

}

}

}

return -1;

}

根据元素找索引的话,就需要遍历整个数组了,从头到尾依次找。

2)LinkedList

遍历 LinkedList 找到某个元素的话,通常也有两种形式:

  • get(int),找指定位置上的元素

public E get(int index) {

checkElementIndex(index);

return node(index).item;

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

结局:总结+分享

看完美团、字节、腾讯这三家的一二三面试问题,是不是感觉问的特别多,可能咱们真的又得开启面试造火箭、工作拧螺丝的模式去准备下一次的面试了。

开篇有提及我可是足足背下了Java互联网工程师面试1000题,多少还是有点用的呢,换汤不换药,不管面试官怎么问你,抓住本质即可!能读到此处的都是真爱

  • Java互联网工程师面试1000题

image.png

而且从上面三家来看,算法与数据结构是必备不可少的呀,因此我建议大家可以去刷刷这本左程云大佬著作的 《程序员代码面试指南 IT名企算法与数据结构题目最优解》,里面近200道真实出现过的经典代码面试题。

  • 程序员代码面试指南–IT名企算法与数据结构题目最优解

image.png

  • 其余像设计模式,建议可以看看下面这4份PDF(已经整理)

image.png

  • 更多的Java面试学习笔记如下,关于面试这一块,我额外细分出Java基础-中级-高级开发的面试+解析,以及调优笔记等等等。。。

image.png

以上所提及的全部Java面试学习的PDF及笔记,如若皆是你所需要的,那么都可发送给你!

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

结局:总结+分享

看完美团、字节、腾讯这三家的一二三面试问题,是不是感觉问的特别多,可能咱们真的又得开启面试造火箭、工作拧螺丝的模式去准备下一次的面试了。

开篇有提及我可是足足背下了Java互联网工程师面试1000题,多少还是有点用的呢,换汤不换药,不管面试官怎么问你,抓住本质即可!能读到此处的都是真爱

  • Java互联网工程师面试1000题

[外链图片转存中…(img-dB5uHzX4-1711961046553)]

而且从上面三家来看,算法与数据结构是必备不可少的呀,因此我建议大家可以去刷刷这本左程云大佬著作的 《程序员代码面试指南 IT名企算法与数据结构题目最优解》,里面近200道真实出现过的经典代码面试题。

  • 程序员代码面试指南–IT名企算法与数据结构题目最优解

[外链图片转存中…(img-EaRucRYd-1711961046553)]

  • 其余像设计模式,建议可以看看下面这4份PDF(已经整理)

[外链图片转存中…(img-rhnMxDRW-1711961046553)]

  • 更多的Java面试学习笔记如下,关于面试这一块,我额外细分出Java基础-中级-高级开发的面试+解析,以及调优笔记等等等。。。

[外链图片转存中…(img-SkJmM3IJ-1711961046554)]

以上所提及的全部Java面试学习的PDF及笔记,如若皆是你所需要的,那么都可发送给你!

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值