当前位置: 首页 > news >正文

【常见集合】ArrayList与LinkedList

1 ArrayList

1.1 底层原理

数组(Array)是一种用连续的内存空间存储相同数据类的线性数据结构

1.2 寻址公式

数组是如何获取其他元素地址的呢

寻址公式:a[i] = baseAddress + i * dataTypeSize

baseAddress : 数组首地址

dataTypeSize :数组中元素类型大小

思考:为什么数组中元素下标从0开始?

如果不是从0开始而是1的话,寻址公式将变成:a[i] = baseAddress + ( i - 1) * dataTypeSize

此时对CPU来说多了一个减法指令,会影响性能

1.3 操作数组的时间复杂度

1.3.1 随机查找

即根据索引查找,时间复杂度为 O(1)

1.3.2 未知索引查找

遍历查找(未排序):时间复杂度为O(n)

二分查找(已排序):时间复杂度为O(logn)

1.3.3 插入、删除

时间复杂度为O(n)

2 ArrayList

2.1ArrayList源码分析(jdk17)

2.1.1 成员变量

/*** 默认初始容量为10,无参构造时首次添加元素会以此值初始化*/
private static final int DEFAULT_CAPACITY = 10;/*** 指定初始容量为0时使用的空数组实例*/
private static final Object[] EMPTY_ELEMENTDATA = {};/*** 无参构造时使用的空数组实例,与EMPTY_ELEMENTDATA区分以实现首次添加元素时扩容至默认容量*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** 存储元素的底层数组,容量为该数组的长度* 当elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,首次添加元素会扩容至DEFAULT_CAPACITY* 用transient修饰避免自动序列化,采用自定义序列化逻辑* 非private修饰是为了方便嵌套类访问*/
transient Object[] elementData; /*** ArrayList中实际包含的元素数量(与数组容量elementData.length区分)*/
private int size;

2.1.2 构造方法

