Java集合——ArrayList源码解析

一 前言

最近去2家大厂面试被虐了,被虐也在意料之中,自己也没有怎么准备,决定回来好好学习,注重细节,多读源码,切勿眼高手低。给我最大感觉是大厂特别重视基础知识,对这些基础知识的深入理解,我们平时做项目的时候,都是会用一些容器,控件,框架,对源码有点了解,但是没有深入解读,半懂不懂,这样在面试官对知识点的层层深入时,就力不从心,结局你懂得,所以我们还是要多注重细节,多读源码。
不说那么多了,我们一起来看看ArrayList源码的源码吧。

二 源码解读

1. ArrayList概述

ArrayList是一个动态数组,与Java中的数组相比,他的容量可以动态增加(1.5倍),我们来看一下ArrayList特点。

  • 快速随机访问
  • 允许存放多个null元素
  • 底层存储是个Object数组
  • 增加元素个数可能很慢(可能需要扩容),删除元素可能很慢(可能需要移动很多元素),对于元素随机访问get和set比较快。
  • 线程不安全

2. ArrayList继承关系

先看一下源码

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

这里写图片描述

  • ArrayList继承于AbstractList,AbstractList继承AbstractCollection实现了List接口。
  • 实现了List接口,那么ArrayList元素是有序的,可以重复的,可以有null元素的集合。
  • 实现了RandomAccess接口,标识着其支持随机快速访问,实际上,查看RandomAccess源码可以看到,是空的啥都没有.ArrayList底层是数组,那么随机快速访问是理所当然的,访问速度O(1)。
  • 实现了Cloneable接口,ArrayList里面的clone()复制其实是浅复制。
  • 实现java.io.Serializable接口,即ArrayList支持序列化,能通过序列化去传输和存储。

3. ArrayList全局变量

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
    // 序列化id
    private static final long serialVersionUID = 8683452581122892189L;
    // 默认初始的容量
    private static final int DEFAULT_CAPACITY = 10;
    // 一个空对象
    private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
    // 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
    // 当前数据对象存放地方,当前对象不参与序列化
    transient Object[] elementData; // 注意点
    // 当前数组长度
    private int size;
    // 数组最大长度
    private static final int MAX_ARRAY_SIZE = 2147483639;
}

我们看到 transient Object[] elementData; 前面有个修饰符 transienttransient标识之后是不被序列化 。
elementData 就是ArrayList的存储容器,为什么不能被序列化?这个地方有点疑惑。
我们在看ArrayList源码中可以看到这个2个方法

    // 序列化
    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();
        }
    }
    // 反序列化
    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(),直接将sizeelement写入ObjectOutputStream;反序列化时调用readObject(),从ObjectInputStream获取sizeelement,再恢复到elementData

为什么不用系统的系列化,自己去重写?

原因:主要是从效率考虑,elementData是一个缓存数组,一般都是会比实际存储大一些,它通常会预留一些容量,等容量不足时再扩充容量(当前容量的1.5倍),那么有些空间可能就没有实际存储元素,采用上面的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。

4. 构造函数

4.1 无参构造方法

如果不传入参数,则使用默认无参构建方法创建ArrayList对象

