ArrayList源码分析 原创
春节不停更,此文正在参加「星光计划-春节更帖活动」
日积月累,水滴石穿 😄
前言
本文环境 jdk 1.8
ArrayList 这个集合容器我相信是 Java 工程师在工作当中用的最常用的集合了。但用的多并不代表就很了解它,对它的了解程度应该只有这样:ArrayList 和 LinkedList 有什么区别。
那本文就来分析一下ArrayList的源码,深入的理解ArrayList实现原理。让你回答面试题的时候,让面试官觉得你不同于那些妖艳贱货。
定义
public class ArrayList<E> extends AbstractList<E> implements 
List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList 本身直接继承AbstractList,实现List、 RandomAccess、 Cloneable、 Serializable接口。
- RandomAccess:标记接口,标注该类可随机访问
 - Cloneable:可以克隆拷贝
 - Serializable:可序列化
 
构造函数
在使用 ArrayList 的时候,都是用使用构造函数进行初始化的。分别来介绍一下它的几个构造函数。
无参构造
- 使用
 
ArrayList<String> list = new ArrayList<>();
- 源码
 
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
elementData和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 都是成员变量,一起来看看:
// Object 数组,用来存放元素
transient Object[] elementData; 
// 空的 Object数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
看到上述无参构造方法的源码,可以知道当我们 new 一个无参 ArrayList 的时候,内部使用了一个空的数组赋值给了用于保存数据的数组。
ArrayList(int initialCapacity)
- 使用
 
ArrayList<String> list = new ArrayList<>(4);
- 源码
 
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);
  }
}
该构造函数可以传入一个 int 值,如果该值大于 0,则创建一个长度为该值的数组;
如果该值等于 0,则使用一个空数组;如果该值小于 0,则抛出IllegalArgumentException异常。
ArrayList(Collection c)
- 使用
 
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("程序员小杰");
ArrayList<String> list = new ArrayList<>(arrayList);
- 源码
 
public ArrayList(Collection <? extends E > c) {
  elementData = c.toArray();
  if((size = elementData.length) != 0) {
    if(elementData.getClass() != Object[].class) 
      elementData =
        Arrays.copyOf(elementData, size, Object[].class);
  }
  else {
    this.elementData = EMPTY_ELEMENTDATA;
  }
}
该构造函数可以传入一个 Collection,将传入的集合转换为数组,然后将该数组赋值给成员变量elementData,将elementData的长度赋值给成员变量 size,判断是否不等于 0 ,如果传入的集合中有元素该判断是成立的,然后再判断elementData.getClass() != Object[].class,这不会成立的。所以结束方法。如果等于零则将EMPTY_ELEMENTDATA赋值给 elementData。
在这段源码中又有两个新的成员变量:
//ArrayList包含的元素个数
//list.size() 方法返回的值就是它 
private int size;
// 空的 Object数组
//EMPTY_ELEMENTDATA 与 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
//的值是一致的,但是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
//有别的用处(扩容),所以专门建了一个变量进行区分
private static final Object[] EMPTY_ELEMENTDATA = {};
add方法
add(E e)
在集合末端添加元素
ArrayList<String> list = new ArrayList<>();
list.add("程序员小杰");
- 源码
 
public boolean add(E e) {
  //确保内部容量能放下元素,如果容量不够,则进行扩容
  //传入当前 size + 1
  ensureCapacityInternal(size + 1); 
  //注意 size++ 中 ++ 的位置
  elementData[size++] = e;
  return true;
}
add方法中就三行代码,代码很简单,就只分析 ensureCapacityInternal方法。
- ensureCapacityInternal
 
