• Java学习笔记 ArrayList源码分析


    1.ArrayList简介

    ArrayList底层是用数组实现的,并且它是动态数组,也就是它的容量是可以自动增长的。

    public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
      // 略...  
    }
    
    • 实现RandomAccess接口:所以ArrayList支持快速随机访问,本质上是通过下标序号随机访问。

    • 实现Serializable接口:使ArrayList支持序列化,通过序列化传输。

    • 实现Cloneable接口:使ArrayList能够克隆。

    1.1.底层关键

    ArrayList底层本质上是一个数组,用该数组来保存数据:

    transient Object[] elementData;
    

    transient:Java关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。

    1.2.追踪源码

    1.2.1.重要属性

    // 默认容量
    private static final int DEFAULT_CAPACITY = 10;
    // 返回值Object类型的空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 默认容量空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 存储我们添加的数据,关键
    transient Object[] elementData;
    // 集合数据长度,因为没有给默认值所以就是0
    private int size;
    

    1.2.2.无参构造

    1、在main函数实例化集合对象,并添加元素

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        // 1.首先添加10个元素
        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }
        // 2.再次添加5个元素
        for (int i = 10; i <= 15; i++) {
            list.add(i);
        }
        list.add(100);
        list.add(200);
    }
    

    2、在第一行设断点,进行Debug,进入到该类的无参构造函数:

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    

    可以看到,在构造函数里给存放数据的数组初始化容量大小为空,因为DEFAULTCAPACITY_EMPTY_ELEMENTDATA就是空数组。

    所以这个时候 list = [ ]

    4、第一次走到list.add(i)进入到源码里,如下:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    

    可以看到,add()内部首先调用ensureCapacityInternal()方法,将集合数据长度先加一,再传入,也就是说第一次传入的是值是1,因为原先默认为0。该方法主要确定数组容量是否足够,是否需要扩容,并不涉及元素的添加,非常重要。

    5、进入到该方法当中:

    // minCapacity最小容量,首次为 size + 1 = 1
    private void ensureCapacityInternal(int minCapacity) { 
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    

    可以看到,又调用了方法的重载,并且首先调用calculateCapacity()方法,并且将存放数据的数组和添加数据到当前时刻的大小传入,比如第一次添加数据,minCapatiry的大小等于1,第二次就等于2,然后调用重载方法,将calculateCapacity()计算的返回结果传入。

    6、首先进入到calculateCapacity()

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    • 第一次进入到这里边,elementData是空的,minCapacity = 1,而判断语句就是判断数组是否为空,所以第一次进入到该方法时,这里的判断条件是成立的。

    • 从默认容量DEFAULT_CAPACITY和当前数据大小minCapacity当中取一个最大值返回。因为第一次进入,DEFAULT_CAPACITY = 10minCapacity是传入的size + 1 = 1

    还要一点需要注意的是:如果这里的minCapacity只在集合添加时使用,那直接返回DEFAULT_CAPACITY即可,因为首次添加数据时minCapacity为1,而DEFAULT_CAPACITY为10,一定大于minCapacity,而下次添加数据数组就不为空所以往后该判断均不成立,所以minCapacity不可能会比DEFAULT_CAPACITY 大,之所以还要调用方法挑选最大的返回,是因为ArrayList支持序列化和反序列化,如果直接通过反序列化获得一个集合扔过来,那么minCapacity的值可能是非常大的,这个时候是需要基于当前大小进行再次扩容。

    所以、第一次最终的返回结果就是 10,这个10代表我们需要的数组容量大小,也就是说、首次添加数据发现数组为空,就直接扩容到10的大小。

    6、进入重载方法:

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    • 首先是modCount++,这是记录集合被修改的次数,因为ArrayList是非线程安全的。

    • 接着判断我们需要的最小容量减去当前数据长度是否大于0,如果条件成立,说明实际需要的容量大小已经大于原始数组容量的大小了,所以就调用grow()进行扩容,第一次添加数据,calculateCapacity()返回的结果是return Math.max(DEFAULT_CAPACITY, minCapacity),也就是10,说明我们实际需要的容量为10才够用,但是数组却是空的,所以条件必定成立,然后进行扩容,如下:

    7、进入到grow()方法当中:

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

    如下分析:

    1. 首先获取数组长度,赋值给oldCapacity,也就是0,这是原始容量大小。
    2. 然后进行位运算,向右移一位,也就是原始数据除以2,然后加上oldCapacity,由于是第一次,所以0 + 0 = 0,这是进行位运算后或者是扩容后的新容量大小。
    3. 这一步非常关键,判断扩容后的容量减去最小容量是否小于0,如果条件成立,就将执行newCapacity = minCapacity。也就是执行了newCapacity = 10,最后将新的容量复制给原始数组,也就是将一个空数组扩容到了大小为 10 的数组。

    所以得出结论:初始化ArrayList数组默认为空,添加第一个数据之前将数组大小扩容到10,往后如果超出10,就会基于10进行1.5倍扩容,这里设计的非常绕,绕来绕去的就是为了扩容到10。

    1.2.3.有参构造

    如果使用的是有参构造,则初始化容量为指定大小,如下:

    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);
        }
    }
    

    添加数据调用add()方法,进入calculateCapacity()方法,如下:

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    

    由于实例化的是有参构造,在里边指定了数组大小,这时判断就不成立了,因为数组不为空,而第一次添加数据minCapacity依旧为1。

    接着进入ensureExplicitCapacity()

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    

    下方的条件依旧不成立,第一次添加数据minCapacity等于1,而数组大小可能为我们指定的8,所以不需要扩容。

    所以:如果使用有参构造实例化集合对象,则初始化elementData为指定大小,如果需要扩容,则直接扩容elementData的1.5倍。

  • 相关阅读:
    [C++空间分配]new运算符、operator new、placement new的区别于联系
    [C++STL]stl源码剖析
    [C++基础]在构造函数内部调用构造函数
    Eclipse Memory Analyzer
    zookeeper 学习 zookeeper下载部署
    Doracle.jdbc.J2EE13Compliant=true
    zookeeper 学习 状态机复制的共识算法
    java -d
    轮盘选择法
    OPENJDK 源码编译
  • 原文地址:https://www.cnblogs.com/dcy521/p/15528085.html
Copyright © 2020-2023  润新知