Java基础 ArrayList源码分析 JDK1.8
一、概述
本篇文章记录通过阅读JDK1.8 ArrayList源码,结合自身理解分析其实现原理。
ArrayList容器类的使用频率十分频繁,它具有以下特性:
- 其本质是一个数组,因此它是有序集合
- 通过 get(int i) 下标获取数组的指定元素时,时间复杂度是O(1)
- 通过 add(E e)插入元素时,可直接向当前数组最后一个位置插入(这个描述不是特别准确,其中涉及到扩容、后续将讲解),其时间复杂度为O(1)
- 通过 add(int i, E e)向指定位置插入元素时,是在原数组的基础上通过拷贝和偏移量实现,其时间复杂度为O(n)
- 通过 remove(int i) 删除指定位置的元素时,同样也是在原数组的基础上通过拷贝和偏移量实现,其时间复杂度为O(n)
- 使用内部类Itr()迭代删除ArrayList删除元素时,需使用迭代器的remove()方法,不能使用ArrayList.remove(Object o)方法
- ArrayList是非线程安全的,可能造成数据丢失,数组越界的问题
二、源码分析
1.重要属性
private static final int DEFAULT_CAPACITY = 10; // 扩容因子默认为10 transient Object[] elementData; // 元素数据 private int size; // 当前ArrayList中实际元素数量 protected transient int modCount = 0; // ArrayList被修改的次数,这个属性是从java.util.AbstractList继承下来的
2.重要操作
构造器
/*
* 无参构造器 将elementData初始化为一个空的数组
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
} /*
* int整形构造器,将elementData初始化为指定大小的数组
*/
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);
}
}
boolean add(E e) 添加一个元素
将添加数组分解成以下5个步骤
// 添加一个元素时 必须确保当前数组可以添加一个新的元素,因此会根据capacity去计算
public boolean add(E e) {
ensureCapacityInternal(size + 1); // ⑤ 将新元素添加到 扩容之后的数组size++坐标上,(注意是size++,先赋值再自增size)
elementData[size++] = e;
return true;
} private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
} // ① 计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
/* 如果初始化ArrayList时 是使用无参构造 new ArrayList()
那么第一次向ArrayList添加元素时会将容量设为DEFAULT_CAPACITY 10个*/
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
} // ② 确认是否扩容
private void ensureExplicitCapacity(int minCapacity) {
// ArrayList被修改次数 + 1
modCount++; /*
如果计算出来的容量 > elementData数组的长度时,那么会要扩容
因为elementData数组放不下新元素了;
否则的话就不需要扩容
*/
if (minCapacity - elementData.length > 0)
grow(minCapacity);
} // ③ 扩容
private void grow(int minCapacity) { // 新数组容量 = 老数组容量 * 1.5
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
/* 但是必须要要保证 新容量 > ①中【计算出来的容量】 ,否则依然放不下新元素
如果出现这种情况,那么使用①中【计算出的容量】最为新数组容量,保证能放下元素 */
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 数组元素过多 使用Integer.MAX_VALUE 作为容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity); // ④ 通过拷贝的方式扩容 调用native层方法
elementData = Arrays.copyOf(elementData, newCapacity);
}
举个例子
// 往ArrayList中插入11个元素
List<Object> list = new ArrayList<>();
for (int i = 0; i < 11; i++) {
list.add(new Object());
}
上述代码很简单,步骤为:
- 调用无参构造器,初始化一个ArrayList
- 循环向该ArrayList插入11个元素
以该步骤为例:
List<Object> list = new ArrayList<>(); // 初始化elementData为一个空数组{} , elementData.length = 0
i = 0 时
list.add(new Object());
这个操作会经历上面代码片段的①②③④⑤步骤
- 在走步骤①时使用默认容量 10
- 走步骤②时 判断10 > elementData.length 因此使用10作为最小容量请求扩容
- 走步骤③时 elementData.length 扩大1.5倍后依然为0 因此使用10作为新容量
- 通过步骤④native层拷贝方法进行数组拷贝
- 通过步骤⑤在新数组的size++ (即第个0个坐标上),并且size自增为1

当 1 ≤ i ≤ 9时
list.add(new Object());
在步骤①算计容量时,minCapacity依次计算出来为 1, 2, 3, 4, 5, 6, 7, 8, 9 都小于elementData.length(当前经历了第一次扩容 为10) ,因此不会考虑扩容,直接将新增元素放到指定的下标中
ps: 此步骤modCount每次都会+1
当 i = 9时, 数组被填充满