private void ensureCapacityInternal(int minCapacity) {
  ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
在方法中调用了ensureExplicitCapacity方法,而ensureExplicitCapacity方法调用了calculateCapacity方法。
- calculateCapacity
 
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
判断元素集合是不是等于空数组,如果是则返回 DEFAULT_CAPACITY。DEFAULT_CAPACITY的默认值为 10。使用无参构造方式进行实例化ArrayList,ArrayList 默认的容量长度为 10。
- ensureExplicitCapacity
将 10 传入ensureExplicitCapacity方法,而这时elementData.length的长度为0,所以会调用扩容方法。 
private void ensureExplicitCapacity(int minCapacity) {
  //记录修改次数,判断是否出现并发修改异常
  modCount++;
  //如果传过来的值大于数组长度,则执行grow方法,也就是进行扩容
  if(minCapacity - elementData.length > 0) grow(minCapacity);
}
- grow
grow这个方法是ArrayList中最重要的方法。都说ArrayList是基于数组实现的,但又不同于数组,ArrayList可以动态扩容,就是使用grow实现的。 
private void grow(int minCapacity) {
    //扩容前元素集合的长度 可能为 0
    int oldCapacity = elementData.length;
    //进行右移一位,也就是除 2的 1 次方
    //假如 oldCapacity 现在为 12 
    // newCapacity = 12 + (12/2) = 18
    //也可以理解新容量为 为旧的容量的1.5倍
    
    //当然旧容量的值需要是偶数,才能保证是 1.5倍。
    //比如:oldCapacity 现在为 11 
    // 11  + (11/2) = 16
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果新容量小于最小容量,按照最小容量进行扩容
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
        
    //将老数组中的元素拷贝到扩容后新的数组中
    elementData = Arrays.copyOf(elementData, newCapacity);
}
如果我们使用无参构造方法进行实例化 ArrayList,即使只添加一个元素都会进行扩容操作。
Arrays.copyOf
单独拎出来实践一下。
Integer[] arr = {1,2,3,4,5};
System.out.println(Arrays.toString(arr));
arr = Arrays.copyOf(arr, 10);
System.out.println(Arrays.toString(arr));
结果:
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5, null, null, null, null, null]
是不是对此方法有点理解了。
add(int index, E element)
在指定下标添加元素
ArrayList<String> list = new ArrayList<>();
list.add(0,"程序员小杰");
- 源码
 
public void add(int index, E element) {
  //检查下标是否越界,越界抛出
 // IndexOutOfBoundsException 异常
  rangeCheckForAdd(index);
  //是否需要扩容
  ensureCapacityInternal(size + 1);
  //进行元素移动
  System.arraycopy(elementData, index, elementData, index + 1, size -
    index);
  elementData[index] = element;
  size++;
}
相对于add(E e)方法,指定index下标添加元素的 add方法稍微复杂点。
- 检查下是否越界
 - 是否需要扩容
 - 进行数组元素移动
 - 将新元素放置到空的
elementData[index]位置 size自加
进行元素移动的时候,调用了System.arraycopy方法,这是一个被 native修饰的静态方法。
public static native void arraycopy(Object src, int srcPos, Object dest,
  int destPos, int length);
参数含义如下:
- src:源数组
 - srcPos:源数组中要复制的起始位置,从哪个下标开始复制。
 - dest:目标数组
 - destPos:目标数组放置复制数据的起始位置,从哪个下标开始放。
 - length:复制的长度
 
总结
通过阅读上述两个add的源码,可以总结出以下几点:
- 使用无参构造方法实例化
ArrayList,默认的容量为 10。 - 扩容之后的容量为旧容量的1.5倍。
 - 如果元素个数确定的情况下,尽量使用 
ArrayList(int initialCapacity)构造方法进行实例化ArrayList,可以减少扩容次数。 - 指定下标插入元素时,需要进行元素的移动。而直接添加元素是放置在元素的末端。
 ArrayList的元素可以为null。
还有两个可以添加元素的方法addAll,方法跟上面逻辑差不多,就不再单独拎出来了,各位小伙伴可以自己去看看。相信我,很简单。
remove 方法
remove(int index)
删除指定下标的元素,时间复杂度为 o(1)。
public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>(2);
    list.add("程序员小杰");
    list.add("源");
    list.remove(1);
    System.out.println(Arrays.toString(list.toArray()));
}
结果:
[程序员小杰]
- 源码
 
public E remove(int index) {
  //检查越界
  rangeCheck(index);
  modCount++;
  //获得指定下标的元素
  E oldValue = elementData(index);
  //计算需要删除元素下标 后面还有多少个元素
  //比如: 15 - 10 - 1 = 4
  int numMoved = size - index - 1;
  //如果 numMoved 大于 0,说明需要删除的元素不是数组中最后一个
  if(numMoved > 0) 
  //进行元素移动 跟add正好相反,所有元素向前移动 1 位
  System.arraycopy(elementData, index + 1,
    elementData, index, numMoved);
    
   //将最后一个元素置为 null
  elementData[--size] = null; 
  //返回被删除的元素
  return oldValue;
}
- 检查下标是否越界,越界则抛出 
IndexOutOfBoundsException - 获得指定下标的元素
 - 计算出需要移动的次数,如果大于 0 ,说明要删除的元素不是数组中最后一位,则调用 
