ArrayList源码阅读

文章目录

本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习

本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则你会不知道我在说什么

简介

ArrayList 实现了List 接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现 。除该类未实现同步外,其余跟Vector 大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。

例子

ArrayList的API使用起来还是比较简单的,例子的话简单看看就行。

java 复制代码
ArrayList<String> sites = new ArrayList<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Weibo");
System.out.println(sites); // 输出[Google, Runoob, Taobao, Weibo]
System.out.println(sites.get(1));  // 访问第二个元素,输出Runoob
sites.set(2, "Wiki"); // 修改第三个元素为Wiki
sites.remove(3); // 删除第四个元素
System.out.println(sites.size()); // 得到元素个数
// 使用for-each遍历
for (String i : sites) {
  System.out.println(i);
}

继承结构概览

当开始分析一个类之前,肯定得知道它继承了什么类或者接口,因为在该类中可能会调用父类的方法,因此有必要先去了解这个类的继承体系。ArrayList继承于AbstractList,AbstractList继承于AbstractCollection并实现List,AbstractCollection又实现Collection。用图表示如下,隐藏了不太算JCF相关的接口,比如Cloneable,Object这些,只保留集合相关的类和接口:

其中的AbstractCollection用于提供一个实现了Collection的抽象类,提供了一些在实现Collection接口时某些方法的通用实现,类似一个骨架,重复性工作都给你做好了,想要实现Collection的类可以首先考虑继承AbstractCollection。

类似地,AbstractList是List的骨架类,提供了一些在实现List接口时某些方法的通用实现。

不熟悉JCF的基础接口和类的话,可以先看这篇:JCF相关基础类接口/抽象类源码阅读

整明白JCF继承结构之后,看一下完整的继承结构:

也就多了三个标记接口:RandomAccess、Cloneabl、Serializable,标记接口指的是没有任何方法的空接口,比如RandomAccess只是用来给使用ArrayList的用户说明这个类可以高效地实现随机访问。这些标记接口不影响我们分析类,想进一步了解可以参考我的JCF基础的博客,这里不再赘述。

代码分析

成员变量

ArrayList的成员变量很少,只有底层数组和size:

java 复制代码
transient Object[] elementData;
private int size;

因此ArrayList其实就是一个对原始数组的封装,并且内部通过强制类型转换实现Object到泛型的转换。

因为是用数组实现的,数组本身的大小是固定的,当大小不足容下更多的元素,只能重新创建更大的新数组,而且为了避免频繁扩容带来的开销,扩容后的大小总是会 >= 实际元素的个数。因此引出两个概念:capacity和size,前者是数组的大小,即elementData.length,后者则是ArrayList实际存储了的元素个数,存于size变量中。

另外还提前声明了两个静态空数组,作用如下:

java 复制代码
// capacity为0时直接用它,避免重复创建空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于无参构造函数构造时,capacity为0,之后第一次扩容至少要到DEFAULT_CAPACITY。与EMPTY_ELEMENTDATA区分开来
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

方法

方法这块看起来讲得比较乱,其实顺序是按照源码从上到下的顺序讲的,并且函数之间的调用我会先讲被调用的函数,或者串讲。

ArrayList有三种构造函数:

java 复制代码
// 空列表,capacity和size都为0,第一次扩容至少要扩容到DEFAULT_CAPACITY
public ArrayList();

// capacity为initalCapacity,size为0
public ArrayList(int initialCapacity);

// capacity和size都为c.length,保存的元素与c中的相同
public ArrayList(Collection<? extends E> c);

比较直观,不列实现代码了。下面看下其他方法,其中一些实现简单的方法就不细讲

java 复制代码
// 将capacity缩小到刚好能容纳所有元素,具体是通过elementData = Arrays.copyOf(elementData, size)
public void trimToSize();

// 确保数组大小至少为minCapacity
public void ensureCapacity(int minCapacity) {
  // 第二个条件:当elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,至少会扩容到DEFAULT_CAPACITY,因此minCapacity小于DEFAULT_CAPACITY的话,直接忽略本次扩容
  if (minCapacity > elementData.length
    && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
         && minCapacity <= DEFAULT_CAPACITY)) {
    modCount++;
    grow(minCapacity);
  }
}