当 i = 10 时
list.add(new Object());
此时就不能将第11个元素放到数组中了,需要第二次扩容
- 执行代码片段步骤①,算出minCapacity=11
- 步骤②判断出minCapacity(11) > elementData.length(10),因此需要扩容
- 步骤③,通过在elementData.length基础上扩大1.5倍 即 15, 同时大于 minCapacity, 因此使用15作为新容量
- 步骤④,以15作为新数组容量,拷贝原数组到新数组中
- 步骤⑤,将新的元素放到扩容之后新数组下标10的位置中,并且size自增

可以看到,上述操作过程中list经历了两次扩容,因此在使用ArrayList的时候可以考虑使用有参构造器,确认ArrayList的大小,防止扩容发生,影响效率
add(int i, E e) 向指定位置添加一个元素
public void add(int index, E element) {
// 检查下标是否越界
rangeCheckForAdd(index);
// 计算容量 考虑是否扩容
ensureCapacityInternal(size + 1);
// 通过拷贝数组的方式插入
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
可以看到向指定位置插入元素和add(E e)方法步骤差不多,只是拷贝方式做了改变
1. 不发生扩容时插入过程

2. 发生扩容时插入过程

E remove(int index) 根据下标删除元素
public E remove(int index) {
// 检查索引是否越界
rangeCheck(index);
// 修改次数 +1
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;
}

删除元素时,是没有缩容操作的,例如一个ArrayList中elementData仅有10个元素并且存满了,remove掉9个元素后,size大小是1,但是elementData.length依旧为10
void trimToSize() 修整大小
// 这个方法可以以当前elementData数组的size实际大小 重新拷贝出一个数组
// 防止上面问题的发生
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
E get(int index) 从指定索引获取元素
// 获取指定元素
public E get(int index) {
rangeCheck(index); return elementData(index);
} E elementData(int index) {
return (E) elementData[index];
}
这个方法很简单,检查完索引范围,直接从elementData数组上去取对应位置的元素
boolean contains(Object o) 查询ArrayList中是否包含某个元素
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
// 遍历elementData数组 查询满足条件的下标
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;
}
可以看到查询ArrayList中是否包含某个元素时,是通过遍历整个数组,依次查询,因此该方法时间复杂度为O(n)
Iterator<E> iterator() 迭代
public Iterator<E> iterator() {
return new Itr();
}
// 内部类 迭代器
private class Itr implements Iterator<E> {
int cursor; // 返回数据的游标
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount; // 迭代器期望的被修改的次数 与ArrayList.modCount相同,
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
// ① expectedModCount != modCount 时抛出ConcurrentModificationException
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
// 通过迭代器进行删除,每删除一个元素时expectedModCount会被重新赋值
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
前文中多次提到ArrayList的属性modCount,内部类Itr中提供的remove方法,在删除元素成功后,会将expectedModCount进行更新,因此,通过迭代器迭代ArrayList时,不能使用ArrayList.remove(Object o)方法,如果直接使用ArrayList.remove(Object o)进行删除(这个方法会对modCount减1),会导致迭代器进行下一次迭代时调用next()方法 检查到expectedModCount与modCount不同(上述代码①处),抛出ConcurrentModificationException,下面的代码示例将展示正确与错误在迭代中删除元素的操作
// 正确操作
List<Integer> list = new ArrayList<>();
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
iterator.next(); // next()操作会更新游标cursor
iterator.remove();
} // 错误操作:将抛出ConcurrentModificationException
List<Integer> list = new ArrayList<>();
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
list.remove(next);
}
3.线程安全问题(非线程安全)
/* size++非原子操作 elementData[size++] = e 等同于
elementData[size] = e;
size = size + 1 */
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
ArrayList是非线程安全的,原因是在add操作时,由于size++是非原子操作,多个线程去对同一ArrayList执行add(E e)方法时,会出现下面两种情况
1.情况1:当两个线程同时进入add()方法,读取到当前size为0,并都往0的下标存元素时,就会导致其中的一个值被覆盖掉的问题
2.情况2:假设当前ArrayList为elementData[10],可存放10个元素,并且当前已经存了9个元素了,那么此时size=9
| 线程A | 线程B |
| add(e1) | add(e2) |
| 读到size为0 | 读到size也为0 |
| elementData[0] = e1 | |
| elementData[0] = e2 |
| 线程A | 线程B |
| add(e1) | add(e2) |
| 读到size为9 不需要扩容 | 读到size为9 不需要扩容 |
| elementData[9] = e1 | |
|
size=size+1//size = 10 |
|
|
elementData[10] = e2 // 由于没有扩容导致数组越界 |
|
| ... | 抛出异常了 |
Java基础 ArrayList源码分析 JDK1.8的更多相关文章
- ArrayList源码分析--jdk1.8
ArrayList概述 1. ArrayList是可以动态扩容和动态删除冗余容量的索引序列,基于数组实现的集合. 2. ArrayList支持随机访问.克隆.序列化,元素有序且可以重复. 3. ...
- Java中ArrayList源码分析
一.简介 ArrayList是一个数组队列,相当于动态数组.每个ArrayList实例都有自己的容量,该容量至少和所存储数据的个数一样大小,在每次添加数据时,它会使用ensureCapacity()保 ...
- Java集合-ArrayList源码解析-JDK1.8
◆ ArrayList简介 ◆ ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了List, RandomAcc ...
- 【thinking in java】ArrayList源码分析
简介 ArrayList底层是数组实现的,可以自增扩容的数组,此外它是非线程安全的,一般多用于单线程环境下(Vector是线程安全的,所以ArrayList 性能相对Vector 会好些) Array ...
- Java基础—ArrayList源码浅析
注:以下源码均为JDK8的源码 一. 核心属性 基本属性如下: 核心的属性其实是红框中的两个: //从注释也容易看出,一个是集合元素,一个是集合长度(注意是逻辑长度,即元素的个数,而非数组长度) 其中 ...
- Java基础——HashTable源码分析
HashTable是基于哈希表的Map接口的同步实现 HashTable中元素的key是唯一的,value值可重复 HashTable中元素的key和value不允许为null,如果遇到null,则返 ...
- Java集合-ArrayList源码分析
目录 1.结构特性 2.构造函数 3.成员变量 4.常用的成员方法 5.底层数组扩容原理 6.序列化原理 7.集合元素排序 8.迭代器的实现 9.总结 1.结构特性 Java ArrayList类使用 ...
- java.util.ArrayList源码分析
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess ...
- ReentrantLock源码分析--jdk1.8
JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分 ...
随机推荐
- Java之微信公众号开发
这次以文本回复作为案例来讲解Java相关得微信公众号开发. 首先必须要有一个个人微信公众号 个人微信公众号相关的接口权限有限,不过用于个人学习体验一下足够了,如图: 然后进入微信公众后台,点击基本配置 ...
- 算法学习之剑指offer(五)
题目1 题目描述 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果.如果是则输出Yes,否则输出No.假设输入的数组的任意两个数字都互不相同. public class Solution ...
- 帝国cms网站管理系统之安全设置最优化分享
首先我们来认识一下帝国CMS安全认证特性:帝国登录四重安全验证:第一重:密码双重MD5加密,密码不可破解,假如数据库被下载,也无法获取真实密码.第二重:后台目录自定义,假如对方知道密码也找不到登录后台 ...
- 自学web前端达到什么水平,才能满足求职的标准?
大多数野生程序员最棘手的问题就是如何依靠技术解决温饱,通俗来讲就是技术折现的问题. 如果是单纯出于兴趣,或者只是为了突击某一阶段或者某一项目技术壁垒,不跟就业挂钩的自学倒也是无关痛痒.但是当上岗成为自 ...
- 无 PowerShell.exe 执行 Empire 的几种姿势
在实战中,Empire成为域渗透.后渗透阶段一大利器,而Empire是一个Powershell RAT,所以PowerShell必须要能运行Empire中几乎所有的启动方法都依赖于使用PowerShe ...
- CTF-SSH服务渗透
环境 Kali ip 192.168.56.102 Smb 靶机ip 192.168.56.101 0x01信息探测 首页发现有类似用户名的信息 先记录下来 Martin N Hadi M Jimmy ...
- [Luogu4550] 收集邮票
题目描述 有n种不同的邮票,皮皮想收集所有种类的邮票.唯一的收集方法是到同学凡凡那里购买,每次只能买一张,并且买到的邮票究竟是n种邮票中的哪一种是等概率的,概率均为1/n.但是由于凡凡也很喜欢邮票,所 ...
- kaldi使用cvte模型进行语音识别
操作系统 : Unbutu18.04_x64 gcc版本 :7.4.0 该模型在thch30数据集上测试的错误率只有8.25%,效果还是不错的. 模型下载地址: http://www.kaldi-as ...
- 子字符串查找之————关于KMP算法你不知道的事
写在前面: (阅读本文前需要了解KMP算法的基本思路.另外,本着大道至简的思想,本文的所有例子都会做从头到尾的讲解) 作者翻阅了大量网上现有的KMP算法博客,发现广为流传的竟然是一种不完整的KMP算法 ...
- day34作业
作业 查看岗位是teacher的员工姓名.年龄 select name,age from teacher where post='teacher'; 查看岗位是teacher且年龄大于30岁的员工姓名 ...