【List篇】ArrayList 详解(含图示说明)
Java中的ArrayList是一个动态数组,可以自动扩展容量以适应数据的添加和删除。它可以用来存储各种类型的数据,例如String,Integer,Boolean等。ArrayList实现了List接口,可以进行常见的List操作,例如添加、插入、删除和访问元素等。
ArrayList具有以下特点:
有序的
:元素按照添加顺序排列。可重复的
:同一元素可以重复出现在不同位置。可变的
:可以动态调整数组大小。线程不安全
:在多线程环境下需要进行额外的同步措施。查询效率快
。这是由于底层的数据结构是基于Object 数组实现的,且数组在内存中是一块连续的空间,所以每次执行get方法获取元素时,可以通过索引 + 地址
的方式能够快速的在数组上的指定位置获取元素增删效率低
。在执行add方法时,可能存在扩容
,就需要生成一个新的数组,然后将旧数组中的元素复制到新数组中,最后将新增的元素放在数组上;执行remove方法时,将指定位置元素删除后,后面所有元素向左移
动一位;因为增删需要对数组的结构进行调整,所以效率低支持序列化
: 实现Serializable
标记性接口。支持克隆功能
: 实现Cloneable
标记性接口。支持随机访问功能
: 实现RandomAccess
标记性接口。也就是通过下标获取元素对象的功能- 继承
Iterable
接口,可以使用for-each
迭代
源码分析(JDK1.8)
成员变量属性
/*** 默认长度为10*/private static final int DEFAULT_CAPACITY = 10;/*** 用于空实例的共享空数组实例* 是为了优化创建ArrayList空实例时产生不必要的空数组,使所有的ArrayList空实例底* 层数据结构都指向同一个空数组* 例如:当构造参数是指定的长度,且为0 * 当构造参数是一个集合,且该参数集合中的元素个数为0*/private static final Object[] EMPTY_ELEMENTDATA = {};/*** 用于默认大小的空实例的共享空数组实例。* 将其与EMPTY_ELEMENTDATA区分开来,以了解添加第一个元素时要扩容多少* 用于标记当前ArrayList是使用空参构造的,在第一次使用add添加元素时,数组的最大长度直接使用默认的10*/private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** ★ 存储ArrayList元素的数组缓冲区,ArrayList的容量就是这个数组缓冲区的长度* 添加第一个元素时,任何elementData==DEFAULTCAPACITY_empty_elementData的空ArrayList* 都将扩展为DEFAULT_CAPACITY(默认初始容量10)*/transient Object[] elementData; /*** ★ 记录ArrayList 的元素个数*/private int size;/*** ★ 数组最大的长度*/private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造方法
ArrayList 有三个构造函数,空参构造方法,指定初始容量值构造方法和指定集合元素列表的构造方法,如果集合中的元素不为空,新建的ArrayList集合中的元素顺序就是构造参数中的元素顺序
- 空参构造
之前在网上看到一个说法" 使用空参构造一个ArrayList时,默认会创建一个容量为10的数组",这个说法其实不准确。在JDK1.8以前是这样的,但是为了节省内存资源,在JDK1.8版本之后,使用空参构造方法,只会创建一个空底层数据结构长度为0的空数组结构,当第一次执行add添加元素时,才会将数组容量扩充到默认10
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}
- 指定初始容量值构造
如果在构建ArrayList之前,就能明确元素的个数,那么就可以在构造ArrayList时,指定容量大小,这样就可以节省因扩容而造成的资源浪费
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {//当指定参数大于0时,真实存储数据的数据长度就是指定值this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {//当指定参数等于0时,使用空数组this.elementData = EMPTY_ELEMENTDATA;} else {//指定的长度参数不能小于0throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}}
- 构造参数是一个集合,并按照参数集合中元素个数和顺序返回新的ArrayList
public ArrayList(Collection<? extends E> c) {Object[] a = c.toArray();if ((size = a.length) != 0) {if (c.getClass() == ArrayList.class) {//如果构造参数集合的类型是ArrayList,那么构造参数的数组结构直接使用elementData = a;} else {//如果构造参数集合的类型不是ArrayList//先初始化的数组长度 = 构造参数集合元素个数//再将构造参数集合中的数据信息,copy到新的数组中elementData = Arrays.copyOf(a, size, Object[].class);}} else {//如果构造参数elementData = EMPTY_ELEMENTDATA;}}
ArrayList 的增删查
add(), 插入元素方法
ArrayList 的add方法有两个,尾部插入
和指定位置插入
- 末尾添加
顾名思义就是直接在数组中最后一个元素的下一个位置上存放新添的元素,在添加元素之前,会校验数组容量是否已经到达临界值,如果到达临界值了,就先扩容,再将新元素添加到扩容后的新数组结构中的最后一个元素后面
public boolean add(E e) {//确定底层elementData的数组长度,且校验是否需要扩容//size + 1 可以理解为假设长度,后面会和当前数组的实际长度相比较,以判断是否需要扩容ensureCapacityInternal(size + 1); // ★ 扩容核心代码//添加元素,实际上就是赋值的操作elementData[size++] = e;return true;
}
- 指定位置插入
也叫插队添加,会打乱原本数组中元素的存储顺序。大致步骤如下:
1.校验数组是否有足够的空间
2.如果空间足够,直接将index及其之后的所有元素向后移动一位
3.如果空间不够,那么先进行扩容,扩容后的新数组长度是旧数组长度的1.5倍,然后将index位置之前的元素全部等位copy到新数组中,index之后的元素全部以index + 1 的形式偏移一位copy到新数组中
public void add(int index, E element) {//校验参数index值是否大于数组的长度rangeCheckForAdd(index);//确定底层elementData的数组长度,且校验是否需要扩容//size + 1 可以理解为假设长度,后面会和当前数组的实际长度相比较,以判断是否需要扩容ensureCapacityInternal(size + 1); //指定位置后面的元素,全部copy一份,并向右移动一位,以便于空出位置System.arraycopy(elementData, index, elementData, index + 1,size - index);//最后对指定位置赋值elementData[index] = element;size++;
}
扩容机制
上面有描述,ArrayList底层是一个动态数组
,何为动态?就是在ArrayList每次在执行add方法时,都会先计算一下,假设当前元素添加成功后,数组的长度是否已经超过实际长度,如果超过,那么就自动进行扩容
。
简单看一下扩容机制源码:
/*** 计算数组长度*/
private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//这里可以看到,当使用空参或者指定长度参数,指定构造参数集合元素为0时//计算结果:此处所需的最小数组长度为10return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;
}private void ensureExplicitCapacity(int minCapacity) {modCount++; //记录数组长度修改次数// overflow-conscious codeif (minCapacity - elementData.length > 0)//当所需最小数组长度 大于 当前实际长度时,进行扩容grow(minCapacity); //★ 核心扩容机制
}/*** 扩容机制*/private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length; //扩容前的数组长度//扩容后的新数组长度 = 扩容前的数组长度 * 1.5int 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://按照新的长度创建一个新数组,然后将原数组的数据copy到新数组中,并将add的元素加入到数组中elementData = Arrays.copyOf(elementData, newCapacity);}
remove(), 删除元素方法
ArrayList 的remove方法有两个,指定元素删除
和指定位置删除
;
- 指定元素删除
由于ArrayList 允许元素重复,所以指定元素删除方法可能存在删除多个。
public boolean remove(Object o) {if (o == null) {//如果指定删除的元素是null,那么循环去校验是由有元素为null//如果存在为null的元素,那么就匹配上,然后在内存中新开辟一个数组空间,将旧数组上,//从匹配上位置左边全部 按原index位置copy到新数组中,右边的全部按照index -1 的位置copy到新数组中//匹配几次,就需要重新修改几次数组的数组结构for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}} else {//如果指定删除的元素不是null,那么循环去校验去匹配//同理for (int index = 0; index < size; index++)if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false;
}/** 快速删除方法*/
private void fastRemove(int index) {modCount++; //记录数组结构修改的次数int numMoved = size - index - 1; //统计需要移动的元素个数if (numMoved > 0)//参数1:elementData 旧数组//参数2:index+1 源数组起点//参数3:elementData 新数组//参数4:index 新数组起点//参数5:numMoved 复制多少位System.arraycopy(elementData, index+1, elementData, index,numMoved);//数组中的最后一位置为null,地址引用删除,方便GC清除 elementData[--size] = null;
}
- 指定位置删除
最多只会删除一个元素,如果指定位置index超出数组结构长度,报错IndexOutOfBoundsException
public E remove(int index) {//校验指定index是否超出数组结构长度rangeCheck(index);modCount++; //记录数组结构发生调整的次数//通过指定索引index,获取数组元素信息E oldValue = elementData(index);//统计需要移动的元素个数int numMoved = size - index - 1;if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index,numMoved);//数组中的最后一位置为null,地址引用删除,方便GC清除 elementData[--size] = null; // clear to let GC do its workreturn oldValue;}/*** 校验指定index是否超出数组结构长度*/private void rangeCheck(int index) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}
ArrayList在执行remove()方法做删除之后,数组中可能会出现大量的空闲空间。而ArrayList没有自动缩容机制,导致底层大量的空闲空间不能被释放,造成内存浪费。对于这种场景,ArrayList也提供了相应的处理方法,如下:
/*** 将此ArrayList实例的容量修改为列表的当前大小。*/public void trimToSize() {//记录数组结构发生调整的次数modCount++;//size 是当前数组中的元素个数//当元素个数小于数组实际长度时,做缩容处理; 比例按照元素个数进行缩容if (size < elementData.length) {//实际元素个数为0,直接将数组置为空数组elementData = (size == 0)? EMPTY_ELEMENTDATA: Arrays.copyOf(elementData, size); //生成新数组, 长度 = 元素个数,将元素copy到新数组中}}
ArrayList 的增删方法总结, 为什么ArrayList是增删慢的?
从上文介绍可知, add() 方法会存在扩容场景, remove() 会存在移动元素的场景, 这些都会对性能产生很大的影响。用时间复杂度来表示是O(n),所以增删效率低
get(), 取元素方法
ArrayList 的get方法只有一个,只能通过索引下标index获取
public E get(int index) {//校验指定index是否超出数组结构长度rangeCheck(index);//直接通过索引index获取指定位置的value值return elementData(index);
}/*** 校验index的有效性* index 不能超过数组元素的个数,否则会报数组越界异常*/
private void rangeCheck(int index) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}
首先数组是存在堆内存中,一块连续的内存空间,并且ArrayList的底层是Object[]数组, 那么数组中的所有元素所占用的字节大小是一致的,所以想要从堆内存中获取元素信息,就必须知道index位置元素在堆内存中的内存地址。数组上元素的内存地址,主要就是看index下标对应的偏移量
,这里就需要计算:
公式: 数组上元素的内存地址 = 数组的起始地址 + index * 元素的大小(单位是字节,)
计算出index对应的元素内存地址后, 即可获取到对应数组上元素信息, 而数组中存放的是元素实际数据信息的内存地址,再通过此地址获取到实际元素数据信息
由于get()取元素方法,是通过下标index 精准定位,继而获取元素信息的, 这期间没有所谓的扩容,copy,迁移等等场景, 用时间复杂度来表示是O(1),所以效率高。
set(), 修改元素方法
ArrayList 中的修改方法只有一个,通过index下标来精准修改, 修改元素信息的步骤,其实就是同一个位置的信息覆盖操作
public E set(int index, E element) {//校验指定index是否超出数组结构长度rangeCheck(index);//通过index,获取旧的元素信息E oldValue = elementData(index);//然后覆盖替换elementData[index] = element;return oldValue;}/*** 校验index的有效性* index 不能超过数组元素的个数,否则会报数组越界异常*/
private void rangeCheck(int index) {if (index >= size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}
相关文章:

【List篇】ArrayList 详解(含图示说明)
Java中的ArrayList是一个动态数组,可以自动扩展容量以适应数据的添加和删除。它可以用来存储各种类型的数据,例如String,Integer,Boolean等。ArrayList实现了List接口,可以进行常见的List操作,例如添加、插…...

SSL证书只有收费的吗?有没有免费使用的?
首先明白SSL证书是什么SSL英文全称:英文全称: Secure Socket Layer Certificate,中文全称:安全套接字层证书。 SSL是一种由数字证书颁发机构(CA) 签发的数字证书。它用于建立安全的加密连接,确保通过网络传输的数据在客户端和服务器之间的安全性和完整性…...
48V轻混技术
文章目录 48V轻混技术的主要特点和优势48V轻混技术的优缺点优点:缺点: 48V轻混技术的主要特点和优势 48V轻混技术(48V Mild Hybrid Technology)是一种汽车动力系统技术,它结合了内燃机和电动机的优势,以提…...

机器学习基础算法--回归类型和评价分析
目录 1.数据归一化处理 2.数据标准化处理 3.Lasso回归模型 4.岭回归模型 5.评价指标计算 1.数据归一化处理 """ x的归一化的方法还是比较多的我们就选取最为基本的归一化方法 x(x-x_min)/(x_max-x_min) """ import numpy as np from sklea…...
MATLAB 软件功能简介
MATLAB 的名称源自 Matrix Laboratory,1984 年由美国 Mathworks 公司推向市场。 它是一种科学计算软件,专门以矩阵的形式处理数据。MATLAB 将高性能的数值计算和可 视化集成在一起,并提供了大量的内置函数,从而被广泛的应用于科学计算、控制…...

deepfm内容理解
对于CTR问题,被证明的最有效的提升任务表现的策略是特征组合(Feature Interaction); 两个问题: 如何更好地学习特征组合,进而更加精确地描述数据的特点; 如何更高效的学习特征组合。 DNN局限 :当我们使…...

postgresql-集合运算
postgresql-集合运算 并集交集差集集合运算符的优先级 并集 create table excellent_emp( year int not null, emp_id integer not null, constraint pk_excellent_emp primary key(year,emp_id) );insert into excellent_emp values(2018,9); insert into excellent_emp value…...
[持续更新]计算机经典面试题基础篇Day2
[通用]计算机经典面试题基础篇Day2 1、单例模式是什么,线程安全吗 单例模式是一种设计模式,旨在确保一个类只有一个实例,并提供全局访问点。通过使用单例模式,可以避免多次创建相同的对象,节省内存资源,同…...

C++:类和对象(二)
本文主要介绍:构造函数、析构函数、拷贝构造函数、赋值运算符重载、const成员函数、取地址及const取地址操作符重载。 目录 一、类的六个默认成员函数 二、构造函数 1.概念 2.特性 三、析构函数 1.概念 2.特性 四、拷贝构造函数 1.概念 2.特征 五、赋值…...

Java“牵手”京东商品详情数据,京东商品详情API接口,京东API接口申请指南
京东平台商品详情接口是开放平台提供的一种API接口,通过调用API接口,开发者可以获取京东商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片等详细信息 。 获取商品详情接口API是一种用于获取电商平台上商品详情数据的接口,通过…...
Fluidd摄像头公网无法正常显示修复一例
Fluidd摄像头在内网正常显示,公网一直无法显示,经过排查发现由于url加了端口号引起的,摄像头url中正常填写的是/webcam?actionsnapshot,或者/webcam?actionstream。但是由于nginx跳转机制,会被301跳转到/webcam/?ac…...

【C++ 学习 ⑳】- 详解二叉搜索树
目录 一、概念 二、实现 2.1 - BST.h 2.2 - test.cpp 三、应用 四、性能分析 一、概念 二叉搜索树(BST,Binary Search Tree),又称二叉排序树或二叉查找树。 二叉搜索树是一棵二叉树,可以为空;如果不…...

Java中网络的基本介绍。网络通信,网络,ip地址,域名,端口,网络通信协议,TCP/IP传输过程,网络通信协议模型,TCP协议,UDP协议
- 网络通信 概念:网络通信是指通过计算机网络进行信息传输的过程,包括数据传输、语音通话、视频会议等。在网络通信中,数据被分成一系列的数据包,并通过网络传输到目的地。在数据传输过程中,需要确保数据的完整性、准…...
【Qt】总体把握文本编码问题
在项目开发中,经常会遇到文本编码问题。文本编码知识非常基础,但对于新手来说,可能需要花费较长的时间去尝试,才能在脑海中建立对编码的正确认知。文本编码原理并不难,难的是在项目实践中掌握正确处理文本编码的方法。…...
Linux命令(77)之curl
linux命令之curl 1.curl介绍 linux命令之curl是一款强大的http命令行工具,它支持文件的上传和下载,是综合传输工具。 2.curl用法 curl [参数] [url] curl参数 参数说明-C断点续传-o <filename>把输出写到filename文件中-x在给定的端口上使用HT…...
详解 sudo usermod -aG docker majn
这个命令涉及到几个 Linux 系统管理的基础概念,包括 sudo、usermod 和用户组管理。我们可以逐一地解析它们: sudo: sudo(superuser do)允许一个已经被授权的用户以超级用户或其他用户的身份执行一个命令。当使用 sudo 前缀一个命令…...

大数据课程L2——网站流量项目的算法分析数据处理
文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解网站流量项目的算法分析; ⚪ 了解网站流量项目的数据处理; 一、项目的算法分析 1. 概述 网站流量统计是改进网站服务的重要手段之一,通过获取用户在网站的行为,可以分析出哪些内…...

jar包或exe程序设置为windows服务
最近在使用java和python制作客户端时突发奇想,是否能够通过一种方法来讲jar包和exe程序打包成windows服务呢?简单了解了一下是可以的。 首先要用到的是winSW,制作windows服务的过程非常简单,仅需几步制作完成,也不需要…...

数据结构--- 树
(一)知识补充 定义 树是一种数据结构,它是由n(n≥0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。 它具有以下的特点: 每个节点有零个或多个子节点; 没有父节点的节点称为根节点;每一个非根…...

两个pdf文件合并为一个怎么操作?分享pdf合并操作步骤
不管是初入职场的小白,还是久经职场的高手,都必须深入了解pdf,特别是关于pdf的各种操作,如编辑、合并、压缩等操作,其中合并是这么多操作里面必需懂的技能之一,但是很多人还是不知道两个pdf文件合并为一个怎…...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 
前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
Rust 异步编程
Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...

使用 SymPy 进行向量和矩阵的高级操作
在科学计算和工程领域,向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能,能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作,并通过具体…...

uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...