// 带初始容量参数的构造方法
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {// 当初始容量大于0时,创建指定容量的Object数组this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {// 当初始容量等于0时,使用EMPTY_ELEMENTDATA空数组this.elementData = EMPTY_ELEMENTDATA;} else {// 当初始容量为负数时,抛出非法参数异常throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}
}/*** 无参构造方法,创建一个初始容量为10的空列表* 注意:实际初始化会延迟到首次添加元素时*/
public ArrayList() {// 使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组作为初始值// 与EMPTY_ELEMENTDATA的区别是首次添加元素时会自动扩容至DEFAULT_CAPACITY(10)this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}/*** 通过集合创建ArrayList的构造方法* @param c 被用来初始化ArrayList的源集合,其元素类型需是E的子类*/
public ArrayList(Collection<? extends E> c) {// 将源集合转换为数组Object[] a = c.toArray();// 将源集合的元素数量赋值给当前ArrayList的sizeif ((size = a.length) != 0) {// 如果源集合是ArrayList类型if (c.getClass() == ArrayList.class) {// 直接复用源集合的内部数组(优化:避免额外复制)elementData = a;} else {// 非ArrayList集合则通过Arrays.copyOf()复制元素到新数组// 第三个参数指定目标数组类型为Object[]elementData = Arrays.copyOf(a, size, Object[].class);}} else {// 源集合为空时,使用EMPTY_ELEMENTDATA空数组elementData = EMPTY_ELEMENTDATA;}
}

2.1.3 逻辑分析

以下面这个程序为例对添加和扩容操作进行分析

public static void main(String[] args) {List<Integer> list = new ArrayList<Integer>();list.add(1);for (int i = 2; i <= 10; i++){list.add(i);}list.add(11);}

核心代码片段:

入口:add(E e) 方法(首次添加元素的调用入口)
/*** 尾部追加元素(首次添加元素从这里进入)*/
public boolean add(E e) {modCount++; // 记录结构修改次数(用于fail-fast迭代器)add(e, elementData, size); // 调用私有重载方法完成实际添加return true;
}

核心逻辑:add(E e, Object[] elementData, int s) 私有方法 java 运行

/*** 私有辅助方法:拆分add(E)逻辑以控制字节码大小(优化JIT内联)* @param e 待添加元素* @param elementData 当前底层数组* @param s 当前元素数量(size)*/
private void add(E e, Object[] elementData, int s) {// 关键判断:当前元素数量 == 底层数组长度 → 容量不足,需要扩容if (s == elementData.length)elementData = grow(); // 扩容(首次添加时触发)// 扩容后,将元素放入数组尾部,size+1elementData[s] = e;size = s + 1;
}

扩容核心:grow() 与 grow(int minCapacity) 方法

/*** 无参扩容:默认扩容到“当前size+1”(首次添加时size=0,minCapacity=1)*/
private Object[] grow() {return grow(size + 1); // 调用有参grow,传入“最小需要容量”
}/*** 有参扩容:根据最小需要容量计算新容量,生成新数组* @param minCapacity 最小需要容量(首次添加时为1)*/
private Object[] grow(int minCapacity) {int oldCapacity = elementData.length; // 旧容量(首次添加时为0)// 分支1:非默认空数组(如new ArrayList(0)创建的集合)if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 计算新容量:优先按“旧容量*1.5”扩容,若不足则用minCapacityint newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, // 最小增长幅度(需要补充的容量)oldCapacity >> 1           // 优先增长幅度(旧容量/2,即1.5倍扩容));// 复制旧数组元素到新数组return elementData = Arrays.copyOf(elementData, newCapacity);} // 分支2:默认空数组(无参构造new ArrayList()创建的集合)else {// 首次添加时,扩容到“默认容量10”与“minCapacity”的较大值(此时minCapacity=1,故取10)return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];}
}

这里我们主要分析首次添加数据和第11次添加数据的情况:

首次添加数据

第11次添加数据

2.2 ArrayList底层实现原理

ArrayList底层由动态数组实现

AarrayList初始容量为0,当插入第一个数据时才会初始化容量为10

ArrayList进行扩容时时原容量的1.5倍(向下取整),每次扩容都要拷贝数组

ArrayList添加数据时:

        确保数组已用长度+1后足够下个数据存储

        计算数值容量,若当前数组已用长度+1后大于其容量,则用grow()方法进行扩容

        确保有地方存储新数据后,将新元素添加到size的位置上

        添加成功后返回布尔值true

2.3 如何实现数组与List间的转换

数组转List调用asList()方法,修改数组内容时,由于List和数组指向同一内存地址,因此List也会受影响

List转数组调用toArray方法,无参toArray方法返回Object数组,传入初始化对象数组,返回该对象数组,修改List内容时,由于是通过拷贝创建新对象,数组内容不会受影响

3 LinkedList

3.1 单向链表

3.1.1 时间复杂度分析

查询操作:时间复杂度为O(n)

增删操作:时间复杂度为O(n)

3.2 双向链表

3.2.1 时间复杂度分析

查询操作:时间复杂度为O(n),给定节点时查询前驱节点时间复杂度为O(1)

增删操作:时间复杂度为O(n),给定节点时增删前驱节点时间复杂度为O(1)

3.2.2 与单向链表对比

需要两个额外空间存储前驱节点和后继结点地址

支持双向遍历,操作更灵活

4 ArrayList和LinkedList的区别

4.1 底层数据结构

ArrayList是由动态数组实现的

LinkedList是由双向链表实现的

4.2 操作数据效率

ArrayList可按下标查询,时间复杂度为O(1),LinkedList不支持下标查询

未知索引时ArrayList与LinkedList查询操作时间复杂度均为O(n)

对ArrayList尾部进行增删操作时间复杂度为O(1),其他部分为O(n)

对LinkedList头尾进行增删操作时间复杂度为O(1),其他部分为O(n)

4.3 内存空间占用

ArrayList底层是数组,内存连续,节省内存

LinkedList底层是双向链表,存储数据同时也存储头尾两个指针,更占用内存

4.4 线程安全问题

二者均不是线程安全的

如果想要保证线程安全有以下两种方案:

只在方法内使用,局部变量是安全的

使用线程安全的ArrayList和LinkedList:使用Collections的synchronizedList()方法进行包装,但会影响性能和效率

http://www.dtcms.com/a/390618.html

相关文章:

  • IPD流程实战:如何跨领域应用IPD思维?
  • Archery:开源、一站式的数据库 SQL 审核与运维平台
  • 北斗GNSS在地质灾害监测中的变形监测技术与应用解析
  • C语言题目:用“*”作为元素打印菱形
  • Redis的java客户端(SpringDataRedis)
  • Amazon Aurora DSQL:分布式无服务器数据库的下一场革命
  • TVS管频繁损坏,是参数错选还是布局出问题?-ASIM阿赛姆
  • 论文阅读:TEMPORAL GRAPH NETWORKS FOR DEEP LEARNING ON DYNAMIC GRAPHS
  • 医疗行业淘汰赛开始了?医疗器械售后维修是否会有影响?
  • 第二部分:VTK核心类详解(第40章 vtkIdList ID列表类)
  • Elasticsearch的自定义score评分
  • 【软考-系统架构设计师】架构权衡分析方法(ATAM)
  • 信息系统项目的成本管理
  • Python进阶指南7:排序算法和树
  • 深入理解 HashMap的数据结构
  • ArcGIS前后两期数据库对比工具
  • React18学习笔记(三) ReactRouter----React中的路由
  • [cesium] vue3 安装cesium方法
  • 埃文科技亮相华为全联接大会2025 联合鲲鹏发布AI使能平台解决方案 共筑AI产业新生态
  • Linux 桌面环境GNOME 49 释出
  • react/umi,浏览器tab设置
  • langchain-PipelinePromptTemplate
  • git 本地仓库与远程仓库链接
  • 绘想 - 百度推出的AI视频创作平台
  • 穿越像素的凝视:深度解析视频中的人物与动物识别算法技术
  • OpenHarmony 4.0 Release源码下载、编译及烧录
  • 大模型提示词Prompt工程:2-全攻略+最佳实践框架+原理解析+实战案例库+七招要诀
  • 大模型微调——Prompt-Tuning
  • code2prompt 快速生成项目 Markdown 文档(结合大模型进行问答)
  • UIKit-CAGradientLayer