一、概述

  本篇文章记录通过阅读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());
}

上述代码很简单,步骤为:

  1. 调用无参构造器,初始化一个ArrayList
  2. 循环向该ArrayList插入11个元素

以该步骤为例:

  List<Object> list = new ArrayList<>();  //   初始化elementData为一个空数组{} , elementData.length = 0

  i = 0 时

  list.add(new Object());

  这个操作会经历上面代码片段的①②③④⑤步骤

  1. 在走步骤①时使用默认容量 10
  2. 走步骤②时  判断10 > elementData.length  因此使用10作为最小容量请求扩容
  3. 走步骤③时 elementData.length 扩大1.5倍后依然为0 因此使用10作为新容量
  4. 通过步骤④native层拷贝方法进行数组拷贝
  5. 通过步骤⑤在新数组的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个元素放到数组中了,需要第二次扩容

  1. 执行代码片段步骤①,算出minCapacity=11
  2. 步骤②判断出minCapacity(11) > elementData.length(10),因此需要扩容
  3. 步骤③,通过在elementData.length基础上扩大1.5倍 即 15, 同时大于 minCapacity, 因此使用15作为新容量
  4. 步骤④,以15作为新数组容量,拷贝原数组到新数组中
  5. 步骤⑤,将新的元素放到扩容之后新数组下标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

情况1
线程A 线程B
add(e1) add(e2)
读到size为0 读到size也为0
elementData[0] = e1  
  elementData[0] = e2
情况2
线程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的更多相关文章

  1. ArrayList源码分析--jdk1.8

    ArrayList概述   1. ArrayList是可以动态扩容和动态删除冗余容量的索引序列,基于数组实现的集合.  2. ArrayList支持随机访问.克隆.序列化,元素有序且可以重复.  3. ...

  2. Java中ArrayList源码分析

    一.简介 ArrayList是一个数组队列,相当于动态数组.每个ArrayList实例都有自己的容量,该容量至少和所存储数据的个数一样大小,在每次添加数据时,它会使用ensureCapacity()保 ...

  3. Java集合-ArrayList源码解析-JDK1.8

    ◆ ArrayList简介 ◆ ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了List, RandomAcc ...

  4. 【thinking in java】ArrayList源码分析

    简介 ArrayList底层是数组实现的,可以自增扩容的数组,此外它是非线程安全的,一般多用于单线程环境下(Vector是线程安全的,所以ArrayList 性能相对Vector 会好些) Array ...

  5. Java基础—ArrayList源码浅析

    注:以下源码均为JDK8的源码 一. 核心属性 基本属性如下: 核心的属性其实是红框中的两个: //从注释也容易看出,一个是集合元素,一个是集合长度(注意是逻辑长度,即元素的个数,而非数组长度) 其中 ...

  6. Java基础——HashTable源码分析

    HashTable是基于哈希表的Map接口的同步实现 HashTable中元素的key是唯一的,value值可重复 HashTable中元素的key和value不允许为null,如果遇到null,则返 ...

  7. Java集合-ArrayList源码分析

    目录 1.结构特性 2.构造函数 3.成员变量 4.常用的成员方法 5.底层数组扩容原理 6.序列化原理 7.集合元素排序 8.迭代器的实现 9.总结 1.结构特性 Java ArrayList类使用 ...

  8. java.util.ArrayList源码分析

    public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess ...

  9. ReentrantLock源码分析--jdk1.8

    JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分 ...

随机推荐

  1. Jmeter结构体系及运行顺序

    一:jmeter运行原理: jmeter时以线程的方式来运行的(由于jmeter是java开发的所以是运行在JVM虚拟机上的,java也是支持多线程的) 二:jmeter结构体系 1.取样器smapl ...

  2. 常用windows命令

    目录 本教程概述 用到的工具 标签 简介 1.cmd的一些规则 2.cd切换目录命令 3.dir显示目录命令 4.type显示文本内容 5.del 删除文件 6.查看IP地址 7.net 命令 8.n ...

  3. PHP 插入排序 -- 希尔排序

    1.希尔排序 -- Shell Insertion Sort 时间复杂度:数学家正在勤劳的探索! 适用条件: 直接插入排序的改进,主要针对移动次数的减少,这取决于"增量队列"的取值 ...

  4. [Luogu3797] 妖梦斩木棒

    题目背景 妖梦是住在白玉楼的半人半灵,拥有使用剑术程度的能力. 题目描述 有一天,妖梦正在练习剑术.地面上摆放了一支非常长的木棒,妖梦把它们切成了等长的n段.现在这个木棒可以看做由三种小段构成,中间的 ...

  5. opencv实践::透视变换

    问题描述 拍摄或者扫描图像不是规则的矩形,会对后期处理产生不 好影响,需要通过透视变换校正得到正确形状. 解决思路 通过二值分割 + 形态学方法 + Hough直线 +透视变换 #include &l ...

  6. i春秋DMZ大型靶场实验(三)内网转发DMZ2

    更具实验文件知道存在源码泄露  下载源码进行源码审计 发现admin账号 查看user.php 发现mysql 账号 端口 对登录后源码进行审计 发现上传文件的两处漏洞 对 fiel name 可以 ...

  7. lcx 内网转发

    把放置到已经控制的内网主机 执行 内网主机输入命令lcx.exe -slave 外网ip 外网端口 内网ip 内网端口lcx.exe -slave 30.1.85.55 2222 127.0.0.1 ...

  8. 利用ansible书写playbook在华为云上批量配置管理工具自动化安装ceph集群

    首先在华为云上购买搭建ceph集群所需云主机: 然后购买ceph所需存储磁盘 将购买的磁盘挂载到用来搭建ceph的云主机上 在跳板机上安装ansible 查看ansible版本,检验ansible是否 ...

  9. HTTP Catcher

    HTTP Catcher HTTP Catcher 是一个 Web 调试工具.它可以拦截.查看.修改和重放来自 iOS 系统的 HTTP 请求. 你不需要连接电脑,HTTP Catcher 可以在后台 ...

  10. FreeSql 已支持 .NetFramework 4.0、ODBC 访问

    FreeSql 开源发布快一年了,目前主仓库代码量 64118 行,用 git 命令统计的命令如下: find . "(" -name "*.cs" " ...