System.arraycopy方法进行元素移动。
 - 将最后一个元素赋值为
null, 可以被gc回收。 - 返回被删除的元素
 
remove(Object o)
按照传入的元素删除,删除匹配到的第一个元素。时间复杂度为 o(n)。
ArrayList<String> list = new ArrayList<>(4);
list.add("程序员小杰");
list.add("源");
list.add("程序员小杰");
list.remove("程序员小杰");
System.out.println(Arrays.toString(list.toArray()));
结果:
[源, 程序员小杰]
- 源码
 
public boolean remove(Object o) {
  if(o == null) {
    for(int index = 0; index < size; index++)
      if(elementData[index] == null) {
      //跟上述 `remove(int index)`方法差不多
        fastRemove(index);
        return true;
      }
  }
  else {
    for(int index = 0; index < size; index++)
      if(o.equals(elementData[index])) {
      //跟上述 remove(int index) 方法差不多
        fastRemove(index);
        return true;
      }
  }
  return false;
}
- 当传入元素的值为空时,遍历数组删除第一个为空的元素,并返回 true。
 - 当传入元素的值不为空时,遍历数组删除第一个与传入元素值相等的元素,并返回 true。
 - 上述两个逻辑都不符合,返回 
false。 
注意:remove 方法并不会改变元素容量的长度,即 elementData.length,只会修改 size 的值。比如 elementData 中有 15 个元素,在调用 remove 方法之前 elementData.length = 15,size = 15,调用 remove 方法之后elementData.length = 15,size = 14。
set 方法
修改指定下标元素的值。
public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
- 检查下标是否越界,越界则抛出 
IndexOutOfBoundsException 
- 获得指定下标的元素
 
- 将传入的值放到指定的 index 上
 - 将 
oldValue返回 
get方法
返回指定下标的元素,时间复杂度为 o(1)。
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}
- 检查下标是否越界,越界则抛出 
IndexOutOfBoundsException 
- 获得指定下标的元素
 
clear 方法
将数组中所有元素置为 null,但并不会改变数组容量的大小;
并将 size置为 0。
public void clear() {
    modCount++;
    for (int i = 0; i < size; i++)
        elementData[i] = null;
    size = 0;
}
contains 方法
判断集合中是否包含指定元素
public boolean contains(Object o) {
    return indexOf(o) >= 0;
}
方法内部调用 indexOf方法,
public int indexOf(Object o) {
//传入元素为 null
    if (o == null) {
        for (int i = 0; i < size; i++)
        //返回等于null的第一个元素下标
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}
- 当传入元素的值为空时,遍历数组,返回第一个等于 null 的元素下标。
 
- 当传入元素的值不为空时,遍历数组,返回第一个与传入元素值相等的元素下标。
 - 上述两个逻辑都不符合,返回 -1。
 
trimToSize 方法
能有效的节约内存。将数组容量的大小变为实际数据的个数,例如:数组容量为10,但只有前三个元素有值,其他为空,那么调用该方法之后,数组的容量变为3。
ArrayList<String> list = new ArrayList<>();
list.add("程序员小杰");
list.add("源");
list.add("源yaun");
list.trimToSize();
- 源码
 
public void trimToSize() {
  modCount++;
  if(size < elementData.length) {
    elementData = (size == 0) ? EMPTY_ELEMENTDATA : 
    Arrays.copyOf(elementData, size);
  }
}
ArrayList 与 Vector 有什么区别
相同点
都是基于数组实现,保证元素的有序行,可以基于下标查询,并且默认容量都是 10。
不同点
- 1、ArrayList 是线程不安全的,而 Vector 中的方法都被 
synchronized标注,是线程安全的。 - 2、在进行扩容时,ArrayList 是扩容 1.5 倍,而 Vector 是默认是扩容 2 倍,但也可以指定扩容数量。
 




















