ArrayList源码解析--值得深读
ArrayList源码解析
基于jdk1.8
ArrayList的定义
类注释
- 允许put null值,会自动扩容;
- size isEmpty、get、set、add等方法时间复杂度是O(1);
- 是非线程安全的,多线程情况下推荐使用CopyOnWriteArrayList或者Vector。
- 增强for循环或使用迭代器过程中,如果数组大小被改变会抛出异常。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
从源码中我们可以看出,ArrayList继承了AbstractList,实现了List接口,RandomAccess,Cloneable,Serializable接口。
实现List接口提供了元素的添加、删除。修改。遍历等功能。
实现RandomAccess接口,他是一个标志性接口,表明实现这个接口后,List集合支持随机访问。
实现Cloneable接口表明ArrayList可以被克隆
实现Serializable接口表示ArrayList支持序列化,能被传输。
ArrayList是一个动态数组,与普通数组相比他的容量可以动态的扩容。
ArrayList的有关属性
private static final long serialVersionUID = 8683452581122892189L;
/*默认容量大小是10*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 用来共享空数组实例
* 我们把它和EMPTY_ELEMENTDATA数组区分出来,当添加第一个元素的时候知道需要扩增多少容量
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 保存ArrayList数据的数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 集合包含元素的个数
*/
private int size;
ArrayList的构造函数
public ArrayList(int initialCapacity) {
// 如果创建的时候传入的有值,并且大于0,就直接创建大小是传入的值的数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果创建的时候没有指定大小,就是一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//传入负数会抛出非法参数异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 构造一个空数组,当增加第一个元素的时候才确定数组容量是10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
*构造函数的参数是一个集合,如果集合为null,会抛空指针异常
*
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
//如果集合的大小不为0 就通过Arrays.copy方法,把Object数组的元素一一复制到elementData数组中
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
// 注意这是java的一个bug,c.toArray()方法返回值可能不是Object数组,这里转成Object类型
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//如果集合里没有元素就是一个空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
上面的那个// c.toArray might (incorrectly) not return Object[] (see 6260652)
一般情况下都不会触发这个bug ,这里演示一下是怎么出现的。
public void testConstructor() {
List<String> arrayList = Arrays.asList("Hello");
//toArray方法返回Object数组类型
Object[] objects = arrayList.toArray();
log.info(objects.getClass().getSimpleName());
// 打印出来是 String[]
//这样写是对的
//objects[0]="Java";
//这样就会报错因为数组元素的类型是String
objects[0] = new Object();
}
不过这个bug已经在Java9解决。
新增和扩容的实现
新增元素
/**
* Appends the specified element to the end of this list.
*增加元素到集合的末尾
*
*/
public boolean add(E e) {
//先扩容,使容量+1
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
/**
* 在指定位置增加元素
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
//检查增加的位置是否越界
rangeCheckForAdd(index);
//扩容,size+1
ensureCapacityInternal(size + 1); // Increments modCount!!
//元素的复制,将 index之后的元素往后 移动一位,size++;
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//最后在index位置上赋值
elementData[index] = element;
size++;
}
扩容
private void ensureCapacityInternal(int minCapacity) {
//确保容积足够
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果出事容量大小有给定的值就一指定的值为准
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
//记录数组被修改的次数加1
modCount++;
// 如果我们期望的数组大小大于目前的数组长度,那么就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容方法
private void grow(int minCapacity) {
// 先记录旧的数组大小
int oldCapacity = elementData.length;
//扩容后的容量=扩容前的+扩容前的大小/2,也就是说扩容后的值是原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容后的值,小于期望的大小,那扩容后的值就改为期望值
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果扩容后的值大于jvm所能分配的最大值,那么就用Integer.MAX_VALUE.,否则等于MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 最后数组复制,底层是System.arraycopy()方法
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 扩容后数组的容量是原先数组容量的1.5倍。
- ArrayList中的数组长度最大是Integer.MAX_VALUE,超过这个值,JVM就不会给数组分配内存空间了。
- 新增时,并没有对新增元素进行严格校验,所以可以新增null。
其实我们可以看出,大多数方法中都用到了Arrays.copyOf方法
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
//新建数组并返回数组
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(
original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
再看System.arraycopy
//src 是原数组,srcPos是原数组的位置,dest是目标数组,destPOS是目标数组的位置,length是拷贝的长度,这个方法是native方法,所以底层可能是C/C++编写的
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
删除和迭代
ArrayList支持两种删除元素的方式
- remove(int index) 按照下标删除
- remove(Object o) 按照元素删除,删除第一个匹配到的元素。
public E remove(int index) {
//数组下标越界检查
rangeCheck(index);
//修改次数自增1
modCount++;
//记录被删除的值
E oldValue = elementData(index);
// 记录将要从index后移动的多少个位数到前面,因为size是从0开始,index是从1开始所以需要减一
int numMoved = size - index - 1;
if (numMoved > 0)
//从elementData的index+1处拷贝,拷贝到elementData数组中,从index处开始放置拷过来的元素,拷贝的长度是 numMoved
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 最后一个元素赋值为null,帮助GC
return oldValue;
}
/**
根据值删除元素
*/
public boolean remove(Object o) {
//y因为ArrayList允许元素值为null,所以删除的时候遍历一次找到第一个是null的值,然后移除。
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
//这里是根据equals方法判断值是否相等,相等再根据索引删除
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
//记录修改数加1
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; //最后元素赋值成null,有助于GC
}
/**
移除集合中所有元素
*/
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
//将集合中的每一个元素赋值为null
elementData[i] = null;
//集合元素的个数设置成0
size = 0;
}
- 新增时因为没有对null做校验所以可以新增null,删除的时候就需要对null值做判断了,按照值删除元素实际上还是根据值找到这个元素的索引,进行删除。比较的方法是通过equals方法。
- 某一个元素被删除后,ArrayList采用的是直接将这个元素后面的所有元素复制到原数组中,最后的一个元素的值设置成null,让JVM进行GC操作。
接下来看迭代
public ListIterator<E> listIterator(int index) {
//如果索引越界直接抛异常IndexOutOfBoundsException,否则返回从指定下标开始的所有元素列表(按熟顺序),ListIterator接口继承了Iterator接口
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
/**
* 返回列表中的列表迭代器按顺序返回,迭代器是fail-fast
*/
public ListIterator<E> listIterator() {
return new ListItr(0);
}
/**
* 以正确顺序返回元素列表的迭代器
*/
public Iterator<E> iterator() {
return new Itr();
}
//ltr是ArrayList的内部类,实现了 Iterator接口,这里只是Itr的部分代码
private class Itr implements Iterator<E> {
int cursor; // i迭代过程中,下一个元素的位置,默认从0开始
int lastRet = -1; // 新增时表示上次迭代过程中,索引的位置;删除场景下是-1
int expectedModCount = modCount;
//期望修改的版本号=实际的修改版本号
Itr() {}
public boolean hasNext() {
//判断下一个元素存不存在,只需知道下一个索引的下标是不是数组大小,如果不是返回true,如果是返回false
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//检查版本号是不是已经被修改
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];
}
public void remove() {
//当lastRet小于0,也就是说在多个线程操作时,直接将迭代器中的列表删完了,里面没有元素了,就会抛出IllegalStateException
if (lastRet < 0)
throw new IllegalStateException();
//迭代过程中判断版本号有没有被修改,如果被修改抛出ConcurrentModificationException
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
//lastRet=-1防止重复删除
lastRet = -1;
//下次迭代时期望的版本号和实际的版本号一致
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//版本号比较
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
小结
- ArrayList底层是使用类似于动态数组实现的,默认构造方法初始化的容量是10,但是在没有指定容量大小时区add第一个元素时才会确定出数组的容量。
- 扩容时,扩容后的长度是扩容前的1.5倍
- 实现了RandomAccess接口,支持随机读写,get读取元素的性能较好
- 线程时不安全的,是因为自身的elementData、size、modCount在进行各种操作时,都没有加锁
- 新增(这里的新增默认是增加到尾部)和删除方法 的时间复杂度是O(1)
以上理解有误的地方欢迎指出,共同进步!
ArrayList源码解析--值得深读的更多相关文章
- 面试必备:ArrayList源码解析(JDK8)
面试必备:ArrayList源码解析(JDK8) https://blog.csdn.net/zxt0601/article/details/77281231 概述很久没有写博客了,准确的说17年以来 ...
- ArrayList源码解析
ArrayList简介 ArrayList定义 1 public class ArrayList<E> extends AbstractList<E> implements L ...
- 顺序线性表 ---- ArrayList 源码解析及实现原理分析
原创播客,如需转载请注明出处.原文地址:http://www.cnblogs.com/crawl/p/7738888.html ------------------------------------ ...
- ArrayList源码解析(二)
欢迎转载,转载烦请注明出处,谢谢. https://www.cnblogs.com/sx-wuyj/p/11177257.html 自己学习ArrayList源码的一些心得记录. 继续上一篇,Arra ...
- Java中的容器(集合)之ArrayList源码解析
1.ArrayList源码解析 源码解析: 如下源码来自JDK8(如需查看ArrayList扩容源码解析请跳转至<Java中的容器(集合)>第十条):. package java.util ...
- Collection集合重难点梳理,增强for注意事项和三种遍历的应用场景,栈和队列特点,数组和链表特点,ArrayList源码解析, LinkedList-源码解析
重难点梳理 使用到的新单词: 1.collection[kəˈlekʃn] 聚集 2.empty[ˈempti] 空的 3.clear[klɪə(r)] 清除 4.iterator 迭代器 学习目标: ...
- 【源码解析】- ArrayList源码解析,绝对详细
ArrayList源码解析 简介 ArrayList是Java集合框架中非常常用的一种数据结构.继承自AbstractList,实现了List接口.底层基于数组来实现动态容量大小的控制,允许null值 ...
- ArrayList源码解析(一)
源码解析系列主要对Java的源码进行详细的说明,由于水平有限,难免出现错误或描述不准确的地方,还请大家指出. 1.位置 ArrayList位于java.util包中. package java.uti ...
- Java集合-ArrayList源码解析-JDK1.8
◆ ArrayList简介 ◆ ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了List, RandomAcc ...
随机推荐
- 笔记-Cats Transport<已写题解>
笔记-Cats Transport Cats Transport 令 \(D_i=\sum_{j=1}^id_i\),\(T_i=t_i-D_{h_i}\). 为 \(T_i\) 从小到大排序,令 \ ...
- STL——容器(Set & multiset)编译器提供的16种构造(挖个坑)
Set & multiset 在vs2019编译器中提供了16种构造方法 1.默认的无参构造 2.比较容器内容,key_comp()函数返回一个比较key的函数. 3.使用迭代器的区间拷贝,拷 ...
- S3C2440从NAND Flash启动和NOR FLASH启动的问题
1.为什么NAND FLASH不能直接运行程序 NAND FLASH本身是连接到了控制器上而不是系统总线上.CPU运行机制为:CPU启动后是要取指令执行的,如果是SROM.NOR FLASH ...
- JavaSE25-Junit&注解
1.Junit单元测试 1.1 测试分类 1. 黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值. 2. 白盒测试:需要写代码的.关注程序具体的执行流程. Junit使用:白盒测试 步骤: ...
- DP-DAY3游记
问题 A: 2017夏令营第一阶段(Day3)问题A拆分数字I 题目描述 把数字N拆分一些正整数的和,问有多少种不同的方法? 例如:N=4,有1+1+1+1.1+1+2.1+2+1.1+3.2+ ...
- s2-061 漏洞复现
0x00 漏洞简介 Apache Struts2框架是一个用于开发Java EE网络应用程序的Web框架.Apache Struts于2020年12月08日披露 S2-061 Struts 远程代码执 ...
- Appium App UI 自动化测试理论知识
(一)App自动化测试背景 随着移动终端的普及,手机应用越来越多,也越来越重要.App的回归测试用例数量越来越多,全量回归也越来越消耗时间.另外移动端碎片化严重(碎片化:兼容性测试,手机品牌多样.An ...
- JavaWeb基础总结:Servlet专题
最近工作中有部分整改老接口的任务,大部分与Spring的拦截器,Tomcat相关,改到一些底层的代码发现,对基础J2EE的知识有些遗忘,需要频繁查阅,索性从头系统的整理一下Servlet和Filter ...
- Core3.0中Swagger使用JWT
前言 学习ASP.NETCore,原链接 https://www.cnblogs.com/laozhang-is-phi/p/9511869.html 原教程是Core2.2,后期也升级到了Core3 ...
- Blogs实现侧边公告栏设置
说明:只需要在博客侧边栏公告(支持HTML代码) (支持 JS 代码)里面添加如下代码 #1.博客运行时长统计 <!--博客运行时长显示开始--!> <div id="sh ...