前言:作为菜鸟,需要经常回头巩固一下基础知识,今天看看 jdk 1.8 的源码,这里记录 ArrayList 的实现。

一、简介

  ArrayList 是有序的集合;

  底层采用数组实现对数据的增删查改;

  不是线程安全的;

  有自动扩容的功能。

二、类图

三、详细总结

  1、ArrayList 是实现了 List 接口的可变数据,非同步实现,并允许包括 null 在内的所有元素。

  2、底层采用数组实现。

  3、在数组增加时,会进行扩容,但由于底层采用的数组实现,所以扩容时会将老数组中的元素拷贝到一份新的数组中,所以性能代价很高

  4、采用了 Fail-Fast 机制,面对并发的修改时,迭代器会抛出异常,而不是冒着在将来某个不确定时间发生任意不确定行为的风险

  5、remove 方法会通过 System.arraycopy() 方法让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便 GC。

四、解惑

1、为什么成员变量 elementData 为什么被 transient 修饰?难道序列化时不需要数组元素?

  参考:https://blog.csdn.net/zero__007/article/details/52166306

  transient 用来表示一个域不是该对象序行化的一部分,当一个对象被序行化的时候,transient 修饰的变量的值是不包括在序行化的表示中的。但是 ArrayList 又是可序行化的类,elementData 是 ArrayList 具体存放元素的成员,用 transient 来修饰 elementData,岂不是反序列化后的 ArrayList 丢失了原先的元素?
       其实玄机在于 ArrayList 中的两个方法:

  /**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size); // Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
} if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
  /**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff
s.defaultReadObject(); // Read in capacity
s.readInt(); // ignored if (size > 0) {
// be like clone(), allocate array based upon size not 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 在序列化的时候会调用 writeObject,直接将 size 和 element 写入 ObjectOutputStream;反序列化时调用 readObject,从 ObjectInputStream 获取 size 和 element,再恢复到 elementData。
       为什么不直接用 elementData 来序列化,而采用上诉的方式来实现序列化呢?原因在于 elementData 是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。

2、为什么有两个默认空数组的成员变量?为什么 new ArrayList() 注释说初始化容量为 10?

  两个虽然都为空数组,但用途稍微有点不一致。

  其中,EMPTY_ELEMENTDATA 用于构造器中给出了初始化容量为 0 时的数组。代码如下:

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

  其中,DEFAULTCAPACITY_EMPTY_ELEMENTDATA  用于默认构造器的数组,主要用于在第一次增加元素时判断是否需要给出默认容量 10 的大小(grow() 方法用于扩容)。这里之所以不直接 new 一个初始容量为 10 的数组,我想是因为有时我们会 new 一个 ArrayList(),但是并不会添加数据,这样就可以节约空间。代码如下:

  public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
    public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
} private void ensureCapacityInternal(int minCapacity) {
     // 判断是否是默认构造函数构造的默认空数组实例,如果是就给出默认容量 10 和 size + 1 的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
} // 根据需要的最小容量来判断是否需要扩容
ensureExplicitCapacity(minCapacity);
} private void ensureExplicitCapacity(int minCapacity) {
// 快速报错机制
modCount++; // overflow-conscious code
// 如果修改后需要的最小容量大于当前数据的容量时就需要扩容
if (minCapacity - elementData.length > 0)
// 开始扩容
grow(minCapacity);
}

3、如何扩容的?

  源码中,每次在 add() 一个元素时,ArrayList 都需要对这个 List 的容量进行一个判断。如果容量够,直接添加,否则需要进行扩容,调用 grow() 方法。扩容调用的是 grow() 方法,通过 grow() 方法中调用的 Arrays.copyof() 方法进行对原数组的复制,再通过调用 System.arraycopy() 方法进行复制,达到扩容的目的。

  源码中,可以看出有三种情况:(这里参考 https://blog.csdn.net/u010890358/article/details/80515284)

  (一)如果当前数组是由默认构造方法生成的空数组并且第一次添加数据。此时 minCapacity 等于默认的容量(10),那么根据源码中的逻辑可以看到最后数组的容量会从 0 扩容成 10。而以后的扩容按照当前容量的1.5 倍进行扩容。1.5 倍这里用了右移一位,不明白的可以自行百度。

  (二)如果当前数组是由自定义初始容量构造方法创建并且指定初始容量为 0。此时 minCapacity 等于 1,newCapacity = 0,那么根据下面逻辑可以看到最后数组的容量会从0变成1。这边可以看到一个严重的问题,一旦我们执行了初始容量为 0,那么根据下面的算法前四次扩容每次都 +1,在第5次添加数据进行扩容的时候才是按照当前容量的1.5倍进行扩容。

  (三)如果当扩容量(newCapacity)大于 ArrayList 数组定义的最大值后会调用 hugeCapacity 来进行判断。如果 minCapacity 已经大于 Integer 的最大值(溢出为负数)那么抛出 OutOfMemoryError(内存溢出)否则的话根据与 MAX_ARRAY_SIZE 的比较情况确定是返回 Integer 最大值还是 MAX_ARRAY_SIZE。这边也可以看到 ArrayList 允许的最大容量就是 Integer 的最大值(-2 的 31 次方~ 2 的 31 次方减 1)。

  源码如下:

    //ArrayList 扩容的核心方法,此方法用来决定扩容量并扩容
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;
if (newCapacity - MAX_ARRAY_SIZE > 0)
//当扩容量(newCapacity)大于ArrayList数组定义的最大值后会调用hugeCapacity来进行判断。如果minCapacity已经大于Integer的最大值(溢出为负数)那么抛出OutOfMemoryError(内存溢出)否则的话根据与MAX_ARRAY_SIZE的比较情况确定是返回Integer最大值还是MAX_ARRAY_SIZE。这边也可以看到ArrayList允许的最大容量就是Integer的最大值(-2的31次方~2的31次方减1)。
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;
} // ArrayList 的成员变量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

4、Java 容器的快速报错机制 fail-fast 是什么?    

  请移步: Java集合框架——容器的快速报错机制 fail-fast 是什么?

5、System.arraycopy 怎么使用的?

  请移步: System.arraycopy 怎么使用的?

五、源码解析

1、主要成员变量

    //默认的初始化容量
private static final int DEFAULT_CAPACITY = 10; //空数组,用于 使用构造器给出初始容量为0时的默认空数组
private static final Object[] EMPTY_ELEMENTDATA = {}; //默认的数组,用于 使用默认构造器创建的默认空数组,主要用于后面第一次增加数据时判断是否需要给出默认容量 10 的大小
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //用于存储 ArrayList 的元素,这里就可以看出 ArrayList 的底层就是数组
transient Object[] elementData; // non-private to simplify nested class access //大小
private int size; //记录被修改的次数,用于迭代器迭代时保证数据没有被修改过
protected transient int modCount = 0; //数组大小的最大值
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

2、构造方法(3个)

  默认构造方法:

  注释说实例化了一个容量为 10 的数组,但其实这里返回的是一个空数组,是在数组第一次增加数据时通过扩容达到的初始容量为 10 的数组。前面解惑的2、为什么有两个默认空数组的成员变量?也提到了。

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

  自定义初始容量的构造方法:

  /**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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);
}
}

  生成一个带数据的 ArrayList 实例:

  /**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
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;
}
}

参考:

https://blog.csdn.net/u010890358/article/details/80515284

所有的集合框架:

http://www.runoob.com/java/java-collections.html

https://blog.csdn.net/qq_25868207/article/details/55259978

Java集合框架——jdk 1.8 ArrayList 源码解析的更多相关文章

  1. Java集合框架之二:LinkedList源码解析

    版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! LinkedList底层是通过双向循环链表来实现的,其结构如下图所示: 链表的组成元素我们称之为节点,节点由三部分组成:前一个节点的引用地 ...

  2. Java 集合系列 09 HashMap详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  3. Java 集合系列 10 Hashtable详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  4. Java 集合系列 06 Stack详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  5. Java 集合系列 05 Vector详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  6. Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  7. Java 集合系列之 Vector详细介绍(源码解析)和使用示例

    Vector简介 Vector 是矢量队列,它是JDK1.0版本添加的类.继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口. Vector 继承 ...

  8. java集合框架02——Collection架构与源码分析

    Collection是一个接口,它主要的两个分支是List和Set.如下图所示: List和Set都是接口,它们继承与Collection.List是有序的队列,可以用重复的元素:而Set是数学概念中 ...

  9. Java集合基于JDK1.8的ArrayList源码分析

    本篇分析ArrayList的源码,在分析之前先跟大家谈一谈数组.数组可能是我们最早接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,由于它直接操作内存,所以数组的性能要比集 ...

随机推荐

  1. 《mysql必知必会》学习_第17章_20180807_欢

    第17章:组合查询 P114 select vend_id ,prod_id,prod_price from products where prod_price <=5 ; select ven ...

  2. Linux-程序包管理

    Linux上的软件安装有2种形式:源码.二进制文件,源码需要在编译环境下编译安装,二进制可以直接安装. 1.程序包管理器 rpm 程序包管理器能够将目标二进制格式(也就是从源码编译好的二进制文件,包括 ...

  3. vmware平台下两次网络不通的诡异事件

      首先表明以下两种情况确实很少见,也可以说确实非常奇怪,无法定位原因由于机缘巧合确实出现了,虽然本文没有找到根因,但是希望能帮遇到类似问题的同学一点思绪. RouteOS内网网卡不可用   首先强调 ...

  4. winform判断一个事件是否已经绑定了事件处理函数

    public static class ComponentHelper<T> where T : Control { public static bool HaveEventHandler ...

  5. Linux程序设计:进程通信

      日期:忘了. 关键词:Linux程序设计:System-V:进程通信:共享内存:消息队列. 一.共享内存   1.1 基本知识 (待补充)   1.2 代码 一个基于share memory实现的 ...

  6. 《分布式Java应用与实践》—— 后面两章

    failover? NAT IP-tunneling DSR vrrp gossip 什么是2PC? 什么是3PC? 什么是Pasox? sna? dal? mpi?

  7. 如何使用spring配合mybatis配置多个数据源并应用?

    使用多数据源的场景应该是很多的,如操作同一台服务器上不同的数据库,或者多地机器上的相同或不相同数据库. 虽然涉及到不同数据库时,我们也许可以通过跨库操作的方式,如 other.user 使用同一数据源 ...

  8. Linux 系统资源管理-top-cpu

  9. LeetCode:94_Binary Tree Inorder Traversal | 二叉树中序遍历 | Medium

    题目:Binary Tree Inorder Traversal 二叉树的中序遍历,和前序.中序一样的处理方式,代码见下: struct TreeNode { int val; TreeNode* l ...

  10. [EXP]Apache Tika-server < 1.18 - Command Injection

    #################################################################################################### ...