ArrayList原理解读

本文深入剖析ArrayList的特性,包括快速随机访问、可存储null、扩容机制、元素添加与删除的效率,以及构造方法的使用。详细解释了ArrayList的扩容策略,如何在添加元素时动态调整容量,并探讨了线程安全和序列化问题。同时,介绍了添加、删除、修改和查询元素的具体实现,以及迭代器的内部工作原理。
摘要由CSDN通过智能技术生成

一,前言

        首先个人发布文章仅用于个人学习记录,仅提供参考,如有说的不对的地方还需阅读者自行理解,此文章有参考其他优秀作者的成分。

二,ArrayList的特点

        1,可快速随机查询元素

        2,允许存放多个null值

        3,底层是Object数组

        4,对于元素的新增操作可能很慢(可能需要扩容)

        5,对于元素的删除操作可能很慢(可能需要移动很多元素)

        6,修改对应索引的元素很快

三,ArrayList继承关系

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{}

1,继承了AbstractList,这个类提供了List接口的骨干实现,最大限度地减少实现"随机访问"数据存储(如数组)支持的该接口所需的工作,对于连续的访问数据(如链表),应优先使用 AbstractSequentialList

2,实现了List接口,代表ArrayList是有序的,可重复的,可存null元素的集合

3,实现了RandomAccess接口,标识着其支持快速的随机访问(其实RandomAccess源码什么都没有定义,其能够快速的随机访问主要是因为ArrayList的底层存储元素用的是数据)

4,实现了Cloneable接口,标识着其可以复制(ArrayList的clone()方法其实是浅复制)

5,实现了Serializable接口,标识着其可以序列化

四,ArrayList构造方法

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 = {};

    /**
     * 另一个共享空数组实例,用的不多,用于区别上面的EMPTY_ELEMENTDATA
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 存放元素的容器
     */  
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 当前存放元素的个数,并不代表数组大小
     */
    private int size;

}

 这是ArrayList主要的成员变量,我们可以看到存放元素的数组有一个transient关键字进行修饰,我也不懂这个关键字的作用,一番百度过后,其大致意思就是:“不被序列化”

看到这个关键字,我有个不理解的困惑,ArrayList是实现了Serializable接口的,是可序列化的,但存放元素的数组是不被序列化的,那一旦被序列化岂不是会丢失数据???

经过一番百度,其实ArrayList在被序列化的时候会调用writeObject()方法,把我们关心的size,elementData写入到ObjectOutputStream,在反序列化的时候再调用readObject()方法,从ObjectOutputStream中获取size,elementData再恢复到elementData中

这么做的原因是因为elementData是一个缓存数组,它会预留一些容量,等容量不足的时候再扩充容量,那么这个数组可能有些空间根本就没有存储元素,使用上面的序列化方式就可以保证只序列化实际存储的元素而不是数组本身,从而节省空间,时间

1,无参构造方法

/**
 * 构造一个初始容量为10的空列表。
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

可以看到,当我们使用无参构造创建一个ArrayList的时候,它默认指向一个空数组,默认初始化容量为10(后面揭晓)

2,指定初始容量的构造方法

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
          //容量大于0 创建数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
          //容量等于0 指向空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
          //容量是一个负数 抛异常
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
}

如果我们预先知道一个集合元素的容纳的个数的时候推荐使用这个构造方法,避免使用ArrayList默认的扩容机制而带来额外的开销。(相信大部人都没用过这个构造)

3,使用另一个集合 Collection 的构造方法

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray可能不返回Object[]类型的数组,这是jdk的bug 
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 如果集合大小为0,指向空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

五,添加元素,扩容机制

/**
* 添加指定元素到末尾
*/
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    //如果是用ArrayList无参构造方法初始化,那么数组指向的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA.第一次add()元素会进入if内部,
    //且minCapacity为1,那么最后minCapacity肯定是10,所以ArrayList无参构造方法上面的默认用量为10就是在这里实现的
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    //记录被修改的次数,用于保证线程安全,如果在迭代的时候该值意外被修改,那么会报ConcurrentModificationException错(这就是为什么在遍历的时候不能增删改元素会报错的原因)
    modCount++;

    //判断是否需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    //1. 记录之前的数组长度
    int oldCapacity = elementData.length;
    //2. 新数组的大小=老数组大小+老数组大小的一半
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //3. 判断上面的扩容之后的大小(newCapacity)是否够装(minCapacity)个元素
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    //4.判断新数组容量是否大于最大值
    //如果新数组容量比最大值(Integer.MAX_VALUE - 8)还大,那么交给hugeCapacity()去处理
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    //5. 复制数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}


