Java--LinkedList真的比ArrayList添加元素快?Open JDK JMH带你揭开真相_为什么arraylist添加元素比linklist快

img
img

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

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

1.2.2 LinkedList常用API
  • 增:效率add(E e) = addLast(E e) = addFirst(E e) > add(int index,E element)
  • 删:效率removeFirst() = removeLast() > remove(int index) >remove(Object o)
  • 改:set(int index,E element)
  • 查:get(int index)
1.2.3 LinkedList常见面试题(附参考答案)

(1)LinkedList为什么不能实现RandomAccess接口?

  • 因为LinkedList底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess 接口

(2)LinkedList几个重要属性各自的含义?

  • LinkedList底层主要属性有size集合大小(链表长度)、first链表头部节点、last链表尾部节点,并且也都使用transient修饰,表示不能外部序列化与反序列化,内部自己实现了序列化与反序列化。

(3)LinkedList默认添加元素是怎么实现的?

  • LinkedList添加元素的方法主要有直接添加和指定位置添加
  • 直接添加元素有add(E e)方法和addAll(Collection c)方法,指定位置添加元素有addFirst(E e)addLast(E e)add(int index,E element)addAll(int index,Collection c)方法
  • 直接添加元素默认添加到链表的尾部,不需要先查找节点,直接通过尾部节点,创建新节点和变换指针指向新节点
  • 指定位置添加元素,如果是添加元素到链表头部或尾部,都不需要先查找节点,直接通过头部或尾部节点,创建新节点和变换指针指向新节点,但是如果是add(int index,E element)添加元素到非首尾位置,会先使用二分查找到该位置对应的节点,再通过该节点,创建新节点和变换指针指向新节点

(4)LinkedList使用哪种方式遍历效率最高?

  • LinkedList底层数据结构是双向链表的,使用foreach循环或iterator迭代器遍历效率最高,通过迭代器的hasNext()next()快速遍历元素
  • 需要注意的是尽量避免使用for循环遍历LinkedList,因为每一次循环,都会使用二分查找先找到指定位置的节点,再通过该节点新建节点以及变换指针,效率极其低下。

(5)LinkedList使用Iterator迭代器遍历时,进行remove(E e)操作,迭代器的指针会发生什么变化?

  • LinkedList使用迭代器遍历,内部自己实现的是ListIterator接口,主要有lastReturned最后返回节点、next下一节点、nextIndex下一指针以及expectedModCount期望修改次数4个重要属性,其中nextIndex下一指针从0开始
  • LinkedList迭代器遍历时,每进行一趟遍历,都会经过hasNext()next()操作,并且在next()方法中,进行了nextIndex++操作
  • 当进行到某一趟遍历,调用remove()进行操作元素,会通过lastReturned进行解链,并且将next = lastReturnednextIndex--操作,也就是迭代器的下一指针会减一

(6)LinkedList默认位置添加元素和指定位置添加元素分别怎么实现,哪种性能更高?

  • LinkedList默认位置添加元素通过add(E e)方法实现,默认位置是添加元素到尾部,时间复杂度是O(1)-常数复杂度,效率最高。
  • LinkedList指定位置添加元素通过addLast(E e)addFirst(E e)方法添加元素到尾部和头部,add(E e)方法默认位置是添加元素到尾部,时间复杂度是O(1)-常数复杂度,效率最高。
  • LinkedList还有一种指定位置添加元素通过add(int index, E element)方法实现,这种方式添加元素前,需要先通过二分查找找到指定位置的元素,再通过该元素进行新节点创建以及指针变换,综合时间复杂度是O(logN),如果添加元素的位置刚好在中间,二分查找发挥的作用最小,效率比较低~

(7)List集合迭代器遍历使用Iterator和ListIterator有什么不同?

  • 都是迭代器,都可以用来遍历对应的实现类
  • Iterator允许遍历所有实现Iterator接口的实现类,而ListIterator只能遍历List集合
  • 两者都有hasNext()next()remove()方法正向操作列表,ListIterator还可以进行逆向操作列表和往集合中添加元素

二、JMH测试ArrayList和LinkedList性能

2.1 JMH是什么?

官方介绍
在这里插入图片描述
简单介绍

JMH全称Java Microbenchmark Harness(Java微基准套件),是Oracle 官方Open JDK提供的一款Java微基准性能测试工具,主要是基于方法层面的基准测试,纳秒级精确度~

2.2 JMH应用场景?

【典型应用】

  • 精确验证某个方法执行时间
  • 测试接口不同实现的吞吐量
  • 等等…

日常开发及优化Java代码时,当定位到热点方法,想进一步优化热点方法的性能时,可以通过JHM精确验证某个方法执行时间,根据测试结果不断进行优化~

2.3 JMH测试ArrayList和LinkedList(揭开真相)

应用场景不在于有多少,在于你用不用得到,这里我们就利用JMH精确验证某个方法执行时间,来验证本文的主题LinkedList添加元素真的比ArrayList快?

大致思路:

