ArrayList核心重点源码解析

本文重点分析ArrayList源码,探讨其扩容、增删等机制。介绍了判断扩容的方法,通过grow方法实现1.5倍扩容及优化策略;指出因无锁导致线程不安全,增删慢是因需移动元素;还提及手动调用trimToSize()方法可触发缩容机制,释放多余空间。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ArrayList是一个可动态添加删除数据的集合,底层数据结构是数组。当添加数据的容量大于底层数组容量时则会产出扩容,即通过生成数组来实现。它的主要核心就是扩容机制(当插入时所需要的长度超过数组原本的长度时则需要扩容)。本文主要抓ArrayList的重点分析。

接下来抱着这几个问题来分析一下ArrayList的源码

1.ArrayList怎么判断是否需要扩容的?

2.ArrayList是怎么扩容的,怎么操作数组实现的?

3.ArrayList在扩容做了什么优化?

4.ArrayList为什么是线程不安全的?

5.ArrayList为什么增删慢(源码角度看)?

6.ArrayList的缩容机制是怎样的?

ArrayList类与成员变量:

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

//默认初始容量
private static final int DEFAULT_CAPACITY = 10;

//默认元素集合
private static final Object[] EMPTY_ELEMENTDATA = {};

//无参构造实例默认元素集合
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//存放元素的数组
transient Object[] elementData;

//记录ArrayList中存储的元素的个数
private int size;

构造函数:

//可指定大小的构造函数
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);
        }
    }

//默认构造函数
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

//传入一个为Collection的对象
public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

三个构造函数目的都是为初始化elementData,一般我们使用中间默认的即可。如需使用其它的可按情况使用,比如当我们知道需要使用的容量那就用第一个指定容量大小,避免频繁扩容。

插入add():

//从尾部插入数据
public boolean add(E e) {
        //判断是否扩容进行扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //在数组后面添加元素e
        elementData[size++] = e;
        return true;
    }

ensureCapacityInternal方法为了判断是否扩容,跟进去

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

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0) //如果所需容量大于现数组容量,则进行扩容
            //扩容方法
            grow(minCapacity);
    }

这里我们就可以解决上面说的第1个问题了

根据所需容量是否大于现数组容量来进行扩容,调用grow方法,继续跟进去

grow方法是ArrayList扩容的核心方法:

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        
        //增加1.5倍的长度
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        
        //当增加1.5倍后还是不够所需要长度,则直接用所需长度来扩容
        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 + (oldCapacity >> 1)(右移左移运算的思路:先转二进制计算后再转回十进制)来实现扩容1.5倍的长度,再通过Arrays.copyOf实现扩容,这里就解决了第2个问题。

第3个问题的解决也在这里,我们可以看到,它是先扩容1.5倍,当增加后还是不够用,则直接使用所需要的长度作为数组的长度。为什么呢?这里ArrayList是做了优化,它先扩1.5倍,如果这次够用下次再来扩容可能就可以不用扩容。如果每次都用所需要的长度来扩容,那么以后每次增加元素都会进行一次扩容操作,当增加后还是不够用的时候,ArrayList无法知道到底给你多少容量才合适,所以就直接使用你所需的长度。扩容的时候底层是需要操作数组的(Arrays.copyOf),经常扩容会消耗性能的。

我们看到ArrayList里面的方法都没有添加锁,即当多个线程同时调用的话会会引发不可预知的错误。这是第4个问题的

删除remove方法:

public boolean remove(Object o) {
        //如果传入元素为null,则循环判断元素为null的进行删除
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);    //删除
                    return true;
                }
        } else {           //循环数组进行判断删除
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);    //删除
                    return true;
                }
        }
        return false;
    }

循环整个数组,判断是否相等进行删除。因为ArrayList 允许空值,所以源码这里进行了多一次的判断是否为null的情况。可以看到核心删除方法是fastRemove。

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
    }

这个核心方法的思路很简单,定位到当前需要删除的元素位置,然后把后面元素的位置+1顶上来一个位置,,然后把最后一个位置设置为null,size同时减1,实现删除。就好比一根三节棍,把中间那截给删了,原来后面的就顶上来跟前面的接在了一起,成了两节棍。

注:其实从这里就可以解决第5个问题,在它删除元素时候后面的元素需要移动上去,采用了System.arraycopy复制,当移动多了自然性能就低了,这就是删除慢的原因。为什么插入慢呢,我想大家都明白了,删除的时候后面得向前移动,那插入的时候同理得向后移动。(通过指定位置插入元素的情况,因为ArrayList可以从尾部,也可以指定位置插入)。

思考一个问题,当我们需要向ArrayList增加大量元素时它会扩容成一个大数组,当我们不需要那么多元素的时候把它删了,那数组虽然元素被删了它还是会在内存中占了空间,好比一个大盆子装一个小鸡蛋。ArrayList没有自动来处理这个问题,它提供了一个方法trimToSize()方法。我们可以手动调用此方法触发ArrayList 的缩容机制。这样就可以释放多余的空间,提高空间利用率。

public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

size是记录元素数据,当size(元素数量)小于此数组的长度(elementData.length),说明数组有空闲空间位置没有被使用,那就把数组转换成长度当前元素数量的长度。这就是第6个问题ArrayList的缩容机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值