ArrayList数组初始化及扩容

220 阅读5分钟

关键字段

1. private int size

ArrayList的size(所包含的元素个数)。

2. private static final Object[] EMPTY_ELEMENTDATA = {}

给空的ArrayList实例们用的,共享的,空的数组。(有参数,但是initialCapacity = 0 或者 传入的集合长度为0时用的)

3. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}

默认大小的,空的ArrayList实例们用的,共享的,空的数组(参数为空的构造函数用的) 将DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA区别开是为了知道,在之后的操作中当第一个元素被添加进来的时候应该填充多少容量。

4. transient Object[] elementData

ArrayList内元素存放的数组。ArrayList的容量(capacity)即数组的长度。 任何elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA(一个空数组字段)的空数组,在第一个元素被加入进来时,容量会被扩充至DEFAULT_CAPACITY(10)。

构造函数

1. 传入参数指定初始容量的构造函数

用传入的参数作为初始容量

创建指定初始容量的空list

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

判断传入的参数initialCapacity:

  • 若initialCapacity 大于0 ,初始化一个大小为initialCapacity的数组并将elementData指向该数组。
  • 若initialCapacity 等于0 ,将elementData指向EMPTY_ELEMENTDATA
  • 若initialCapacity 小于0 ,抛出IllegalArgumentException异常

2. 无参构造函数

构建一个初始容量为10的空list。 (先是指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,然后在后面用到的时候才真正的初始化elementData,一个懒加载的概念)

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

3. 传入另一个集合作为参数的构造函数

构建一个包含制定集合中的元素的list,顺序为该集合构造器返回的顺序。

    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

将集合转为数组a,将数组长度赋值给size,并且判断长度的值

  • 若数组长度不为0 判断集合是否为arraylist

    • 集合为arraylist,elementData直接等于数组a
    • 集合不为arraylist,elementData等于copy方法返回的,指定长度为size的新数组
  • 若长度为0,elementData指向EMPTY_ELEMENTDATA

In Summary

  • 当未传入参数指定大小时,elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • 当传入参数指定大小,但是传入的参数为0或者数组长度为0时,elementData指向EMPTY_ELEMENTDATA

增加元素的方法

1. 末尾增加元素的add方法

将特定元素添加至末尾的方法

    public boolean add(E e) {
        // step 1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // step 2 注意此处先取size的值再自增!
        elementData[size++] = e;
        return true;
    }

tips: 此处如果构造函数不是走的传入集合的那个,或者传入的集合大小为0,size都没有赋值,默认为0;

  1. 先是走ensureCapacityInternal这个方法,传入的参数size + 1为当前元素长度+1,即增加元素后size应该有的长度。
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

先看作为参数调用的calculateCapacity(elementData, minCapacity),elementData为保存当前元素的数组,minCapacity为增加元素后size应该有的长度:

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
  • 如果elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,也就是刚通过无参构造函数初始化了list,此时是第一次添加数据时:返回DEFAULT_CAPACITY(10)与minCapacity(增加元素后的size应有的长度)二者之间最大值;
  • 返回minCapacity(增加元素后的size应有的长度) calculateCapacity返回DEFAULT_CAPACITY或者size+1,作为参数传给ensureExplicitCapacity:
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

增加元素的方法在此处对modCount的值进行了更新,涉及快速失败机制。

后面做了溢出管理?

判断了增加后的size是否比原先elementData的容量要大,如果是的话进入grow方法为数组扩容,传入的参数为size+1,即之前的元素个数+1,所需的最小容量,否则不扩容:

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

增加容量以确保至少可以持有由最小容量指定的个数那么多的元素

  • oldCapacity赋值为原先的数组长度(elementData.length)
  • newCapacity赋值为oldCapacity + oldCapacity / 2 (即原长度的1.5倍)
  • 如果newCapacity 小于 传入的参数minCapacity,newCapacity = minCapacity。
  • 再判断如果newCapacity小于等于MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8,即2^31 - 1 - 8),如果大于MAX_ARRAY_SIZE,计算hugeCapacity:
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

先是溢出的情况,抛出oom,若minCapacity > MAX_ARRAY_SIZE,返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE;