分别实现LinkedListArrayList默认添加元素即尾部添加元素的方法~
利用JMH分别对两个方法进行基准测试,根据测试结果进行分析性能~

2.3.1 代码实现

(1)添加依赖
需要依赖jmh-core和jmh-generator-annprocess两个架包,可以去Maven官网下载~

Maven工程直接在pom.xml中添加依赖如下:

		<dependency>
			<groupId>org.openjdk.jmh</groupId>
			<artifactId>jmh-core</artifactId>
			<version>1.26</version>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>org.openjdk.jmh</groupId>
			<artifactId>jmh-generator-annprocess</artifactId>
			<version>1.26</version>
			<scope>provided</scope>
		</dependency>

(2)添加元素方法

package com.justin.java;

import org.openjdk.jmh.annotations.\*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.Throughput) //基准测试类型:吞吐量(单位时间内调用了多少次)
@OutputTimeUnit(TimeUnit.MILLISECONDS) //基准测试结果的时间类型:毫秒
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) //预热:2 轮,每次1秒
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) //度量:测试5轮,每轮1秒
@Fork(1) //Fork出1个线程来测试
@State(Scope.Thread) // 每个测试线程分配1个实例
public class ArrayAndLinkedJmhTest {
    private List<Object> arrayList;
    private List<Object> linkedList;
    @Param({"10", "100", "1000"})
    private int count; //分别测试不同个数的添加元素

    @Setup(Level.Trial)
    public void init() {
        arrayList = new ArrayList<>();
        linkedList = new LinkedList<>();
    }

    public static void main(String[] args) throws RunnerException {
        //启动基准测试
        Options opt = new OptionsBuilder()
                .include(ArrayAndLinkedJmhTest.class.getSimpleName()) //要导入的测试类
                .output("C:\\Users\\Administrator\\Desktop\\ArrayAndLinkedJmhTest.log") //输出测试结果的文件
                .build();
        //执行测试
        new Runner(opt).run();
    }

    @Benchmark
    public void arrayListAddTest() {
        for (int i = 0; i < count; i++) {
            arrayList.add("Justin");
        }
    }

    @Benchmark
    public void linkedListAddTest() {
        for (int i = 0; i < count; i++) {
            linkedList.add("Justin");
        }
    }

}


注解说明

注解说明
@BenchmarkMode(Mode.AverageTime)基准测试类型: AverageTime表示平均时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)基准测试结果的时间类型:NANOSECONDS表示微秒
@Warmup(iterations = 5)预热:iterations指定预热迭代几轮
@Measurement(iterations = 10)度量:iterations 指定迭代几轮
@Fork(2)Fork2个线程来测试
@State(Scope.Thread)每个测试线程分配1个实例
@Param({"10", "100", "1000"})指定某项参数的多种情况,数值根据实际给定
@Setup(Level.Trial)初始化方法,在全部Benchmark运行之前进行
@BenchmarkMode用在方法上,表示该方法要进行基准测试,类似JUnit@Test
@TearDown(Level.Trial)结束方法,在全部Benchmark运行之后进行

更多官方Sample:
OpenJDK Samples
GitHub OpenJDK Samples

**由于添加元素消耗的内存比较大,idea执行基准测试过程中可能会出现内存泄露的报错:**java.lang.OutOfMemoryError: Java heap space
加大JVM的内存参数值即可,例如idea中调整方式:
Help -> Edit Custom VM Options…

在这里插入图片描述

2.3.2 结果分析

分析输出的结果文件ArrayAndLinkedJmhTest.log,输出的内容很多,只要前面没有error都不用管,直接拉到最后看Score结果~

Benchmark                                (count)  Mode  Cnt     Score     Error  Units
ArrayAndLinkedJmhTest.arrayListAddTest        10  avgt   20    74.287 ±   1.629  ns/op
ArrayAndLinkedJmhTest.arrayListAddTest       100  avgt   20   371.329 ±  23.952  ns/op
ArrayAndLinkedJmhTest.arrayListAddTest      1000  avgt   20  3118.537 ±  38.943  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest       10  avgt   20    95.773 ±   1.261  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest      100  avgt   20   907.471 ±  13.856  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest     1000  avgt   20  8901.777 ± 108.044  ns/op

示例中我们使用注解的是@BenchmarkMode(Mode.AverageTime)OutputTimeUnit(TimeUnit.NANOSECONDS),因此测试的结果是ns/ops(每次调用需要多少微秒),每次调用添加相同的元素,平均时间越高,表明效率越低~

对应的结果指标是Score,可以看到ArrayList添加元素从少到多,效率都吊打LinkedList~

不过,细心的同学可能发现了,ArrayList之所以吊打LinkedList这么猛,是因为代码中ArrayList指定了合适的初始化容量大小

List<Object> arrayList = new ArrayList<>(count);

加上添加元素默认是在尾部进行,可谓是如虎添翼,ArrayList底层数据结构是数组,数组是一块连续的内存空间,从数组尾部添加数据时,不需要进行任何数组数据的复制重排,效率最高,时间复杂度为O(1)

