概述

ArrayList 是 List 接口的一个实现类,也是 Java 中最常用的容器实现类之一,可以把它理解为「可变数组」。

我们知道,Java 中的数组初始化时需要指定长度,而且指定后不能改变。ArrayList 内部也是一个数组,它对数组的功能做了增强:主要是在容器内元素增加时可以动态扩容,这也是 ArrayList 的核心所在。

前面「JDK源码分析-List, Iterator, ListIterator」已经概述了 List 接口的方法,ArrayList 的主要方法与 List 基本一致,因此这里重点分析其内部结构和扩容的原理。

ArrayList 的继承结构如下(省略部分接口):

构造器

我们先从构造器着手进行分析。ArrayList 有三个构造器,分别为:

1. 无参构造器

/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

该构造器涉及两个变量:elementData 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。这两个变量的定义如下:

transient Object[] elementData; // non-private to simplify nested class access

可以看到 elementData 是一个 Object 类型的数组,该数组也是 ArrayList 作为容器用于存储数据的地方。

/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个 Object 类型的空数组。因此,该无参构造器的作用就是将 elementData 初始化为一个 Object 类型的空数组。

2. 指定初始化容量的构造器

该构造传入一个参数,即初始化内部数组容量的 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);
}
}

该构造器根据传入的初始容量(initialCapacity)初始化用于存储元素的数组 elementData 变量。当初始容量为 0 时,elementData 被初始化为 EMPTY_ELEMENTDATA,该变量如下:

private static final Object[] EMPTY_ELEMENTDATA = {};

该数组与 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 都是一个空的 Object 数组,二者名字不同是为了区分 ArrayList 初始化时是否指定了容量,后期进行扩容的时候有所不同。

3. 指定初始化集合的构造器

该构造器传入一个集合 Collection,即使用 Collection 中的元素初始化 ArrayList 对象,代码如下:

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;
}
}

注意:这里若 Collection 为空时会抛出 NPE,因此初始化前有必要判空。

扩容实现原理

上面分析了 ArrayList 的构造器,但 ArrayList 如何做到动态扩容呢?

我们可以从 add() 方法着手进行分析(addAll() 方法类似,不再单独分析),如下:

// ArrayList 的大小(包含元素的个数)
private int size; // 将指定的元素添加到 List 末尾
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

可以看到,在 add() 方法执行时,会首先执行 ensureCapacityInternal() 方法:

private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

该方法会先通过 calculateCapacity 方法计算数组需要的容量 minCapacity,然后判断是否需要执行 grow() 方法:

// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10; private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 这里只会在使用无参构造器初始化,并且第一次使用 add 方法时执行(将容量初始化为 10)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
} private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

这里可以看到,若 elementData 初始值为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,即使用无参构造器初始化 ArrayList,则默认初始化容量为 10.

若所需容量大小 minCapacity 比原数组长度大(即原数组长度不够用了),则执行 grow() 方法(对数组进行扩容):

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);
} private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

新容量大小计算:

int newCapacity = oldCapacity + (oldCapacity >> 1);

由此可以看出,新容量为原容量的 1.5 倍;若扩容为 1.5 倍后,仍未达到所需容量,则直接使用所需要的容量。

如何扩容的呢?使用 Arrays.copyOf() 方法创建了一个新的数组,然后将原先数组的元素拷贝到新的数组中:

elementData = Arrays.copyOf(elementData, newCapacity);

跟踪该方法可以发现,最终调用了 System.arraycopy() 方法:

public static native void arraycopy(Object src,  int  srcPos,
Object dest, int destPos, int length);

ArrayList 扩容小结

1. 若未指定初始化容量

当第一次执行 add() 方法时,将数组长度默认初始化为 10,之后再添加元素时不扩容,直至容量等于 10,再添加第 11 个元素时,将容量扩容为 15 (10 + 10 >> 1),以此类推。

2. 若指定了初始化容量 initialCapacity

当数组容量到达 initialCapacity 之前,不进行扩容,当容量等于 initialCapacity 时若再添加元素,则执行扩容,扩容操作同上。

3. 新容量大小

