前言:作为菜鸟,需要经常回头巩固一下基础知识,今天看看 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. YUM仓库安装LAMP与LNMP

    LAMP动态网站部署架构是由一套 Linux+Apache+MySQL+PHP 组成的动态网站系统解决方案. LNMP动态网站部署架构是由一套 Linux+Nginx+MySQL+PHP 组成的动态网 ...

  2. linux下编写C++程序播放音频

    参考: https://blog.csdn.net/zlyaxixuexi/article/details/79014441 格式转换: https://www.media.io/zh/

  3. 用jQuery修改右键菜单

    概述 以前在网上找过屏蔽右键菜单的代码,也找过屏蔽F12的代码,今天无意之中看到别人的右键菜单很有意思,我也想来搞一个. 思路 建立一个菜单并且隐藏起来. 用window.oncontextmenu屏 ...

  4. Java学习笔记32(集合框架六:Map接口)

    Map接口与Collection不同: Collection中的集合元素是孤立的,可理解为单身,是一个一个存进去的,称为单列集合 Map中的集合元素是成对存在的,可理解为夫妻,是一对一对存进去的,称为 ...

  5. 【深入 MongoDB 开发】使用正确的姿势连接分片集群

    MongoDB分片集群(Sharded Cluster)通过将数据分散存储到多个分片(Shard)上,来实现高可扩展性.实现分片集群时,MongoDB 引入 Config Server 来存储集群的元 ...

  6. 21天打造分布式爬虫-Selenium爬取拉钩职位信息(六)

    6.1.爬取第一页的职位信息 第一页职位信息 from selenium import webdriver from lxml import etree import re import time c ...

  7. Java高并发之线程池详解

    线程池优势 在业务场景中, 如果一个对象创建销毁开销比较大, 那么此时建议池化对象进行管理. 例如线程, jdbc连接等等, 在高并发场景中, 如果可以复用之前销毁的对象, 那么系统效率将大大提升. ...

  8. 理解Array.prototype.fill和Array.from

    之所以将这两个方法放在一起说,是因为经常写这样的代码: Array.from({length: 5}).fill(0),看起来很简洁,但是踩到坑之后才发现自己对这两个方法实在是不求甚解. Array. ...

  9. [java初探总结篇]__java初探总结

    前言 终于,java初探系列的学习,要告一阶段了,java初探系列在我的计划中是从头学java中的第一个阶段,知识主要涉及java的基础知识,所以在笔记上实在花了不少的功夫.虽然是在第一阶段上面花费了 ...

  10. Nginx 的两种认证方式

    简介: 今天来研究一下 Nginx 的两种认证方式. 1.auth_basic 本机认证 2.ngx_http_auth_request_module 第三方认证 一.安装 Nginx shell & ...