ArrayList默认添加元素的关键源码

	//关键源码1
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    ...
    //关键源码2
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }    
    ...
    //关键源码3
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }    
	...
	//关键源码4
    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);
    }	

虽然LinkedList添加元素到尾部,也不需要查找元素,效率也是最高的,但是多了新节点对象创建以及变换指针指向对象的过程,时间复杂度为O(1)~

LinkedList默认添加元素源码

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    ...
        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++;
    }

OK,那么问题来了,ArrayList默认初始化容量,添加元素的效率还能吊打LinkedList吗?
我们来测试一下,首先修改测试代码中arrayListAddTest方法,去掉指定count~

List<Object> arrayList = new ArrayList<>();

ArrayList默认初始化容量添加元素,前面指定最大添加元素是1000数量级,测出的效率可能不是很明显,我这里再加个10000级别的(跑的好慢,差不多半个小时)`

    @Param({"10", "100", "1000","10000"})
    private int count; //指定添加元素的不同个数,便于分析结果

重新跑一次,看测试结果~
ArrayList默认容量大小的测试结果:

Benchmark                                (count)  Mode  Cnt      Score      Error  Units
ArrayAndLinkedJmhTest.arrayListAddTest        10  avgt   20     59.312 ±    1.726  ns/op
ArrayAndLinkedJmhTest.arrayListAddTest       100  avgt   20    560.263 ±   25.140  ns/op
ArrayAndLinkedJmhTest.arrayListAddTest      1000  avgt   20   5144.459 ±  156.745  ns/op
ArrayAndLinkedJmhTest.arrayListAddTest     10000  avgt   20  49326.440 ± 1810.873  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest       10  avgt   20    100.151 ±    2.699  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest      100  avgt   20    976.157 ±   55.646  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest     1000  avgt   20   9472.496 ±  314.018  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest    10000  avgt   20  91279.112 ± 2883.922  ns/op

比对前面的ArrayList指定合适容量大小的测试结果:

Benchmark                                (count)  Mode  Cnt     Score     Error  Units
ArrayAndLinkedJmhTest.arrayListAddTest        10  avgt   20    74.287 ±   1.629  ns/op
ArrayAndLinkedJmhTest.arrayListAddTest       100  avgt   20   371.329 ±  23.952  ns/op
ArrayAndLinkedJmhTest.arrayListAddTest      1000  avgt   20  3118.537 ±  38.943  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest       10  avgt   20    95.773 ±   1.261  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest      100  avgt   20   907.471 ±  13.856  ns/op
ArrayAndLinkedJmhTest.linkedListAddTest     1000  avgt   20  8901.777 ± 108.044  ns/op

可以看到ArrayList使用默认容量大小进行添加元素时,虽然ArrayList多了数组扩容的过程,但是由于在尾部添加元素,效率还是比LinkedList添加元素效率高,只不过没有指定合适初始化容量大小时差距那么明显~

那么在什么情况下LinkedList才比ArrayList添加元素效率高?

答案是:头部添加元素~
ArrayList在数据结构头部添加元素比LinkedList效率低很多~
ArrayList越往数组前面添加元素,效率越低,头部添加数据,会导致后面的全部数组元素都要进行复制重排,效率最低~
LinkedList直接在头部或尾部,不需要进行二分查找,直接创建新节点元素及变换指针,效率最高~
不过需要注意的是LinkedList指定位置添加元素时,会先通过二分查找(中间划分,从前往后或从后往前遍历查找)查找到该位置的元素,当指定的位置元素刚好是中间时,二分查找发挥的作用最小,效率比较低~~

本文JHM测试主要验证默认添加元素(尾部添加元素),至于头部中间添加元素的效率对比,具体可以自行添加方法,通过JMH验证下~

2.3.3 结果分析(图形)

前面JMH输出的普通结果文件,虽然拉到能直接看到结果,不过有点不太直观,这里我们将 main方法的输出文件部分进行改造一下,重新执行输出json结果文件~

    public static void main(String[] args) throws RunnerException {
        //1、启动基准测试:输出json结果文件(用于查看可视化图)
        Options opt = new OptionsBuilder()
                .include(ArrayAndLinkedJmhTest.class.getSimpleName()) //要导入的测试类
                .result("C:\\Users\\Administrator\\Desktop\\ArrayAndLinkedJmhTest.json") //输出测试结果的json文件
                .resultFormat(ResultFormatType.JSON)//格式化json文件
                .build();

        //2、执行测试
        new Runner(opt).run();
    }

执行过程中可能看到ArrayAndLinkedJmhTest.json没有写入内容,不用管,最后执行完会刷新到文件中~

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

e.JSON)//格式化json文件
.build();

    //2、执行测试
    new Runner(opt).run();
}

**执行过程中可能看到`ArrayAndLinkedJmhTest.json`没有写入内容,不用管,最后执行完会刷新到文件中~**


[外链图片转存中...(img-2XJTNeZu-1715836145507)]
[外链图片转存中...(img-roPJVZtV-1715836145508)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值