Loading

ArrayList源码分析

不分析也不行啊...分析了都找不着工作...

属性

/**
 * 默认容量,当前大小以及当前数组元素,没啥好说的
 */
private static final int DEFAULT_CAPACITY = 10;
private int size;
transient Object[] elementData;

/**
 * 一眼看过去就是享元设计:
 *    假如你调用了new ArrayList(0),这时ArrayList需要为你准备一个底层空数组,为了不让系统中每个刚创建的空ArrayList都创建自己的空数组,所以这里用静态常量做享元设计,类似Boolean.FALSE、Redis中的小数字字符串
 *    加入你调用了new ArrayList(),也就是使用默认容量`10`,那这时使用`DEFAULTCAPACITY_EMPTY_ELEMENTDATA`
 */
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

享元设计

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 传入了>0的初始容量,那就创建自己的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 如果传入0,就使用EMPTY_ELEMENTDATA,所有这样的ArrayList都共享这个实例
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                            initialCapacity);
    }
}

public ArrayList() {
    // 如果没传入初始容量,就使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

add

ArrayList中的size代表当前你放置在其中的元素数,但它并不是底层数组的实际容量,所以在添加之前,首先调用ensureCapacityInternal来确保目前底层数组具有足够的容量(size+1

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

这里注意参数的命名,minCapacity,代表当前需要底层数组的最小容量。然后又调用了ensureExplicitCapacity,并使用calculateCapacity计算参数。

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

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

calculateCapacity很简单,就是为了保证如果当前是使用默认容量10的话,取10和当前需要的容量中最大的一个返回,这种代码我们在源码分析里可以忽略,屁用没有。

然后,ensureExplicitCapacity就去判断,需要的底层数组容量如果大于底层数组的真实容量,就grow

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

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

其实到了现在都很简单,md真的不知道我要看这个源码干啥,为啥java现在这么难混...每天早上三点钟焦虑醒,真的要受不了了

总结

总结一下,add的时候就判断底层数组容量是否足够,不够就grow

grow

private void grow(int minCapacity) {
    // overflow-conscious code (考虑了溢出情况)
    int oldCapacity = elementData.length;
    // 新容量等于旧容量的1.5倍 相当于newCapacity = oldCapacity + oldCapacity / 2
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 如果增长完容量反而比需要还少,这里可能是发生了溢出,或者更加可能的是上次一次性添加了很多,超出1.5倍了
    // 这时,取minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新容量比最大数组容量还大,调用这个`hugeCapacity`得到新数组容量,注意,传入的是需要的最小容量而不是计算出的新容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // 使用经过一系列计算得到的newCapacity来扩容数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

MAX_ARRAY_SIZEhugeCapacity

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

MAX_ARRAY_SIZE为啥是Int最大值减8?首先,Java中的数组使用整形做下标,所以理应,数组的最大长度应该是Integer.MAX_VALUE,减8是因为某些JVM可能在数组上保留一些头部字,如果你真的创建了Integer.MAX_VALUE这么大的数组,可能就会发生OOM,所以这里预留了8个。

我们要注意,传入hugeCapacity的是当前所需要的最小容量,而不是简单一拍脑门计算出来的1.5倍的新容量。如果所需最小容量minCapacity < 0,明显就是整数发生了溢出变成了负的,证明我们不能创建再大的数组了,直接OOM,否则,如果需要的容量比最大数组长度还大(就是Integer最大值减8),那就尝试使用Integer.MAX_VALUE分配,否则还是用最大数组值来分配。

hugeCapacity存在的目的,就是为了保证在非必要的情况下,不将底层数组扩容至MAX_ARRAY_SIZE之上,因为在一些JVM中可能会发生OOM,如果计算出的1.5倍的newCapacity已经超过了,就尝试判断下需要的最小容量minCapacity是否真的超过了MAX_ARRAY_SIZE,如果没有就不将它扩展到MAX_ARRAY_SIZE之上,而是让它正好等于MAX_ARRAY_SIZE

总结

如果底层数组长度小于最小需要容量

  1. 尝试将数组扩大1.5倍
  2. 如果1.5倍还不够,就使用足够情况下的最小长度
  3. 如果决定扩容后的新数组长度过大,超过Integer.MAX_VALUE - 8,为了保证不在一些JVM上发生OOM,ArrayList会判断实际的需求是否真的需要这么多
  4. 如果不需要,则使用Integer.MAX_VALUE - 8
  5. 否则,没办法了,摆烂!使用Integer.MAX_VALUE

最后总结

  1. add的时候就判断底层数组容量是否足够,不够就grow
  2. grow时首先尝试将数组扩大1.5倍
  3. 如果1.5倍还不够,就使用足够情况下的最小长度
  4. 如果决定扩容后的新数组长度过大,超过Integer.MAX_VALUE - 8,为了保证不在一些JVM上发生OOM,ArrayList会判断实际的需求是否真的需要这么多
  5. 如果不需要,则使用Integer.MAX_VALUE - 8
  6. 否则,没办法了,摆烂!使用Integer.MAX_VALUE
posted @ 2022-09-29 07:39  yudoge  阅读(55)  评论(0编辑  收藏  举报