// 将数组扩容到至少minCapacity
private Object[] grow(int minCapacity) {
  int oldCapacity = elementData.length;
  if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    // 如果数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    // 扩容为oldCapacity的1.5倍(希望扩容的大小),否则扩容到minCapacity
    int newCapacity = ArraysSupport.newLength(oldCapacity,
      minCapacity - oldCapacity, /* minimum growth */
      oldCapacity >> 1           /* preferred growth */);
    return elementData = Arrays.copyOf(elementData, newCapacity);
  } else {
    // 如果数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,至少扩容到DEFAULT_CAPACITY
    return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
  }
}

这里grow(minCapacity)方法中,ArraysSupport.newLength是个什么的呢?简单来说这个函数是基于当前数组长度、希望增长的长度、最少增长的长度,来计算新的数组长度的。当然,里面还有更加细致的考量,了解一下即可:

java 复制代码
// 基于当前数组的长度,计算新的长度
// minGrowth是最少要增长的长度
// prefGrowth是希望增长的长度
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
  int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
  if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
    // 如果希望增长的长度合适,则直接返回
    return prefLength;
  } else {
		// 希望增长的长度不适合,继续计算
    return hugeLength(oldLength, minGrowth);
  }
}

private static int hugeLength(int oldLength, int minGrowth) {
  int minLength = oldLength + minGrowth;
  if (minLength < 0) {
    // minLength > Integer.MAX
    throw new OutOfMemoryError(
        "Required array length " + oldLength + " + " + minGrowth + " is too large");
  } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
    // minLength <= SOFT_MAX_ARRAY_LENGTH,说明prefGrowth > minGrowth但是prefGrowth会导致prefLength > SOFT_MAX_ARRAY_LENGTH,那么让结果尽量接近prefGrowth,直接返回SOFT_MAX_ARRAY_LENGTH
    return SOFT_MAX_ARRAY_LENGTH;
  } else {
    // minLength > SOFT_MAX_ARRAY_LENGTH 并且 minLength < Integer.MAX,倒也可以返回minLength。SOFT_MAX_ARRAY_LENGTH实际上是照顾JVM的实现,因为有的JVM对最大数组大小的限制在Integer.MAX附近,而SOFT_MAX_ARRAY_LENGTH可以保证不会超出大部分JVM最大数组的限制
    return minLength;
  }
}

继续看其他的方法:

java 复制代码
// ArrayList继承了Cloneable,支持克隆一个新的ArrayList。底层数组与克隆的底层数组是两个不同的实例,但里面的元素都是一样的
public Object clone() {
  try {
    // 获取浅拷贝
    ArrayList<?> v = (ArrayList<?>) super.clone();
    // 创建新数组
    v.elementData = Arrays.copyOf(elementData, size);
    // 新的ArrayList,modCount(结构性修改次数)自然是0
    v.modCount = 0;
    return v;
  } catch (CloneNotSupportedException e) {
    // this shouldn't happen, since we are Cloneable
    throw new InternalError(e);
  }
}

// 将元素插入index为下标的位置,原来[index, size)的元素都往后移动
public void add(int index, E element) {
  rangeCheckForAdd(index);
  modCount++;
  final int s;
  Object[] elementData;
  if ((s = size) == (elementData = this.elementData).length)
    elementData = grow();
  // 将从index开始到数组末尾的元素,整体复制到index+1开始的位置
  // remove(index)的实现也是用这个System.arraycopy,只不过是index+1的位置复制到index而已
  System.arraycopy(elementData, index,
                   elementData, index + 1,
                   s - index);
  elementData[index] = element;
  size = s + 1;
}

