ArrayList原理剖析

提问

  1. 基于什么实现的?数组?链表?队列?
  2. 为什么能一直add元素?

分析

实现方式

定义的变量:

维护了一个数组:

transient Object[] elementData; // non-private to simplify nested class access

private int size;

ArrayList内部所有的add、remove、set、get都是对elementData这个数组进行操作,所以ArrayList是基于数组实现的没错了。

两个长度:size ====>当前列表的长度 elementData.length ====>数组长度
数组的长度 ≥ List的长度

默认长度和两个默认数组:

/**
 * 默认的数组长度,当我们直接创建一个ArrayList对象时,容量为10
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 直接创建无参ArrayList时,内部指向该数组
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 创建带初始长度的ArrayList,或者传入另一个列表为参数创建对象时,如果长度为0或者传入列表长度为0,内部指向该数组
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
构造方法

ArrayList定义了三个构造方法,分别为无参构造,传入一个int类型参数构造方法,传入一个列表作为参数构造方法。

  1. 传入初始长度,这个方法一般用在我们知道列表长度的情况下,避免申请过多无用内存空间
public ArrayList(int initialCapacity) {
    //如果长度大于0,则创建该长度的数组
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
    //长度为0,指向默认数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
  1. 无参构造,将elementData指向默认数组,长度为0
  2. 传入一个列表。通过Arrays.copyOf()方法将传入的列表copy到新的数组,然后elementData指向该地址

操作方法

各个方法的内部实现,都是对数组的操作

get

直接取出数组对应下标的值

public E get(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    return (E) elementData[index];
}
set

同样的对数组操作,将数组中该下标对应的值替换为新的值

public E set(int index, E element) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    E oldValue = (E) elementData[index];
    elementData[index] = element;
    return oldValue;
}
add

下标为当前size+1,将需要add的值设置为数组中该下标对应的值。因为数组长度是固定的,所以其中涉及到最核心重要的扩容增长策略,ensureCapacityInternal(size + 1)方法

public boolean add(E e) {
    //扩容算法,传入当前长度+1
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //扩容完成后赋值
    elementData[size++] = e;
    return true;
}
addAll

方法,首先将列表参数转为数组,然后使用System.arraycopy方法将该数组拷贝到elementData中,其中同样涉及到扩容增长策略,只是传入的参数不一样

public boolean addAll(Collection<? extends E> c) {
    //先转为数组
    Object[] a = c.toArray();
    int numNew = a.length;
    //扩容算法,传入当前长度+需要add的元素的数量
    ensureCapacityInternal(size + numNew);  // Increments modCount
    //拷贝数组
    System.arraycopy(a, 0, elementData, size, numNew);
    //列表长度修改
    size += numNew;
    return numNew != 0;
}
remove

同样是对数组进行了拷贝操作,类似于将数组中需要移除的元素后每一个元素向前移动一位,覆盖掉原来的值。

public E remove(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    modCount++;
    E oldValue = (E) elementData[index];

    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

    return oldValue;
}

扩容

前面addaddAll方法中,会有 ensureCapacityInternal1方法进行扩容算法
先看扩容相关代码:

//①传入列表长度值
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}
//②如果计算后需要的长度大于当前数组的长度,执行扩容操作
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

//③对数组进行扩容
private void grow(int minCapacity) {
    // overflow-conscious code
    //获取当前长度
    int oldCapacity = elementData.length;
    //计算新长度为旧长度 * 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果新长度小于需要的长度,使用传入的长度,适用于第一次创建后添加元素的扩容和addAll方法元素很多超过原有1.5倍的情况下
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //如果计算后长度大于最大列表长度
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //通过copyOf方法将原有数据拷贝到一个新的数组对象中,再赋值给elementData,至此,扩容完成。
    elementData = Arrays.copyOf(elementData, newCapacity);
}

分析:

  1. 首先我们传入了一个值,代表我们列表在add或者addAll之后的长度
  2. 不能每次add就扩容一次数组,否则花销太大,所以对数组的扩容不会每次长度只增加一。
  3. 如果当前为空数组,取默认值和传入值中的最大值。为了使第一次直接创建长度为10的数组,否则增加一个元素扩容一次,花销大
  4. 计算是否需要进行扩容
  5. 扩容操作,正常情况下,数组容量扩充1.5倍,两种特殊情况:
    1. 第一次add元素,直接扩容为默认长度10
    2. addAll元素很多,加起来长度超过原有1.5倍,直接扩容到该长度
  6. 通过数组拷贝方法将原有所有数据拷贝到扩容后的新数组中,最后将该数组赋值给elementData
  7. 扩容完成

总结

  1. ArrayList是基于数组实现的,数组默认长度是10,所有操作都是对所维护的数组的操作
  2. add、addAll方法都有可能触发数组扩容
  3. 长度不够时,扩容长度一般为现有长度的1.5倍
  4. 扩容、删除等都是通过拷贝数组方式实现的,所以列表不要太长,否则每次的拷贝对性能消耗太大!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值