/**
 * Constructs an empty list with an initial capacity of ten.
 * 构造一个初始容量为10的空列表。
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

我看一下DEFAULTCAPACITY_EMPTY_ELEMENTDATA成员变量

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];

此时我们创建的ArrayList对象中的elementData中的长度是1,size是0,elementData指向了一个空数组,为什么注释却说初始容量为10。
原因在于当进行第一次add的时候,elementData将会变成默认的长度:10。后面我们会具体分析。

4.2 指定初始容量的构造方法
public ArrayList(int initialCapacity) {
     // 如果初始化初始化容量大于0
    if (initialCapacity > 0) {
          // new一个该大小的object数组赋给elementData
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) { // 如果大小为0
          // 将空数组赋给elementData
        this.elementData = EMPTY_ELEMENTDATA;
    } else { // 小于0
          // 则抛出IllegalArgumentException异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

注意事项:如果我们知道ArrayList存储的个数时候推荐使用这个构造方法,避免使用ArrayList的扩容机制而带来额外的开销.

4.3 带参数Collection的构造方法
/**
 * 构造一个包含指定集合元素的列表,元素的顺序由集合的迭代器返回。
 */
 public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray 可能(错误地)不返回 Object[]类型的数组 参见 jdk 的 bug 列表(6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 如果集合大小为空将赋值为 EMPTY_ELEMENTDATA    空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

参数c为一个Collection,Collection的实现类大概有以下几种常用类型:
List:元素可以重复的容器
Set: 元素不可重复的容器
Queue:结构是一个队列,先进先出

构造方法解析

  1. 将collection对象转换成数组,然后将数组的地址的赋给elementData。
  2. 更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData。
  3. 如果size的值大于0,c.toArray 可能(错误地)不返回 Object[]类型的数组 参见 jdk 的 bug 列表(6260652),则执行Arrays.copy方法,把collection对象的内容copy到elementData中。

5. 重要方法

5.1 增加元素和扩容机制
5.1.1 add(E e)方法

作用: 添加指定元素到末尾

/**
* 添加指定元素到末尾
*/
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {           //获取默认的容量和传入参数的较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

如果是以ArrayList()无参构造方法初始化,那么数组指向的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA.第一次add()元素会进入if内部,且minCapacity为1,那么最后minCapacity肯定是10,所以ArrayList()构造方法上面有那句很奇怪的注释.

下面看一下 ensureExplicitCapacity(minCapacity);方法。

//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    //列表结构被修改的次数,用于保证线程安全,如果在迭代的时候该值意外被修改,那么会报ConcurrentModificationException错
    modCount++;

    // 如果最小需要空间比elementData的内存空间要大,则需要扩容 
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

下面我们来分析扩容机制grow(minCapacity)

/**
* 扩容
*/
private void grow(int minCapacity) {
    // overflow-conscious code
    //1. 记录之前的数组长度
    int oldCapacity = elementData.length;
    //2. 新数组的大小=老数组大小+老数组大小的一半
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //3. 判断上面的扩容之后的大小newCapacity是否够装minCapacity个元素,不够就将数组长度设置为需要的长度 
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    //4.判断新数组容量是否大于最大值  如果新数组容量比最大值(Integer.MAX_VALUE - 8)还大,那么交给hugeCapacity()去处理,该抛异常则抛异常
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    //5. 复制数组
    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;
}

// 我们看一下Integer.MAX_VALUE 和MAX_ARRAY_SIZE,MAX_ARRAY_SIZE 就是获取Java中int的最大限制,以防止越界    
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 
public static final int   MAX_VALUE = 0x7fffffff;

add主要的执行逻辑如下:
1)如果原数组是空的,那么第一次添加元素时会给数组一个默认大小10。
确保数组已使用长度size加1之后是否还有容量存储下一个数据。
2)修改次数modCount 标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组,grow方法会将当前数组的长度变为原来容量的1.5倍。
3)确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
4)返回添加成功布尔值。

5.1.2 add(int index, E element)

作用:在ArrayList的index位置,添加元素element

public void add(int index, E element) {
    // 1. 判断index是否越界  
    rangeCheckForAdd(index);
     //2. 判断是否需要扩容
    ensureCapacityInternal(size + 1);  
     //3. 将elementData从index位置开始,复制到elementData的index+1开始的连续空间
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
     // 4.在elementData的index位置赋值element
    elementData[index] = element;
     // 5. 记录当前真实数据个数 
    size++;
}

//index不合法时,抛IndexOutOfBoundsException
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

1)确保数插入的位置小于等于当前数组长度,并且不小于0,否则抛出异常
2)确保数组已使用长度(size)加1之后足够存下 下一个数据
3)修改次数(modCount)标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组
4)grow方法会将当前数组的长度变为原来容量的1.5倍。
5)确保有足够的容量之后,使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位。
6)将新的数据内容存放到数组的指定位置(index)上

