【08期】ArrayList常见面试题
简介
ArrayList是我们开发中非常常用的数据存储容器之一,其底层是数组实现的,我们可以在集合中存储任意类型的数据,ArrayList是线程不安全的,非常适合用于对元素进行查找,效率非常高。
线程安全性
对ArrayList的操作一般分为两个步骤,改变位置(size)和操作元素(e)。所以这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的。
源码分析
1. 属性分析
/**
* 默认初始化容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 如果自定义容量为0,则会默认用它来初始化ArrayList。或者用于空数组替换。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 如果没有自定义容量,则会使用它来初始化ArrayList。或者用于空数组比对。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 这就是ArrayList底层用到的数组
* 非私有,以简化嵌套类访问
* transient 在已经实现序列化的类中,不允许某变量序列化
*/
transient Object[] elementData;
/**
* 实际ArrayList集合大小
*/
private int size;
/**
* 可分配的最大容量
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
扩展:什么是序列化
序列化是指:将对象转换成以字节序列的形式来表示,以便用于持久化和传输。
实现方法:实现Serializable接口。
然后用的时候拿出来进行反序列化即可又变成Java对象。
transient关键字解析
Java中transient关键字的作用,简单地说,就是让某些被修饰的成员属性变量不被序列化。
有了transient关键字声明,则这个变量不会参与序列化操作,即使所在类实现了Serializable接口,反序列化后该变量为空值。
那么问题来了:ArrayList中数组声明:
transient Object[] elementData;,事实上我们使用ArrayList在网络传输用的很正常,并没有出现空值。
原来:ArrayList在序列化的时候会调用writeObject()方法,将size和element写入ObjectOutputStream;反序列化时调用readObject(),从ObjectInputStream获取size和element,再恢复到elementData。
那为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?
原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
2. 构造方法分析
根据initialCapacity 初始化一个空数组,如果值为0,则初始化一个空数组:
/**
* 根据initialCapacity 初始化一个空数组
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
不带参数初始化,默认容量为10:
/**
* 不带参数初始化,默认容量为10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
通过集合做参数的形式初始化:如果集合为空,则初始化为空数组:
/**
* 通过集合做参数的形式初始化
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
3. 主干方法
trimToSize()方法:
用来最小化实例存储,将容器大小调整为当前元素所占用的容量大小。
/**
* 这个方法用来最小化实例存储。
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
clone()方法
用来克隆出一个新数组。
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
通过调用Object的clone()方法来得到一个新的ArrayList对象,然后将elementData复制给该对象并返回。
add(E e)方法
在数组末尾添加元素
/**
* 在数组末尾添加元素
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
看到它首先调用了ensureCapacityInternal()方法.注意参数是size+1,这是个面试考点。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
这个方法里又嵌套调用了两个方法:计算容量+确保容量
计算容量:如果elementData是空,则返回默认容量10和size+1的最大值,否则返回size+1
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
计算完容量后,进行确保容量可用:(modCount不用理它,它用来计算修改次数)
如果size+1 > elementData.length证明数组已经放满,则增加容量,调用grow()。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
增加容量:默认1.5倍扩容。
-
获取当前数组长度=>oldCapacity -
oldCapacity>>1 表示将oldCapacity右移一位(位运算),相当于除2。再加上1,相当于新容量扩容1.5倍。 -
如果 newCapacity>1=1,1<2所以如果不处理该情况,扩容将不能正确完成。 -
如果新容量比最大值还要大,则将新容量赋值为VM要求最大值。 -
将elementData拷贝到一个新的容量中。
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);
}
size+1的问题
好了,那到这里可以说一下为什么要size+1。
size+1代表的含义是:
-
如果集合添加元素成功后,集合中的实际元素个数。 -
为了确保扩容不会出现错误。
假如不加一处理,如果默认size是0,则0+0>>1还是0。 如果size是1,则1+1>>1还是1。有人问:不是默认容量大小是10吗?事实上,jdk1.8版本以后,ArrayList的扩容放在add()方法中。之前放在构造方法中。我用的是1.8版本,所以默认ArrayList arrayList = new ArrayList();后,size应该是0.所以,size+1对扩容来讲很必要.
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
System.out.println(arrayList.size());
}
输出:0
事实上上面的代码是证明不了容量大小的,因为size只会在调用add()方法时才会自增。有办法的小伙伴可以在评论区大显神通。
add(int index, E element)方法
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
rangeCheckForAdd()是越界异常检测方法。ensureCapacityInternal()之前有讲,着重说一下System.arrayCopy方法:
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
代码解释:
-
Object src : 原数组 -
int srcPos : 从元数据的起始位置开始 -
Object dest : 目标数组 -
int destPos : 目标数组的开始起始位置 -
int length : 要copy的数组的长度
示例:size为6,我们调用add(2,element)方法,则会从index=2+1=3的位置开始,将数组元素替换为从index起始位置为index=2,长度为6-2=4的数据。
异常处理:
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
set(int index,E element)方法
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
E elementData(int index) {
return (E) elementData[index];
}
逻辑很简单,覆盖旧值并返回。
indexOf(Object o)方法
根据Object对象获取数组中的索引值。
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
如果o为空,则返回数组中第一个为空的索引;不为空也类似。
注意:通过源码可以看到,该方法是允许传空值进来的。
get(int index)方法
返回指定下标处的元素的值。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
rangeCheck(index)会检测index值是否合法,如果合法则返回索引对应的值。
remove(int index)方法
删除指定下标的元素。
public E remove(int index) {
// 检测index是否合法
rangeCheck(index);
// 数据结构修改次数
modCount++;
E oldValue = elementData(index);
// 记住这个算法
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
return oldValue;
}
这里又碰到了System.arraycopy()方法,详情请查阅上文。
大概思路:将该元素后面的元素前移,最后一个元素置空。
ArrayList优缺点
优点:
-
因为其底层是数组,所以修改和查询效率高。 -
可自动扩容(1.5倍)。
缺点:
-
插入和删除效率不高。 -
线程不安全。
手写ArrayList
那面试手写ArrayList应该就不是问题了。
下面贴出我手写的一个简单阉割版的ArrayList:
public class MyArrayList {
// 非私有,以简化嵌套类访问
// transient 在已经实现序列化的类中,不允许某变量序列化
transient Object[] elementData;
//默认容量
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的 空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 实际ArrayList集合大小
private int size;
/**
* 构造方法
*/
public MyArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public MyArrayList(){
this(DEFAULT_CAPACITY);
}
public void add(Object o){
//1. 判断数据容量是否大于 elementData
ensureExplicitCapacity(size+1);
//2. 使用下标进行赋值
elementData[size++] = o;
}
private void ensureExplicitCapacity(int minCapacity){
if (size == elementData.length){
// 需要扩容,扩容1.5倍(ArrayList默认扩容1.5倍)
// 注意:如果oldCapacity值为1
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量 < 最小容量, 则将最小容量赋值给新容量
// 如果 oldCapacity=1, 则 minCapacity=1+1=2 newCapacity=1+(1>>1)=1
if (newCapacity - minCapacity < 0){
newCapacity = minCapacity;
}
// 创建新数组
Object[] objects = new Object[newCapacity];
// 将数据复制给新数组
System.arraycopy(elementData, 0, objects, 0, elementData.length);
// 修改引用
elementData = objects;
}
}
public Object get(int index) {
rangeCheck(index);
return elementData[index];
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException("下标越界");
}
/**
* 通过下标删除
* @param index
* @return
*/
public Object remove(int index) {
rangeCheck(index);
// modCount++;
// 先查出元素
Object oldValue = elementData[index];
// 找出置换结束位置
int numMoved = size - index - 1;
if (numMoved > 0)
// 从 index+1 开始 将值覆盖为 index-numMoved 的值
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public boolean remove(Object o) {
for (int index = 0; index < size; index++){
if (o.equals(elementData[index])) {
remove(index);
return true;
}
}
return false;
}
}
本文由 mdnice 多平台发布
相关文章:
【08期】ArrayList常见面试题
简介 ArrayList是我们开发中非常常用的数据存储容器之一,其底层是数组实现的,我们可以在集合中存储任意类型的数据,ArrayList是线程不安全的,非常适合用于对元素进行查找,效率非常高。 线程安全性 对ArrayList的操作…...
Android studio之GridView使用
目录 效果图:代码: 效果图: 代码: UserGridviewAdapter package com.example.gridviewpro.Adapter;import android.content.Contex…...
Ubuntu系统环境搭建(七)——Ubuntu安装MySQL8.0
ubuntu环境搭建专栏🔗点击跳转 Ubuntu系统环境搭建(七)——Ubuntu安装MySQL8.0 文章目录 Ubuntu系统环境搭建(七)——Ubuntu安装MySQL8.01、安装1.1、下载1.2、解压安装 2、配置工作2.1、基本设置2.1.1、文件夹重命名…...
Nginx详解 三:高级配置
文章目录 1. 网页的状态页2. Nginx第三方模块2.1 echo模块 3. 变量3.1 内置变量3.1.1 示例 3.2 自定义变量3.2.1 自定义访问日志3.2.2 自定义json 格式日志 3.4 Nginx压缩功能 4. HTTPS4.1 Nginx的HTTPS工作原理4.2 启用功能模块的配置过程 5、自定义图标 1. 网页的状态页 基于…...
mysql 表备份 遇到的问题 【全网最全】
目录 省流: 正文: 1、报错 2、原因 3、解决方法 方法一:关闭 ENFORCE_GTID_CONSISTENCY (不推荐): 方法二(推荐): 4、开启关闭GTID 省流: 不推荐如…...
11.添加侧边栏,并导入数据
修改CommonAside的代码: <template><div><el-menu default-active"1-4-1" class"el-menu-vertical-demo" open"handleOpen" close"handleClose":collapse"isCollapse"><!--<el-menu-it…...
ThinkPHP 通用的API格式封装
ThinkPHP 通用的API格式封装 1.创建status.php 用于设置通用的状态码返回枚举类2.将API返回格式统一封装3.重写BaseController中的__call方法4.在控制器下面新建Error控制器,然后添加__call方法 1.创建status.php 用于设置通用的状态码返回枚举类 <?phpreturn[…...
自己动手写数据库:实现一个小型 SQL 解释器(下)
本节我们完成 SQL 解释器的最后一部分,它涉及到数据的删除和更改,首先我们看删除语句的解析。我们先看 delete 对应的语法: Delete -> DELETE FROM ID (where Predicate)?从语法规则可以看出,delete 语句必须以关键字 DELETE…...
2023年信息安全管理与评估任务书模块一网络平台搭建与设备安全防护
全国职业院校技能大赛 高等职业教育组 信息安全管理与评估 任务书 模块一 网络平台搭建与设备安全防护 比赛时间 本阶段比赛时长为180分钟。 赛项信息 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 第一阶段 网络平台搭建与设备安全防护 任务1 网络平台搭建 9:00- 12:00 …...
JS -RSA 明文加密--用户密码加密
1 配置文件引入 加密包 package.json "jsencrypt": "^3.0.0-rc.1",2 加密公钥配置 import { JSEncrypt } from jsencrypt import request from "/utils/request";const RSA_PUBLIC_KEY "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJVol0cJ…...
clickhouse中replacingMergeTree
ReplacingMergeTree是在MergeTree上添加了去重的功能,但是这个功能不可控,合并是一个后台的操作,除非手动触发,不然无法控制,并且它会删除具有相同(区内)主键的重复项。 特点: 1,去重时机不定&a…...
pdf怎么转换成word?
随着数字化时代的到来,PDF(Portable Document Format)已成为最受欢迎的文档格式之一,因其在各种设备上的可视性和稳定性而备受推崇。然而在某些情况下,将PDF转换为Word文档可能是必要的,这使得编辑、修改和重新格式化文本变得更加…...
汇编攻城记-Cortex-M3指令集
类型 指令 全称 功能 内存访问 LDR Load register 加载字到寄存器 LDRB 加载字节到寄存器 LDRH 加载半字到寄存器 LDRSH 加载半字到寄存器,再带符号扩展到32位 LDRD 从连续的地址空间加载双字(64位整数)到…...
大语言模型之五 谷歌Gemini
近十年来谷歌引领着人工智能方向的发展,从TensorFlow到TPU再到Transformer,都是谷歌在引领着,然而,在大语言模型上,却被ChatGPT(OpenAI)抢了风头,并且知道GPT-4(OpenAI&a…...
使用selenium实现对页面元素的抓取
一、背景介绍 工作中有个需求是需要对某个页面进行监控,但由于要监控页面数据是异步加载的,因此很难从状态码和返回结果层面进行校验。于是乎想到了通过判断页面元素是否存在且显示内容是否正确来达到此目标。调研了一下发现selenium可以实现对这种动态…...
大数据课程K12——Spark的MLlib概述
文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解Spark的MLlib概念; ⚪ 掌握Spark的MLlib基本数据模型; ⚪ 掌握Spark的MLlib统计量基础; 一、Spark MLlib介绍 1. 概述 MLlib是Apache Spark的可迭代机器学习库。 2. 易于使用 …...
流程制造智能工厂总体架构及建设路线规划方案PPT
本资料来源公开网络,仅供个人学习,请勿商用,如有侵权请联系删除,更多浏览公众号:智慧方案文库 数字孪生智能制造(智改数转)数字化架构设计及应用..水泥智能工厂解决方案.pptx智慧制造规划设计解决方案.pptx智能工厂落…...
网络有源号角(50W-100W)社区小区广播 工地语音播报,隧道广播,钢铁广播广播系统
网络有源号角(50W-100W)社区小区广播 工地语音播报,隧道广播,钢铁广播广播系统 SV-7042T 50W网络有源号角 SV-7042T是深圳锐科达电子有限公司的一款壁挂式网络有源号角,具有10/100M以太网接口,可将网络音…...
【Kali Linux高级渗透测试】深入剖析Kali Linux:高级渗透测试技术与实践
📕作者简介:热爱跑步的恒川,致力于C/C、Java、Python等多编程语言,热爱跑步,喜爱音乐的一位博主。 📗本文收录于恒川的日常汇报系列,大家有兴趣的可以看一看 📘相关专栏C语言初阶、C…...
DHCP中继实验
文章目录 一、实验背景与目的二、实验拓扑三、实验需求四、实验解法1. 配置IP地址2.配置R1为DHCP服务器,能够跨网段为192.168.2.0/24网段自动分配IP地址3. 在PC3上Ping 192.168.1.1,确认可以Ping通 摘要: 本实验旨在通过配置DHCP中继实现跨网…...
如何通过精细风扇控制优化Windows电脑的散热与静音体验
如何通过精细风扇控制优化Windows电脑的散热与静音体验 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trending/fa/FanContro…...
终极显卡风扇控制指南:用FanControl彻底解决NVIDIA风扇异常
终极显卡风扇控制指南:用FanControl彻底解决NVIDIA风扇异常 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trendi…...
如何在10分钟内搭建个人游戏云:Sunshine跨平台游戏串流终极指南
如何在10分钟内搭建个人游戏云:Sunshine跨平台游戏串流终极指南 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 想要在任何设备上畅玩PC游戏吗?厌倦了被硬件…...
人像抠图用什么软件好?2026年实测9款抠图工具制作方法对比
人像抠图(背景分离)是日常生活中的常见需求——换证件照背景、制作社交媒体头像、编辑产品图等场景都离不开它。今年人像抠图的工具选择已经非常丰富,从零基础用户到专业设计师都能找到趁手的方案。本文会详细对比9款主流人像抠图工具的制作方…...
书匠策AI拆解手册:它是怎么帮你把毕业论文从0拉到1的?
同学们好,我是你们的论文科普老司机。 今天不整虚的,直接拿一个工具开刀——书匠策AI( 官网直达:www.shujiangce.com,微信搜一搜"书匠策AI"即可关注公众号)。 这篇文章的目的只有一个ÿ…...
java篇12-Java中的异常
java中的异常是一个类,处理异常就是创建一个异常类对象并抛出这个对象,java处理异常的机制是中断,异常不是语法错了,语法错了编译不通过,不会产生字节码文件,不会运行,而异常是在运行过程中导致…...
抖音无水印下载器全解析:从零构建你的个人视频收藏库
抖音无水印下载器全解析:从零构建你的个人视频收藏库 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback suppor…...
群晖NAS远程SSH配置全解:从权限控制到独立模式实战
1. 为什么群晖的SSH不是“开个开关”就完事——从权限失控风险说起群晖NAS作为家用与小型办公场景中最普及的存储设备,很多人买来装好硬盘、配好共享文件夹,就觉得万事大吉。直到某天想批量处理照片缩略图、想用rsync做异地备份、想部署一个轻量级服务&a…...
yudao-cloud云原生权限安全深度剖析:OAuth2、JWT与Nacos风险实战
1. 这不是一次“走流程”的渗透测试,而是一次对云原生权限模型的实战压力测试“yudao-cloud渗透测试:安全风险发现与修复”——这个标题里藏着三个关键信号:yudao-cloud是一个真实落地的、基于 Spring Cloud Alibaba 的国产开源微服务管理平台…...
嵌入式工控机在AGV叉车中的核心应用与工程实践
1. 项目概述:当AGV叉车遇上嵌入式工控机在制造业和物流仓储领域,智能AGV(自动导引运输车)叉车早已不是什么新鲜概念。但真正深入到项目一线,你会发现,从“能跑起来”到“跑得稳、算得准、管得好”ÿ…...
