简介

  • 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. 《菜鸟程序员成长之路:从技术小白到阿里巴巴Java工程师》

    <菜鸟程序员成长之路:从技术小白到阿里巴巴Java工程师> 国庆节快乐!一年一度长度排第二的假期终于来了. 难得有十一长假,作者也想要休息几天啦. 不管你是选择出门玩,还是在公司加班,在学 ...

  2. 短视频处理LanSoEditor-SDK之功能介绍

    短视频处理LanSoEditor-SDK之功能介绍  (注释: 我们的SDK每3周更新一次, 一下功能是在2.8.2版本上列出的,可能不是最新的功能, 请知悉) 和别家最大的不同在于: 别人提供功能, ...

  3. Disruptor—核心概念及体验

    本文基于最新的3.4.2的版本文档进行翻译,翻译自: https://github.com/LMAX-Exchange/disruptor/wiki/Introduction https://gith ...

  4. asp.net core 3.0 更新简记

    asp.net core 3.0 更新简记 asp.net core 3.0 更新简记 Intro 最近把活动室预约项目从 asp.net core 2.2 更新到了 asp.net core 3.0 ...

  5. SpringBoot注入Mapper失败

    SpringBoot注入Mapper失败,可能是因为没有加扫描Mapper层的注解 方式一:在所有mapper接口使用@Mapper注解 @Mapper public interface UserMa ...

  6. 如何在linux下编写一个简单的Shell脚本程序

    在了解了linux终端和其搭配的基本Shell(默认为bash)的基础下,我们就可以在终端中用vi/vim编辑器编写一个shell的脚本程序了 Shell既为一种命令解释解释工具,又是一种脚本编程语言 ...

  7. Fcitx使用搜狗词库与皮肤

    在 \(\text{Linux}\) 环境下,\(\text{Fcitx}\) 确实是最好用的开源输入法之一.然而 \(\text{Windows}\) 下的巨头输入法 -- 搜狗,对 \(\text ...

  8. Python flask构建微信小程序订餐系统☝☝☝

    Python flask构建微信小程序订餐系统☝☝☝ 一.Flask MVC框架结构 1.1实际项目结构 1.2application.py  项目配置文件 Flask之flask-script模块使 ...

  9. 微信小程序开发环境安装以及相关设置配置

    微信小程序开发环境安装以及相关设置配置 一.安装 软件名称:wechat_devtools_1.02.1907232_x64 软件安装地址:https://developers.weixin.qq.c ...

  10. 毕业生想学习web前端,应该怎么学才能最快找到工作?

    首先无论你要学习任何技能,必须有一个清晰的版图,什么是清晰的版图呢?首先了解你学的技术将来要从事什么工作,这个工作的条件是哪些? 然后你要有一个非常清晰的学习大纲,切记学习任何东西都要系统,不可胡乱的 ...