前言:作为菜鸟,需要经常回头巩固一下基础知识,今天看看 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. IIC通讯协议(非原创,转载他人,用于学习)

    I2C协议:1.空闲状态 2.开始信号 3.停止信号 4.应答信号 5.数据的有效性 6.数据传输 IIC详解 1.I2C总线具有两根双向信号线,一根是数据线SDA,另一根是时钟线SCL 2.IIC总 ...

  2. Android -Services 使用简介

    Android Services 四大组件之一,主要用于后台长时间运行.没有界面.这里讲解两种services的启动还有AIDL通信方式. 1.startservices a.建立继承services ...

  3. poj 2505 A multiplication game

    题目 题意:两个人轮流玩游戏,Stan先手,数字 p从1开始,Stan乘以一个2-9的数,然后Ollie再乘以一个2-9的数,直到谁先将p乘到p>=n时那个人就赢了,而且轮到某人时,某人必须乘以 ...

  4. Hibernate3.0配置

    我的系统Win10(64x),Eclipse jee 2018-09 ,Sql2018版本. 以下是Hibernate3.0配置包 链接:https://pan.baidu.com/s/10Kizby ...

  5. 冲刺博客NO.8

    今天做了什么: 多天学习后,实现了短信验证的功能,可以选择国家,可以在Mob的后台管理短信验证 遇到的困难: 注册回调事件,afterEvent的判定(事件完成后调用)

  6. WCF双工通信单工通信

    1.单工模式 单向通信,指通信只有一个方向进行,即从客户端流向服务,服务不会发送响应,而客户端也不会期望会有响应.这种情况下,客户端发送消息,然后继续执行 运行后报错: 2.双工模式 双工模式的特点是 ...

  7. 剑指offer编程题Java实现——面试题9斐波那契数列

    题目:写一个函数,输入n,求斐波那契数列的第n项. package Solution; /** * 剑指offer面试题9:斐波那契数列 * 题目:写一个函数,输入n,求斐波那契数列的第n项. * 0 ...

  8. 骚年,看我如何把 PhantomJS 图片的 XSS 升级成 SSRF/LFR

    这篇文章实在是太好了,我看了好几篇,所以极力推荐给大家 原文地址   http://buer.haus/2017/06/29/escalating-xss-in-phantomjs-image-ren ...

  9. Spark基础脚本入门实践3:Pair RDD开发

    Pair RDD转化操作 val rdd = sc.parallelize(List((1,2),(3,4),(3,6))) //reduceByKey,通过key来做合并val r1 = rdd.r ...

  10. Liferay7 BPM门户开发之11: Activiti工作流程开发的一些统一规则和实现原理(完整版)

    注意:以下规则是我为了规范流程的处理过程,不是Activiti公司的官方规定. 1.流程启动需要设置启动者,在Demo程序中,“启动者变量”名统一设置为initUserId 启动时要做的: ident ...