简介

  • ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。
  • ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。
  • ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。

存储结构

// 当前数据对象存放地方,当前对象不参与序列化
// 这个关键字最主要的作用就是当序列化时,被transient修饰的内容将不会被序列化
transient Object[] elementData;
  • Object类型数组。

数据域

    // 序列化ID
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 一个空数组,方便使用,主要用于带参构造函数初始化和读取序列化对象等。
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 和官方文档写的一样,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的区别
* 仅仅是为了区别用户带参为0的构造和默认构造的惰性初始模式对象。
* 当用户带参为0的构造,第一次add时,数组容量grow到1。
* 当用户使用默认构造时,第一次add时,容量直接grow到DEFAULT_CAPACITY(10)。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 当前数据对象存放地方,当前对象不参与序列化
// 这个关键字最主要的作用就是当序列化时,被transient修饰的内容将不会被序列化
transient Object[] elementData; // non-private to simplify nested class access
// 当前数组中元素的个数
private int size;
// 数组最大可分配容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 集合数组修改次数的标识(由AbstractList继承下来)(fail-fast机制)
protected transient int modCount = 0;
  • ArrayList的无参构造函数。初始化的时候并没有真正的创建10个空间,这是惰性初始模式对象。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的区别仅仅是为了区别用户带参为0的构造和默认构造的惰性初始模式对象。
  • modCount用来记录ArrayList结构发生变化的次数。用于Fail-Fast机制

构造函数

	public ArrayList() {
// 只有这个地方会引用DEFAULTCAPACITY_EMPTY_ELEMENTDATA
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
} public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 使用 EMPTY_ELEMENTDATA,在其他的多个地方可能会引用EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
} public ArrayList(Collection<? extends E> c) {
// 把传入集合传化成[]数组并浅拷贝给elementData
elementData = c.toArray();
// 转化后的数组长度赋给当前ArrayList的size,并判断是否为0
if ((size = elementData.length) != 0) {
//c.toArray可能不会返回 Object[],可以查看 java 官方编号为 6260652 的 bug
if (elementData.getClass() != Object[].class)
// 若 c.toArray() 返回的数组类型不是 Object[],则利用 Arrays.copyOf(); 来构造一个大小为 size 的 Object[] 数组
// 此时elementData是指向传入集合的内存,还需要创建新的内存区域深拷贝给elementData
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 传入数组size为零替换空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA 的区别仅仅是为了区别用户带参为0的构造和默认构造的惰性初始模式对象。
  • 注意深拷贝和浅拷贝
  • 带参为0的构造会惰性初始化,不为0的构造则不会惰性初始化。

add()源码解析

public boolean add(E e) {
// 确保数组已使用长度(size)加1之后足够存下 下一个数据
ensureCapacityInternal(size + 1); // Increments modCount!!
// 数组的下一个index存放传入元素。
elementData[size++] = e;
// 始终返回true。
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 这里就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和
// EMPTY_ELEMENTDATA 最主要的区别。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 默认构造第一次add返回10。
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 带参为0构造第一次add返回 1 (0 + 1)。
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
// 自增修改计数
modCount++; // overflow-conscious code
// 当前数组容量小于需要的最小容量
if (minCapacity - elementData.length > 0)
// 准备扩容数组
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
// 获得当前数组容量
int oldCapacity = elementData.length;
// 新数组容量为1.5倍的旧数组容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
// 若 newCapacity 依旧小于 minCapacity
newCapacity = minCapacity;
// 判断是需要的容量是否超过最大的数组容量。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 在Arrays.copyOf()中会将原数组整个赋值到扩容的数组中。
elementData = Arrays.copyOf(elementData, newCapacity);
}
  • 扩容操作需要调用Arrays.copyOf()把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建ArrayList对象时就指定大概的容量大小,减少扩容操作的次数。

add(int index, E element)源码分析

// 这是一个本地方法,由C语言实现。
public static native void arraycopy(Object src, // 源数组
int srcPos, // 源数组要复制的起始位置
Object dest, // 目标数组(将原数组复制到目标数组)
int destPos, // 目标数组起始位置(从目标数组的哪个下标开始复制操作)
int length // 复制源数组的长度
); public void add(int index, E element) {
// 判断索引是否越界
rangeCheckForAdd(index);
// 确保数组已使用长度(size)加1之后足够存下 下一个数据
ensureCapacityInternal(size + 1); // Increments modCount!!
// 运行到这里代表数组容量满足。
// 数组从传入形参index处开始复制,复制size-index个元素(即包括index在内后面的元素全部复制),
// 从数组的index + 1处开始粘贴。
// 这时,index 和 index + 1处元素数值相同。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 把index处的元素替换成新的元素。
elementData[index] = element;
// 数组内元素长度加一。
size++;
}
  • 需要调用System.arraycopy()将包括index在内后面的元素都复制到index + 1位置上,该操作的时间复杂度为O(N),可以看出ArrayList数组头增加元素的代价是非常高的。

remove(int index)源码分析

public E remove(int index) {
// 检查index
rangeCheck(index); modCount++;
E oldValue = elementData(index); int numMoved = size - index - 1;
if (numMoved > 0)
// 和 add(int index, E element)原理想通。
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 引用计数为0,会自动进行垃圾回收。
elementData[--size] = null; // clear to let GC do its work
// 返回旧元素
return oldValue;
}
  • 需要调用System.arraycopy()将包括index + 1在内后面的元素都复制到index位置上,该操作的时间复杂度为O(N),可以看出ArrayList数组头增加元素的代价是非常高的。

Fail-Fast机制

fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常。fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。

  • 结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组大小,仅仅只是设置元素的值不算结构发生变化。
  • 在进行序列化或者迭代操作时,需要比较操作前后modCount是否改变,如果改变了需要跑出ConcurrentModificationException
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
// 期待的修改值等于当前修改次数(modCount)
int expectedModCount = modCount; public boolean hasNext() {
return cursor != size;
} public E next() {
// 检查 expectedModCount是否等于modCount,不相同则抛出ConcurrentModificationException
checkForComodification();
/** 省略此处代码 */
} public void remove() {
if (this.lastRet < 0)
throw new IllegalStateException();
checkForComodification();
/** 省略此处代码 */
} final void checkForComodification() {
if (ArrayList.this.modCount == this.expectedModCount)
return;
throw new ConcurrentModificationException();
}
}

一个单线程环境下的fail-fast的例子

     public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0 ; i < 10 ; i++ ) {
list.add(i + "");
}
Iterator<String> iterator = list.iterator();
int i = 0 ;
while(iterator.hasNext()) {
if (i == 3) {
list.remove(3);
}
System.out.println(iterator.next());
i ++;
}
}

