前言:作为菜鸟,需要经常回头巩固一下基础知识,今天看看 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. Forward团队-爬虫豆瓣top250项目-模块测试

    项目托管平台地址:https://github.com/xyhcq/top250 模块测试:爬虫对信息的处理部分 测试方法: 实际运行一下代码: 可以看见,信息都已经爬取出来了 其他补充说明: 原本系 ...

  2. Beta冲刺 (4/7)

    Part.1 开篇 队名:彳艮彳亍团队 组长博客:戳我进入 作业博客:班级博客本次作业的链接 Part.2 成员汇报 组员1(组长)柯奇豪 过去两天完成了哪些任务 共享编辑文章的后端数据处理 展示Gi ...

  3. Do More With These Great Plugins for Windows Live Writer(old)

    This article is out of day,now we use open live wirter, but we don’t have so much works great plugin ...

  4. 使用Python对Twitter进行数据挖掘(Mining Twitter Data with Python)

    目录 1.Collecting data 1.1 Register Your App 1.2 Accessing the Data 1.3 Streaming 2.Text Pre-processin ...

  5. Python之旅Day2 元组 字符串 字典 集合

    元组(tuple) 元组其实跟列表差不多,也是存一组数,与列表相比,元组一旦创建,便不能再修改,所以又叫只读列表. 语法: names = ("Wuchunwei","Y ...

  6. 1.SpringMVC入门

    创建一个web工程 导入jar 配置web.xml 在web.xml配置前端控制器:DispatcherServlet <?xml version="1.0" encodin ...

  7. Spring 使用javaconfig配置aop

    1.在xml中需要配置自动代理 /** * */ package com.junge.demo.spring.dao; import org.springframework.context.annot ...

  8. hadoop安装hive及java调用hive

     1.安装hive 在安装hive前,请确保已经安装好了hadoop,如未安装,请参考centoos 安装hadoop集群进行安装: 1.1.下载,解压 下载hive2.1.1:http://mirr ...

  9. 写一个shell 快速启动停止你的微服务吧

    在这个微服务盛行的时代,docker获得了巨大的成功,因为我们需要在一台服务器装上N个服务. 本文不是想讨论如何使用docker,而是,在一台服务器安装了多个服务后,怎样启动方便的启动服务呢? 一.在 ...

  10. PHP利用Session实现上传进度

    实现文件上传进度条基本是依靠JS插件或HTML5的File API来完成,其实PHP配合ajax也能实现此功能. PHP手册对于session上传进度是这么介绍的: 当 session.upload_ ...