最后Arrays.copyOf返回一个按照给定长度(newCapacity),复制了原elementData内容

addAll方法与add异曲同工,只不过size+1变成了size+集合长度

即:在newCapacity与minCapacity之间选最大的,然后再与MAX_ARRAY_SIZE做比较,若小于MAX_ARRAY_SIZE则用该值扩容,若大于则计算hugeCapacity并用该值扩容。

elementData不再等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,关于它的判断并且可能返回DEFAULT_CAPACITY的操作不会再执行。

2. 中间插入元素的方法

在list的特定位置插入特定元素。将当前位置上的元素(如果有的话)向右平移,剩下的在右边的元素也都向右(索引+1)。

    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 右边的右移
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 给目标位置赋值
        elementData[index] = element;
        size++;
    }

由代码可见与末尾插入的方法异曲同工,只是多了rangeCheckForAdd方法用以检查索引值是否越界:

    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

总结

  • 若ArrayList通过无参构造函数初始化,第一次增加元素时,将增加元素后的size应有的长度与DEFAULT_CAPACITY(10)做比较,大的那个作为容量。 后续操作不再考虑DEFAULT_CAPACITY,直接使用增加元素后的size应有的长度与原size的1.5倍做比较,选择大的那一方。
  • 有参的构造函数如果initCapacity或者集合的长度不为0,则初始长度为initCapacity或集合的长度,即elementData的长度(oldCapacity)。增加元素时比较oldCapacity的1.5倍与增加元素后的size对比,选大的一方(与上边无参的后续操作一样)
  • 涉及到overflow-conscious的先不考虑。

验证

我们来一一验证上面的结论:

1. 使用无参构造函数的情况

  • 第一次增加元素,增加大小 小于等于10

            List<String> list = new ArrayList<>();
            list.add("apple");
            // 调用无参构造函数,新增元素小于等于10,内部持有的数组扩容至10
            System.out.println(getElementDataLength(list));

预期:elementData大小为10

输出结果:10

注:getElementDataLength为通过反射获取elementData长度的方法。

  • 第一次增加元素,增加大小 大与10

apples为长度为12的String数组。

            List<String> list1 = new ArrayList<>();
            list1.addAll(Arrays.asList(apples));
            // 调用无参构造函数,新增元素大于10,内部持有的数组扩容至新增元素长度
            System.out.println(getElementDataLength(list1));

预期:12

输出:12

  • 容量大小为10(默认)时,继续增加元素至扩充容量(增加12个元素)【该情况与有参函数的情况相同】
            List<String> list = new ArrayList<>();
            list.add("apple");
            list.addAll(Arrays.asList(apples));
            System.out.println(getElementDataLength(list));

预期:15 (同有参构造函数的操作相同)

输出:15

  • 容量大小为10(默认),新增加的元素与之前元素总和超出原容量的1.5倍时,选大的(总和)【该情况与有参函数的情况相同】

            List<String> list = new ArrayList<>();
            // 先来五个,容量扩充至10
            list.addAll(Arrays.asList("apple", "apple", "apple", "apple", "apple"));
            System.out.println(getElementDataLength(list));
            // 再来12个,容量扩充,在原长度的1.5倍(15)以及添加元素后总长度(17)中选择大的(17)。
            list.addAll(Arrays.asList(apples));
            System.out.println(getElementDataLength(list));

2. 使用传入初始大小或集合的构造函数

  • 初始大小传入0
            // 传入初始大小0,初始长度为0,初始长度的1.5倍为0,增加元素后长度为1,二者选大的(1)
            List<String> list2 = new ArrayList<>(0);
            list2.add("apple");
            System.out.println(getElementDataLength(list2));

预期:1

输出:1

  • 初始传入大小,element的大小为传入的值:
            List<String> list2 = new ArrayList<>(21);
            list2.addAll(Arrays.asList(apples));
            System.out.println(getElementDataLength(list2));

预期:21

输出:21

  • 后续情况与上面无参里面摆哦住的情况相同

结束

看了将近一天,不禁思考我是不是看的太过于细节了以至于失去了重点_(:з」∠)_

如有错漏,欢迎指正