序列化

ArrayList 实现了 java.io.Serializable 接口,但是自己定义了序列化和反序列化。因为ArrayList基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没有必要全部进行序列化。因此 elementData 数组使用 transient 修饰,可以防止被自动序列化。

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
// 将当前类的非静态(non-static)和非瞬态(non-transient)字段写入流
// 在这里也会将size字段写入。
s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone()
// 序列化数组包含元素数量,为了向后兼容
// 两次将size写入流
s.writeInt(size); // Write out all elements in the proper order.
// 按照顺序写入,只写入到数组包含元素的结尾,并不会把数组的所有容量区域全部写入
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
// 判断是否触发Fast-Fail
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 设置数组引用空数组。
elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff
// 将流中的的非静态(non-static)和非瞬态(non-transient)字段读取到当前类
// 包含 size
s.defaultReadObject(); // Read in capacity
// 读入元素个数,没什么用,只是因为写出的时候写了size属性,读的时候也要按顺序来读
s.readInt(); // ignored if (size > 0) {
// be like clone(), allocate array based upon size not capacity
// 根据size计算容量。
int capacity = calculateCapacity(elementData, size);
// SharedSecrets 一个“共享机密”存储库,它是一种机制,
// 用于调用另一个包中的实现专用方法,而不使用反射。TODO
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
// 检查是否需要扩容
ensureCapacityInternal(size); Object[] a = elementData;
// Read in all elements in the proper order.
// 依次读取元素到数组中
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}

ArrayList中为什么size要序列化两次?

在代码中s.defaultWriteObject();中size应该也被序列化了,为什么下边还要再单独序列化一次呢?

这样写是出于兼容性考虑。

旧版本的JDK中,ArrayList的实现有所不同,会对length字段进行序列化。

而新版的JDK中,对优化了ArrayList的实现,不再序列化length字段。

这个时候,如果去掉s.writeInt(size),那么新版本JDK序列化的对象,在旧版本中就无法正确读取,

因为缺少了length字段。

因此这种写法看起来多此一举,实际上却保证了兼容性。

小结

  • ArrayList基于数组方式实现,无容量的限制(会扩容)
  • 添加元素时可能要扩容(所以最好预判一下),删除元素时不会减少容量(若希望减少容量可以使用trimToSize()),删除元素时,将删除掉的位置元素置为null,下次gc就会回收这些元素所占的内存空间。
  • 线程不安全
  • add(int index, E element):添加元素到数组中指定位置的时候,需要将该位置及其后边所有的元素都整块向后复制一位
  • get(int index):获取指定位置上的元素时,可以通过索引直接获取(O(1))
  • remove(Object o)需要遍历数组
  • remove(int index)不需要遍历数组,只需判断index是否符合条件即可,效率比remove(Object o)高
  • contains(E)需要遍历数组