private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0)
        //溢出则抛一个内存溢出异常
        throw new OutOfMemoryError();
 
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

1,每次调用add方法添加元素到数组末尾都会去判断数组会不会溢出(数组容量装不下当前要添加的元素)

2,如果elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的话,那么第一次添加元素会给数组一个默认大小10(使用无参构造构建ArrayList)

3,记录被修改的次数,用于线程安全(不可一边遍历查询,一边增删改元素)

4,判断是否需要扩容

5,匹配扩容规则,扩容规则:新数组长度 = 旧数组长度的1.5倍,最大容量Integer.MAX_VALUE

6,复制数组

六, 添加元素到指定位置

public void add(int index, E element) {
    //1. 索引合法性校验,抛出索引越界异常
    rangeCheckForAdd(index);

    //2. 判断是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //3. 将elementData从index处开始,复制size - index个元素复制到elementData的index + 1处
    //相当于index处以及后面的元素往后移动了一位
    System.arraycopy(elementData, index, elementData, index + 1,
                        size - index);
    //4. 将元素放到index处,填坑
    elementData[index] = element;
    //5. 记录当前真实数据个数
    size++;
}

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

1,进行索引合法性校验,这就是抛出索引越界异常的地方

2,判断是否需要扩容容量

3,将数组index处后面的元素往后移动一位

4,将元素存放到index处进行填坑

5,记录当前数据量

七,添加集合到末尾

public boolean addAll(Collection<? extends E> c) {
    //1. 将传进来的集合转换成Object数组(此处传进来的集合为null就会报空指针异常)
    Object[] a = c.toArray();
    //2. 需要插入的数据量
    int numNew = a.length;
    //3. 判断一下是否需要扩容
    ensureCapacityInternal(size + numNew);
    //4. 将a数组全部复制到elementData末尾处
    System.arraycopy(a, 0, elementData, size, numNew);
    //5. 记录最新数据量
    size += numNew;
    //6. 是否插入成功(传进来集合不为空)
    return numNew != 0;
}

1,将传进来的集合转为Object数组,此处有空指针隐患

2,判断是否需要扩容

3,将传进来的集合转换成的数组全部复制到elementData的末尾处

4,记录集合中最新的数据量

八,添加集合到指定位置

public boolean addAll(int index, Collection<? extends E> c) {
    //1. 索引合法性校验,抛出索引越界异常
    rangeCheckForAdd(index);

    //2. 将传进来的集合转换成Object数组(此处传进来的集合为null就会报空指针异常)
    Object[] a = c.toArray();
    //3. 需要插入的数据量
    int numNew = a.length;
    //4. 判断是否需要扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount

    //5. 需要往后移的元素个数
    int numMoved = size - index;
    
    //后面有元素才需要复制,否则插入到末尾
    if (numMoved > 0)
        //6. 将elementData的从index处开始复制numMoved个元素到index + numNew处
        System.arraycopy(elementData, index, elementData, index + numNew,
                            numMoved);

    //7. 将a复制到elementData的index处  
    System.arraycopy(a, 0, elementData, index, numNew);
    //8. 记录最新数据量
    size += numNew;
    //9. 是否插入成功(传进来集合不为空)
    return numNew != 0;
}
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

1,索引合法性校验,抛出索引越界异常

2,将传进来的集合转换成Object数组

3,判断是否需要扩容

6,将elementData的从index处开始复制numMoved个元素到index + numNew处

7,将传进来的集合转换成的Object数组复制到elementData的index处(如果坑位原先有值则是覆盖值操作)

8,记录集合最新数据量

九,移除指定位置元素