默认扩容后数组的容量为原数组容量的 1.5 倍;若仍未达到所需大小(使用 addAll() 方法时可能出现),则扩容为所需的容量。

线程安全性

线程安全可以简单理解为:多个线程同时操作一个方法或变量时,不会出现问题;若出现问题,可认为是线程不安全的。

ArrayList 是线程不安全的,主要体现有二:

1. 多个线程往 ArrayList 添加数据时(扩容时),可能会产生数组越界异常(ArrayIndexOutOfBoundsException);

2. 多个线程遍历同一个 ArrayList,有线程对其进行修改时,可能会抛出 ConcurrentModificationException。

先对 add() 方法进行分析:

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

注意:i++ 操作是非原子性的。

场景分析一:

若有一个初始容量为 1 的 ArrayList,线程 T1 和 T2 同时向其中添加元素(add() 方法),当添加第 2 个元素时,需要进行扩容。

此时若有以下执行时序:

1. T1、T2 检测到需要扩容

此时,T1 和 T2 拿到的都是 elementData.length=1, size=1,若 T1 先执行 ensureCapacityInternal() 方法扩容,则 elementData.length=2, size=1;之后 T2 再执行 ensureCapacityInternal() 方法时,因为初始 size=1,而 T1 扩容后 elementData.length=2,所以 T2 不会再进行扩容(不再执行 grow() 方法)。

2. T1 执行赋值操作和 size++ 操作

之后 T1 执行赋值操作 elementData[1]=XX 和 size++,size 自增为 2;

3. T2 执行赋值操作(数组越界)和 size++ 操作

由于上一步 T1 执行了 size++ 操作,当前 size=2,这时的赋值 elementData[size++] 将对 elementData[2] 执行赋值操作,而 elementData.length=2,最大下标为 1,这时会发生数组越界异常(ArrayIndexOutOfBoundsException)。

场景分析二:

有一个 ArrayList,线程 T1 对其进行遍历;线程 T2 对其遍历,并移除部分元素。

对 ArrayList 进行遍历时,以 iterator 方法为例,其代码如下:

public Iterator<E> iterator() {
return new Itr();
}
会创建一个内部类 Itr,如下:
private class Itr implements Iterator<E> {
int expectedModCount = modCount; // ...
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];
} // 检查是否有其他线程进行结构性修改
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

而 ArrayList 的 add()、remove() 等结构性修改的操作都会使  modCount++。因此有:若线程 T1 只对 ArrayList 进行遍历;而线程 T2 对同一个 ArrayList 进行了移除元素操作,则会修改 modCount 的值,导致线程 T1 中 modCount != expectedModCount,从而触发 ConcurrentModificationException。

ArrayList 小结

1. ArrayList 可以理解为「可以自动扩容的数组」,默认初始化容量为 10,默认每次扩容为原容量的 1.5 倍;

2. 扩容时会创建一个新的数组,并将之前的元素拷贝到新数组中(因此,若要将数量已知的元素放入 ArrayList,在初始化时指定长度可以避免多次扩容);

3. 线程不安全,不适合在多线程场景下使用。

Stay hungry, stay foolish.

PS: 本文首发于微信公众号。