// 当两个List拥有相同size并且每个元素都equals时返回true
// 如果o不是ArrayList(但仍是List)则使用迭代器遍历,如果是ArrayList的话,使用fori遍历
public boolean equals(Object o) {
  if (o == this) {
    return true;
  }

  if (!(o instanceof List)) {
    return false;
  }

  final int expectedModCount = modCount;
  boolean equal = (o.getClass() == ArrayList.class)
      ? equalsArrayList((ArrayList<?>) o)
      : equalsRange((List<?>) o, 0, size);

  checkForComodification(expectedModCount);
  return equal;
}

// clear名为删除所有元素,在ArrayList中实现为将数组每个引用置空,使得将来可以复用数组
public void clear() {
  modCount++;
  final Object[] es = elementData;
  for (int to = size, i = size = 0; i < to; i++)
    es[i] = null;
}

还有几个可能稍微难理解一点的方法:

java 复制代码
// 相当于删除在[lo, hi)间的元素
private void shiftTailOverGap(Object[] es, int lo, int hi) {
  System.arraycopy(es, hi, es, lo, size - hi);
  for (int to = size, i = (size -= hi - lo); i < to; i++)
    es[i] = null;
}

// 当complement=false,删除在[from, end)间,且存在于c中的元素
// 当complement=true,删除在[from, end)间,且不存在于c中的元素
boolean batchRemove(
  Collection<?> c, boolean complement,
  final int from, final int end) {
  Objects.requireNonNull(c);
  final Object[] es = elementData;
  int r;
  // 确定从哪开始删除:这个for循环完了之后,r保存第一个将被删除元素的下标
  for (r = from;; r++) {
    if (r == end)
      return false;
    if (c.contains(es[r]) != complement)
      break;
  }
  // w保存将要保留的元素的下标
  // 这是实现删除数组中某些元素的时候常用的双指针算法,目的是保持数组元素连续:w和r分别为两个指针,r负责遍历(r无条件自增),当r指向的元素符合条件的时候存到w的位置,w自增(不符合条件的话w保持当前值)
  int w = r++;
  try {
    for (Object e; r < end; r++)
      if (c.contains(e = es[r]) == complement) // 遍历到要删除的元素
        es[w++] = e;
  } catch (Throwable ex) {
    // 如果contains抛异常,为了避免数组出现空洞做的善后处理
    // 放弃遍历[r, end)这堆没遍历完的元素,直接把他们前移到w指向的位置
    System.arraycopy(es, r, es, w, end - r);
    w += end - r;
    throw ex;
  } finally {
    modCount += end - w;
    // 处理完元素之后[w, end)这段会出现空洞,把[end, size)前移进来
    shiftTailOverGap(es, w, end);
  }
  return true;
}

// 删除[i, end)区间中所有满足filter的元素
// 与batchRemove大同小异,不同之处只是在于条件通过filter.test判断
boolean removeIf(Predicate<? super E> filter, int i, final int end) {
  Objects.requireNonNull(filter);
  int expectedModCount = modCount;
  final Object[] es = elementData;
  // 确定从哪开始删除:这个for循环完了之后,i保存第一个将被删除元素的下标
  for (; i < end && !filter.test(elementAt(es, i)); i++)
    ;
  if (i < end) {
    // 与batchRemove的不同:先标记所有符合条件(将被删除)的元素,然后才删除
    // 这样做的原因在于:filter.test传入的是当前遍历到的列表元素,但其可能不仅依赖于传入的元素来决定返回值为true还是false,而是依赖于整个列表(也就是可能会依赖于其他的元素)。因此在要保证在filter遍历的过程中列表是可重入读的,即不能遇到filter.test返回true就立刻删除,这样对于下一次filter.test来说列表已经发生了改变。而是用deathRow先标记将要删除的元素,遍历完之后再一次性删除。
    final int beg = i;
    final long[] deathRow = nBits(end - beg);
    deathRow[0] = 1L;
    for (i = beg + 1; i < end; i++)
      if (filter.test(elementAt(es, i)))
        setBit(deathRow, i - beg);
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
    modCount++;
    int w = beg;
    // 双指针w和i
    for (i = beg; i < end; i++)
      if (isClear(deathRow, i - beg))
        es[w++] = es[i];
    shiftTailOverGap(es, w, end);
    return true;
  } else {
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
    return false;
  }
}