public E remove(int index) {
    //1. 索引合法性校验,抛出索引越界异常
    rangeCheck(index);
    
    modCount++;
    //2. 取出要移除的元素,保存起来
    E oldValue = elementData(index);

    //3. 计算需要往前面移动1位的元素个数
    int numMoved = size - index - 1;
    
    //后面有元素才挪动
    if (numMoved > 0)
        //4. 将index处后面的元素往前移动1位(复制数据并覆盖index处的值)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);

    //5. 将数组末尾处的值置为null,方便gc回收
    elementData[--size] = null;

    //6. 将旧值返回
    return oldValue;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

1,索引合法性校验,抛出索引越界异常

2,取出要移除的元素保存起来

3,计算要往前移动1位的元素个数

4,将index处后面的元素往前移动1位,这个操作相当于是覆盖之前坑位的值

5,将数组末尾处的值置为null,方便gc回收,因为整个数组从index+1处到末尾处的值移动到了index处,所以最后一个坑位是重复值,没有意义,需要释放内存(所谓的移动数组我觉得:覆盖之前坑位的值更容易理解)

十,移除指定元素

public boolean remove(Object o) {
    //1. 是否为null
    if (o == null) {
        //2. 循环遍历获取第一个为null的元素
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                //3. 移除元素
                fastRemove(index);
                return true;
            }
    } else {
        //4. 循环遍历获取第一个与o equals()的元素
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                //5. 移除元素
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    modCount++;
    
    //1. 计算需要往前移动1位的元素个数
    int numMoved = size - index - 1;
    //2. 后面有元素才需要移动
    if (numMoved > 0)
        //3. 将index处后面的元素往前移动1位(复制数据并覆盖index处的值)
        System.arraycopy(elementData, index+1, elementData, index,
                           numMoved);
    //4. 将数组末尾处的值置为null,方便gc回收
    elementData[--size] = null;
}

1,遍历elementData,查找相等的第一个元素

2,获取元素在elementData中的索引,移除元素

3,计算要往前移动1位的元素个数

4,将index+1处的元素复制到index处

5,将数组末尾的元素置为null,方便gc回收(重复的末尾值,没有存在意义)

十一,移除所有包含在指定集合中的元素

public boolean removeAll(Collection<?> c) {
    //判断传进来的集合是否为null 抛出空指针
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    //r 是记录整个数组下标的, w是记录有效元素索引的
    int r = 0, w = 0;
    boolean modified = false;
    try {
        //2. 循环遍历数组
        for (; r < size; r++)
            //3. 如果complement为false  相当于是取c在elementData中的补集,c包含则不记录,c不包含则记录
            //如果complement为true  相当于是取c和elementData的交集,c包含则记录,c不包含则不记录
            if (c.contains(elementData[r]) == complement)
                //r是正在遍历的位置,w是用于记录有效元素的,在w之前的全是有效元素,w之后的会被删除
                elementData[w++] = elementData[r];
    } finally {
        //4. 如果上面在遍历的过程中出错了,那么r肯定不等于size,就将出错位置r后面的元素全部放到w后面
        if (r != size) {
            System.arraycopy(elementData, r,
                                elementData, w,
                                size - r);
            w += size - r;
        }
        //5. 如果w是不等于size,代表有需要删除元素的,否则就是找不到要删除的元素
        if (w != size) {
            //6. 将w之后的元素全部置空  因为这些已经没用了,置空方便GC回收
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            //7. 记录最新数据量
            size = w;
            //8. 标记已修改
            modified = true;
        }
    }
    return modified;
}

1,检查传进来的集合是否为null,为null则抛出空指针异常

2,循环遍历elementData数组,判断是否需要移除这个元素,不需要移除的元素就放到elementData的头部(w的初始值为0,不断的修改w++元素的值,把不需要移除的元素都存放到w++的位置)

3,判断r != size,不等于就是遍历过程中出错,把r后面的元素放到w后面

4,判断w != size,不等于就是有找到要删除的元素,就把elementData中w处开始以及后面的元素置为null,方便gc回收(w处以及后面的元素都是重复值,没有意义)

5,更新最新数据量,更新已修改标记

十二,清除集合

public void clear() {
    modCount++;

    //循环遍历,逐个索引处置为null
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

1,可以看到,清除集合的做法很简单,就是一个循环遍历,每个索引处的值置为null,方便gc回收

十三,移除区间内的所有元素

protected void removeRange(int fromIndex, int toIndex) {
    modCount++;
    //1. 计算toIndex后面要保存下来的元素个数
    int numMoved = size - toIndex;
    //2. 将toIndex后面的元素复制到fromIndex处
    System.arraycopy(elementData, toIndex, elementData, fromIndex,
                        numMoved);

    //3. 将有效元素后面的元素置空
    int newSize = size - (toIndex-fromIndex);
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    //4. 记录最新数据量
    size = newSize;
}

1,计算区间尾部要保留下来的元素个数

2,将区间尾部的元素复制到区间头部位置

3,计算最新的数据量,把最新数据量后面的元素置为null,方便gc回收(因为值已被复制到区间头部位置,后面的都是重复值,没有意义)

十四,改动元素

public E set(int index, E element) {
    //1. 索引合法性校验,抛出索引越界异常
    rangeCheck(index);

    //2. 获取旧值并保存
    E oldValue = elementData(index);
    //3. 替换最新值
    elementData[index] = element;
    //4. 返回旧值
    return oldValue;
}

1, 索引合法性校验

2,获取旧值,替换新值,返回旧值

十五,获取元素

E elementData(int index) {
    return (E) elementData[index];
}

public E get(int index) {
    //1. 索引合法性校验
    rangeCheck(index);
    //2. 返回索引处的元素
    return elementData(index);
}

1,索引合法性校验,抛出空指针

2,返回数组中索引处的值

十六,迭代器遍历

public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    //下一个元素索引
    int cursor;
    //访问的最后一个元素的索引
    int lastRet = -1;
    int expectedModCount = modCount;

    /**
     * 判断是否有下一个元素
     */
    public boolean hasNext() {
        //拿下一个元素的索引跟集合数组大小作比较,大于或者等于数组长度就是没有下一个元素,小于数组长度代表还有下一个元素 
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        //1. 判断一下该列表是否被其他线程改过,修改过则抛异常(迭代过程中修改,删除,添加元素)
        checkForComodification();
        //2. 第一次的时候是等于0,从0开始往后取数据
        int i = cursor;
        //3. 如果索引越界,则抛异常
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        //4. 下一个元素的索引=当前元素索引+1
        cursor = i + 1;
        //5. 将索引对应的元素返回
        return (E) elementData[lastRet = i];
    }

    /**
     * 移除当前访问到的元素
     */
    public void remove() {
        //1. 索引合法性校验
        if (lastRet < 0)
            throw new IllegalStateException();
        //2. 判断一下该列表是否被其他线程改过,修改过则抛异常(迭代过程中修改,删除,添加元素)
        checkForComodification();

        try {
            //3. 移除当前访问到的元素
            ArrayList.this.remove(lastRet);
            //4. 更新下一个元素的索引
            cursor = lastRet;
            //5. 更新当前访问到的元素的索引
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    /**
     * 快速遍历元素
     */
    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        //1. 空指针校验
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        //2. 判断是否需要继续遍历
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        //3. 如果索引越界,则抛异常
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }

        //4. 循环遍历,不断回调consumer.accept()把元素返回
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        //5. 更新下一个元素索引,当前访问元素索引
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

    /**
     * 判断一下该列表是否被其他线程改过,修改过则抛异常(迭代过程中修改,删除,添加元素)
     */
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

1,以前用迭代器的时候就觉得好厉害,当看了代码,也就那么回事,无非就是不断的索引+1的操作,或许这就是进步吧。

ArrayList特点

1,底层用Object数组来存储元素

2,有扩容机制,默认扩容机制是10,每次扩容都是扩容到之前的1.5倍

3,添加元素到指定位置可能会移动很多元素并且可能会触发扩容机制,如果是添加元素到末尾那么只可能触发扩容机制

4,删除指定位置的元素可能会移动很多元素,删除末尾元素代价是最小的,ArrayList删除元素是将末尾元素置为null

5,查询或者修改某个具体位置的元素是很快的

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值