5.1.3 添加集合到末尾 addAll(Collection
public boolean addAll(Collection<? extends E> c) {
    //1. 生成一个包含集合c所有元素的数组a
    Object[] a = c.toArray();
    //2. 记录需要插入的数组长度
    int numNew = a.length;
    //3. 判断一下是否需要扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount
    //4. 将a数组全部复制到elementData末尾处
    System.arraycopy(a, 0, elementData, size, numNew);
    //5. 标记当前elementData已有元素的个数
    size += numNew;
    //6. 是否插入成功:c集合不为空就行
    return numNew != 0;
}
5.1.4 添加集合到指定位置 addAll(int index, Collection
public boolean addAll(int index, Collection<? extends E> c) {
    //1. 首先检查一下下标是否越界
    rangeCheckForAdd(index);

    //2. 生成一个包含集合c所有元素的数组a
    Object[] a = c.toArray();
    //3. 记录需要插入的数组长度
    int numNew = a.length;
    //4. 判断是否需要扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount

    //5. 需要往后移的元素个数
    int numMoved = size - index;
    if (numMoved > 0) //后面有元素才需要复制哈,否则相当于插入到末尾
        //6. 将elementData的从index开始的numMoved个元素复制到index + numNew处
        System.arraycopy(elementData, index, elementData, index + numNew,
                            numMoved);
    //7. 将a复制到elementData的index处  
    System.arraycopy(a, 0, elementData, index, numNew);
    //8. 标记当前elementData已有元素的个数
    size += numNew;
    //9. 是否插入成功:c集合不为空就行
    return numNew != 0;
}
5.2 删除元素
5.2.1 移除指定位置元素 remove(int index)
public E remove(int index) {
    //1. 检查参数是否合法
    rangeCheck(index);
    modCount++;
    //2. 记录下需要移除的元素
    E oldValue = elementData(index);

    //3. 需要往前面挪动1个单位的元素个数
    int numMoved = size - index - 1;
    if (numMoved > 0) //后面有元素才挪动
        //4. 将index后面的元素(不包含index)往前"挪动"(复制)一位
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    //5. 这里处理得很巧妙,首先将size-1,然后将elementData原来的最后那个元素赋值为null(方便GC回收)
    elementData[--size] = null; // clear to let GC do its work

    //6. 将旧值返回
    return oldValue;
}

//检查参数是否合法   参数>size抛出IndexOutOfBoundsException   参数小于0则抛出ArrayIndexOutOfBoundsException
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
5.2.2 移除指定元素 remove(Object o)
public boolean remove(Object o) {
    //1. 是否为null
    if (o == null) {
        //2. 循环遍历第一个为null的元素
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                //3. 移除   移除之后就返回true
                fastRemove(index);
                return true;
            }
    } else {
        //4. 循环遍历第一个与o equals()的元素
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                //5. 移除指定位置元素
                fastRemove(index);
                return true;
            }
    }
    return false;
}
/*
私有的方法,移除指定位置元素,其实和remove(int index)是一样的.不同的是没有返回值
*/
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    elementData[--size] = null; // clear to let GC do its work
}
5.3 改动元素(替换指定下标的元素内容) set(int index, E element)
public E set(int index, E element) {
    //1. 检查是否越界
    rangeCheck(index);

    //2. 记录原来该index处的值
    E oldValue = elementData(index);
    //3. 替换
    elementData[index] = element;
    return oldValue;
}
5.4 查询元素(返回指定位置处元素) set(int index, E element)
E elementData(int index) {
    return (E) elementData[index];
}

/**
* 返回指定位置处元素
*/
public E get(int index) {
    // 检查是否越界
    rangeCheck(index);

    return elementData(index);
}
5.5 清空列表(清空当前集合的所有元素) set(int index, E element)
public void clear() {
    modCount++;

    // clear to let GC do its work 
    // 就是将数组所有元素都置为null,然后GC就有机会去把它回收了
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

猜你喜欢

转载自blog.csdn.net/zhangqiluGrubby/article/details/80337748