第一个函数shiftTailOverGap不用多说

第二个函数batchRemove简单总结就是用来移除数组中包含于c/不包含于c中的元素,并且函数退出之后,数组不能出现空洞,即数组留下来的元素得保持连续,不然的话之后这个数组还怎么使用hhhhh。

第三个函数removeIf与batchRemove是一样的思想,只不过移除的是"符合条件的元素",用途更为广泛一点。batchRemove相当于是其的一个特例,条件为"是否包含于/不包含于c"。然后还得保证predict(filter)的可重入性,也就是该方法对于调用者有一个规范/假设:对每个元素进行predict的时,列表结构不能发生改变,更具体的解释可以看注释。

至此,基本的方法介绍得差不多了,都比较简单,下面来看一下迭代器和子列表相关的东西。ArrayList没有使用AbstractList提供的默认迭代器类和子列表类,而是自己重新实现了一套,至于为什么,我想是为了直接操纵底层数组提高运行效率(注释也说了An optimized version of AbstractList.Itr)。

先看迭代器:

迭代器

java 复制代码
private class Itr implements Iterator<E> {
  int cursor;       // index of next element to return
  int lastRet = -1; // index of last element returned; -1 if no such
  int expectedModCount = modCount;
  
  // ...
}

private class ListItr extends Itr implements ListIterator<E> {
  // ...
}

还是熟悉的配方。。。参考我JCF博客关于AbstractList.Itr和AbstractList.ListItr的分析就行了,大同小异。不同之处只是ArrayList直接访问底层数组,而AbstractList借助get方法访问。

迭代器是fail-fast的,意思是会在运行过程中检测结构性修改,如果有的话直接抛出ConcurrentModificationException异常,而不是继续迭代遍历。这一点其实在AbstractList.Itr也是一样的。

但注意ArrayList不是并发安全的,因此用户不能完全依赖于fail-test来判断有没有发生结构性修改,可能在某个时间点检查是正常的,在这个时间点之后发生了结构性修改无法再检测出来,fail-fast只是尽最大努力去发现结构性修改。总结下来即:抛异常则一定发生了结构性修改,但没抛异常不一定意味着没发生结构性修改,因此这个特性只能用来debug,或者说这个是一个heuristic的功能(启发式的,意味着不一定准确)。

子列表

java 复制代码
private static class SubList<E> extends AbstractList<E> implements RandomAccess {
  private final ArrayList<E> root;
  private final SubList<E> parent;
  private final int offset;
  private int size;
}

成员变量还是熟悉的配方,里面的方法都与AbstractList.SubList的大同小异,基本上类似根据菜谱做菜(先放油、再放菜、放盐、放葱)那样,有的方法看着有点长,其实就是步骤比较多:先访问元素、修改指针、检查并发访问...就不再介绍了。

总结

整体来说ArrayList没什么难点,掌握核心思想,即:ArrayList就是对数组进行封装的类,并通过扩容机制(每次扩容1.5倍)实现了动态修改大小的特性。其中有的方法看起来很长,其实是维护变量罢了,理解核心思想的话,并不难分析。还有几个方法比如batchRemove、removeIf可能有些让人疑惑的点,比如双指针算法、标记删除法,读懂就好了。

最后ArrayList不是并发安全的,进一步来说,里面有的方法可能会抛出ConcurrentModificationException,但是不抛出这个异常的情况下,不代表着没有"异常"。

参考链接

「博客园」JCF相关基础类接口/抽象类源码阅读

「博客园」J源码中transient的用途

「Java全栈知识体系」Collection - ArrayList 源码解析

相关推荐
吾日三省吾码2 小时前
JVM 性能调优
java
弗拉唐3 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi774 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3434 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀4 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20205 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深5 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
shuangrenlong5 小时前
slice介绍slice查看器
java·ubuntu
牧竹子5 小时前
对原jar包解压后修改原class文件后重新打包为jar
java·jar