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 ...
随机推荐
- 哔哩哔哩直播录制工具v1.1.18
软件介绍 看直播有时候非常精彩想要录制下来,或者非常喜欢某个主播想录制下直播全程,可去找录制软件的时候却发现有这样那样的问题,导致一番操作不尽人意.但是现在<B站直播录制工具>可以完美解决 ...
- sql注入之双查询注入
双查询注入前需要了解什么是子查询 子查询可以理解在一个select语句中再插入一个select 里面的select语句就是子查询 例子:select concat((select database() ...
- 环境篇:Atlas2.1.0兼容CDH6.3.2部署
环境篇:Atlas2.1.0兼容CDH6.3.2部署 Atlas 是什么? Atlas是一组可扩展和可扩展的核心基础治理服务,使企业能够有效地满足Hadoop中的合规性要求,并允许与整个企业数据生态系 ...
- Tokyo 五年 IT 生活
今天阳光甚好,在家中小屋,闲来无事,回顾一下这五年的历程.我想从来东京的缘由.东京的环境.生活.IT这四个方面介绍一下. 首先,说一下为什么我会来到东京. 电子信息专业毕业,大学实验室学习IT,毕业后 ...
- mini-web框架-装饰器-总结2(5.3.2)
@ 目录 1.说明 2.代码 关于作者 1.说明 多级装饰器嵌套 带参数的装饰器 这里打印print(index) 会在函数定义的时候@test(222) 就被调用,返回一个test2继续装饰 2.代 ...
- MySQL索引与SQL注入
SQL注入: SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作, ...
- SVN 使用教程 命令 visual studio 使用SVN
首先推荐大家一个应该是国内外最好的SVN仓库,不限私有,不限成员:https://svnbucket.com/ SVN官网 https://tortoisesvn.net/downloads.html ...
- sql server 汉字转拼音首字母
create function fun_getPY ( @str nvarchar(4000) ) returns nvarchar(4000) as begin declare @word ncha ...
- Python-对比参考目录查找多个文件夹中不同的文件
改完文件名称后,Dr.he 发现分别保存5个状态的jpg 文件的文件夹出现缺少文件的情况.为了让他少熬夜查找缺失文件,结合网友分享的脚本,写了查找以下代码,满足他的需求,也以防自己忘记.以下代码能解决 ...
- 操作系统微内核和Dubbo微内核,有何不同?
你好,我是 yes. 在之前的文章已经提到了 RPC 的核心,想必一个 RPC 通信大致的流程和基本原理已经清晰了. 这篇文章借着 Dubbo 来说说微内核这种设计思想,不会扯到 Dubbo 某个具体 ...