Java ArrayList底层实现原理源码详细分析Jdk8的更多相关文章

  1. Java HashMap底层实现原理源码分析Jdk8

    在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依 ...

  2. SpringBoot自动配置原理源码级别分析

    SpringBoot自动配置原理 前言 后面还会讲到SpringBoot自动配置原理,会主要讲解@EnableAutoConfiguratuon注解帮助我们做了什么事情,是如何自动把自动配置类扫描到容 ...

  3. Spring Boot自动装配原理源码分析

    1.环境准备 使用IDEA Spring Initializr快速创建一个Spring Boot项目 添加一个Controller类 @RestController public class Hell ...

  4. LinkedHashMap 源码详细分析(JDK1.8)

    1. 概述 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题.除此之外,Linke ...

  5. ArrayList 源码详细分析

    1.概述 ArrayList 是一种变长的集合类,基于定长数组实现.ArrayList 允许空值和重复元素,当往 ArrayList 中添加的元素数量大于其底层数组容量时,其会通过扩容机制重新生成一个 ...

  6. java 1.8 动态代理源码分析

    JDK8动态代理源码分析 动态代理的基本使用就不详细介绍了: 例子: class proxyed implements pro{ @Override public void text() { Syst ...

  7. HashMap 源码详细分析(JDK1.8)

    一.概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值, ...

  8. DownloadProvider 源码详细分析

    DownloadProvider 简介 DownloadProvider 是Android提供的DownloadManager的增强版,亮点是支持断点下载,提供了“开始下载”,“暂停下载”,“重新下载 ...

  9. Java中HashMap底层原理源码分析

    在介绍HashMap的同时,我会把它和HashTable以及ConcurrentHashMap的区别也说一下,不过本文主要是介绍HashMap,其实它们的原理差不多,都是数组加链表的形式存储数据,另外 ...

随机推荐

  1. hadoop集群单点配置

    =================== =============================== ----------------hadoop集群搭建 --------------------- ...

  2. 解决CentOS6.x或RedHat Linux 6.x版本不能通过System eth0以固定IP访问外网的问题

    当你在VMware Workstation Pro中,打开从别人那里克隆来的系统,或者是开启迁移后的虚拟机系统时,VMware将会提示你:此虚拟机可能已被移动或 复制.为了配置特定的管理和网络功能.V ...

  3. flask+layui+echarts实现前端动态图展示数据

    效果图: 该效果主要实现一个table展示数据,并在下方生成一个折线图. 实现方式: 1.首先需要对表格进行一个数据加载,这里用到了layui的table.render,具体用法可以参考 https: ...

  4. if [ $# -ne 1 ] 作用

    在shell脚本中经常会使用if [ $# -ne 1 ];then...这类脚本 ];then 这段命令是用于判断参数的个数是否为1,不是则进行then的逻辑处理,其中$#表示参数个数,-ne是不等 ...

  5. cmd控制台 wrapper | OpenSCManager failed - 拒绝访问。 (0x5)解决

    在windows上安装mycat执行命令时, D:\develop\Mycat\bin>mycat.bat install 返回wrapper | OpenSCManager failed - ...

  6. Windows搭建MongoDB复制集

    ​上篇,我们已经知道了什么是MongoDB的复制集,不知道的可以查看上篇哦,传送门来了. 光说不练,假把式,咱来自己搭建一个复制集.先下载安装哦,不知道的查看上篇哦,https://blog.csdn ...

  7. 【Unity与Android】02-在Unity导出的Android工程中接入Google Admob广告

    我在上一篇文章 [Unity与Android]01-Unity与Android交互通信的简易实现) 中介绍了Unity与Android通讯的基本方法. 这一篇开始进入应用阶段,这次要介绍的是如何在An ...

  8. VirtualBox 启动时提示“获取 VirtualBox COM 对象失败”的解决

    昨天给电脑打了一堆补丁和更新,今天启动 VirtualBox 的时候提示 “获取 VirtualBox COM 对象失败”,好在百度到了 CSDN 上的一篇文章解决了这个问题. 错误详情 “获取 Vi ...

  9. 部署主从dns

    主机部署:yum安装DNS服务和依赖 [admin@haifly-bj-dns1 ~]$ sudo yum install bind-chroot启动named-chroot服务 [admin@hai ...

  10. python selenium句柄操作

    一.获取当前窗口句柄 1.元素有属性,浏览器的窗口其实也有属性的,只是你看不到,浏览器窗口的属性用句柄(handle)来识别. 2.人为操作的话,可以通过眼睛看,识别不同的窗口点击切换.但是脚本没长眼 ...