【JDK】JDK源码分析-ArrayList的更多相关文章

  1. JDK Collection 源码分析(2)—— List

    JDK List源码分析 List接口定义了有序集合(序列).在Collection的基础上,增加了可以通过下标索引访问,以及线性查找等功能. 整体类结构 1.AbstractList   该类作为L ...

  2. JDK AtomicInteger 源码分析

    @(JDK)[AtomicInteger] JDK AtomicInteger 源码分析 Unsafe 实例化 Unsafe在创建实例的时候,不能仅仅通过new Unsafe()或者Unsafe.ge ...

  3. 设计模式(十八)——观察者模式(JDK Observable源码分析)

    1 天气预报项目需求,具体要求如下: 1) 气象站可以将每天测量到的温度,湿度,气压等等以公告的形式发布出去(比如发布到自己的网站或第三方). 2) 需要设计开放型 API,便于其他第三方也能接入气象 ...

  4. [源码分析]ArrayList和LinkedList如何实现的?我看你还有机会!

    文章已经收录在 Github.com/niumoo/JavaNotes ,更有 Java 程序员所需要掌握的核心知识,欢迎Star和指教. 欢迎关注我的公众号,文章每周更新. 前言 说真的,在 Jav ...

  5. JDK源码分析 – ArrayList

    ArrayList类的申明 ArrayList是一个支持泛型的,底层通过数组实现的一个可以存任意类型的数据结构,源码中的定义如下: public class ArrayList<E> ex ...

  6. 源码分析--ArrayList(JDK1.8)

    ArrayList是开发常用的有序集合,底层为动态数组实现.可以插入null,并允许重复. 下面是源码中一些比较重要属性: 1.ArrayList默认大小10. /** * Default initi ...

  7. JDK源码分析-ArrayList

    ArrayList 储存结构 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Objec ...

  8. JDK Collection 源码分析(3)—— Queue

    @(JDK)[Queue] JDK Queue Queue:队列接口,对于数据的存取,提供了两种方式,一种失败会抛出异常,另一种则返回null或者false.   抛出异常的接口:add,remove ...

  9. JDK Collection 源码分析(1)—— Collection

    JDK Collection   JDK Collection作为一个最顶层的接口(root interface),JDK并不提供该接口的直接实现,而是通过更加具体的子接口(sub interface ...

随机推荐

  1. ZooKeeper 系列(二)—— Zookeeper单机环境和集群环境搭建

    一.单机环境搭建         1.1 下载         1.2 解压         1.3 配置环境变量         1.4 修改配置         1.5 启动         1. ...

  2. RestTemplate使用不当引发的问题分析

    背景 系统: SpringBoot开发的Web应用: ORM: JPA(Hibernate) 接口功能简述: 根据实体类ID到数据库中查询实体信息,然后使用RestTemplate调用外部系统接口获取 ...

  3. 《菜鸟也要学会C》-和大家聊一聊

    简介 为什么要出本系列作品? 怎么学好C? 学完这套课程后,我的编程会怎么样? 1.1为什么要出本系列作品? 随着大部分人喜欢编程,大部分人都有一个毛病,就是想要急切的学完编程.其实这种思想是错误的, ...

  4. K-近邻算法介绍与代码实现

    声明:如需转载请先联系我. 最近学习了k近邻算法,在这里进行了总结. KNN介绍 k近邻法(k-nearest neighbors)是由Cover和Hart于1968年提出的,它是懒惰学习(lazy ...

  5. 用 IQ分布模拟图来测试浏览器的性能

    今天天气太凉快,跟这个日历上属于夏天的那一页显得格格不入!就连我我床下那台废弃的ThinkPad,居然也十分透凉气,那外壳连我的体温高都没有,于是,我就开始想一个方法,让我那个废弃的电脑发热,顺便用它 ...

  6. restapi(0)- 平台数据维护,写在前面

    在云计算的推动下,软件系统发展趋于平台化.云平台系统一般都是分布式的集群系统,采用大数据技术.在这方面akka提供了比较完整的开发技术支持.我在上一个系列有关CQRS的博客中按照实际应用的要求对akk ...

  7. 使用JavaScript实现量化策略并发执行——封装Go函数

    在实现量化策略时,很多情况下,并发执行可以降低延时提升效率.以对冲机器人为例,需要获取两个币的深度,顺序执行的代码如下: 请求一次rest API存在延时,假设是100ms,那么两次获取深度的时间实际 ...

  8. 【MM系列】SAP 采购订单的批量修改

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[MM系列]SAP 采购订单的批量修改   前言 ...

  9. C++学习书籍推荐《Effective C++ 第三版(英文)》下载

    百度云及其他网盘下载地址:点我 作者简介 Scott Meyers is one of the world's foremost authorities on C++, providing train ...

  10. 详解rel 属性及noflow

    rel属性的意思是指: 当前页和指向页的关系. 而 rev 属性则指定从目标文档到源文档的关系.百度目前支持: rel='noflow'属性. rel属性的意思是指: 当前页和指向页的关系. < ...