Java集合之ArrayList源码分析

JAVA集合之ArrayList源码分析

1. 概述

ArrayList是一个动态数组序列,能动态增长和缩减容量。允许存入空值和重复元素,基于数组实现,具备快速随机查找功能。非线程安全。本文针对jdk1.8从源码角度分析ArrayList结构,初始化,容量,以及扩容。

2. 源码分析

2.1 继承结构

打开源码我们可以看到ArrayList继承的类与实现的接口


分析:

  • 继承AbstractList,而AbstractList实现List接口
    为何ArrayList不直接实现List接口?
    思考一下,接口与抽象类的区别:接口中全是抽象方法,而抽象类可以有抽象方法,也可以有具体实现方法。
    与ArrayList一样继承AbstractList的还有LinkedList和Vector,它们应该都有共同的通用方法,让这些通用方法在AbstractList里做具体实现,它们在自己的具体类中直接拿到这些通用方法,自己再实现自己特有的方法。这样能够减少代码重复,让代码更加简洁。这是我们在日常写代码时也应该具备的思维方式。
  • 实现List接口
    AbstractList已经实现了List接口,为何ArrayList还要再实现一次?
    这个做法让我很迷惑。如果有找到答案的朋友,希望可以给我留言。
  • 实现RandomAccess接口
    查看文档说明

    RandomAccess是标记接口,用来表示支持快速随机访问,文档中也说明实现了该接口后,for循环遍历性能最优。
  • 实现Cloneable接口
    实现Cloneable接口,就可以使用Object.clone()进行克隆了。
  • 实现Serializable接口
    实现序列化接口,说明该类能够启用其序列化功能。序列化其实就是持久化,将class转变为字节流传输,然后保存,反序列化就是读取。

2.2 类属性

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 版本号
    private static final long serialVersionUID = 8683452581122892189L;
    // 缺省容量
    private static final int DEFAULT_CAPACITY = 10;
    // 空对象数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 缺省空对象数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 元素数组
    transient Object[] elementData;
    // 实际元素大小,默认为0
    private int size;
    // 最大数组容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

2.3 构造方法

ArrayList有三个构造方法,分别为:
ArrayList()
ArrayList(int)
ArrayList(Collection<? extends E>)

2.3.1 无参构造
    /**
     * Constructs an empty list with an initial capacity of ten. 初始化容量为10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }


在构造这个ArrayList对象的时候,将缺省空数组对象赋予elementData,但是在空参构造函数的备注中却说明初始化容量为10,这一点在下面的add()方法分析。

2.3.2 有参构造1


允许传入一个int类型作为ArrayList的容量,实则都是初始化一个数组对象,该参数作为数组的长度。这里分别判断给定长度的数值大于零,等于零,小于零的情况,当然,数组长度不应该有负值,所以小于零的情况做了异常处理,抛出非法数值异常。

2.3.3 有参构造2


这里的参数是允许传入一个泛型Collection<>。作用就是所有继承自该泛型的子类都可以转化为ArrayList<>
elementData = c.toArray();这里做了数组转换
进而判断该长度不等于零、等于零的情况,这里做判断的原因是不同的Collection的toArray()的实现不同,要对Collection进行改造,变成我们想要的Object[]。
长度等于零的时候统一改造成之前已经定义好的EMPTY_ELEMENTDATA
长度不等于零的时候就判断该Collection属不属于Object[]类型,不属于就使用Array.copyOf重新进行改造

2.3.4 小结

ArrayList构造方法都是在初始化容器,而这个容器本质就是一个数组,在这里被定义为elementData

2.4 增

ArrayList主要的特点是动态扩容,增加元素操作有四个方法,这里只分析add()方法

2.4.1 add(e)

先看看add()方法

注释中说明add方法往list末尾追加一个元素
ensureCapacityInternal(size + 1); 添加了一个元素,容量变化+1
再看看这个方法怎么做的

    private void ensureCapacityInternal(int minCapacity) {
        //这里判断elementData是不是一个空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
            //如果是,则将minCapacity变成10,也就是先前定义好的DEFAULT_CAPACITY(默认容量)
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        
        ensureExplicitCapacity(minCapacity);
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code  //当前数组长度比数组elementData容量要大时调用grow()
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

看到这里,就可以理解在本文2.3.1无参构造中ArrayList作者为何说明容器初始容量为10了,到这里,很显然只是初始化容器容量的值minCapacity,还未对elementData数组初始化容量。接着看源码,当minCapacity - elementData.length > 0(存在剩余容量的情况)调用grow()方法,这个方法就很有趣了,接着看!

定义一个oldCapacity(旧容量)
int newCapacity = oldCapacity + (oldCapacity >> 1); 这是实现ArrayList动态扩容的核心步骤,oldCapacity>>1位移运算,相当于0.5倍,加上自身则为1.5倍增长

if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;

此处又将新容量newCapacity与minCapacity比较,哪个大就作为最新容量

当超过最大值(最大值为Integer最大值-8)时,调用hugeCapacity()

这里扩容也很简单,以MAX_ARRAY_SIZE作为临界,当前数组长度比 MAX_ARRAY_SIZE大时,容量扩展到Integer的最大值,否则扩展为MAX_ARRAY_SIZE

为什么是-8?而不是直接用Integer最大值,这里涉及到JVM的知识,数组作为对象,需要一定的内存存储自身的对象头信息,而对象头的信息最大占用内存为8 Bytes

再回到grow()最后一步

//到这一步才真正完成elementData的扩容
elementData = Arrays.copyOf(elementData, newCapacity);

ArrayList的扩容机制大致流程如下:

2.4.2 add(int,e)

先看add(int,e)源码

很明显,调用的rangeCheckForAdd(index)方法,是对index越界的检查

接着调用System.arraycopy()方法,看看该方法的文档说明

看到这里,不难理解ArrayList为何插入元素效率会低,每插入一个元素都会拷贝一份新的数组,同时进行元素的位移。当然这种结构带来的优势在于根据索引获取元素的效率杠杠地。

2.4.3 小结

add(e)方法会在集合末尾添加指定的元素,允许存入空值和重复元素,容量会以原容量的1.5倍动态扩展。
add(int, e)方法在指定位置插入指定元素,指定位置到末尾的元素会进行位移

2.5 删

删除操作有五个方法如下:

2.5.1 remove(int)


rangeCheck(index): 对下标做检查,越界异常处理
E oldValue = elementData(index); 保存好要删除的元素,函数结束时返回
int numMoved = size - index - 1; 计算出要移动的元素个数
删除元素也要进行位移,与插入元素原理一样,也是调用System.arraycopy()
最后将集合最后位置设置为null,上面作者的注释也可以看到,设置为null让GC快点把它干掉。

2.5.2 remove(Object)


remove(Object)方法允许传入Object类型的参数,说明ArrayList支持放入null值 这里做了遍历查看elementData中是否有指定的这个元素,会调用fastRemove(index)方法:

可以看到fastRemove(index)的实现基本和remove(index)一样,区别在于它没有返回值。

2.5.3 removeAll(Collection<?> c)

这是一个批量删除的操作,与之相似的还有另一个方法retainAll(Collection<?> c)


它们都调用了batchRemove()方法,我们看看这个方法做了什么

private boolean batchRemove(Collection<?> c, boolean complement) {
    //将原数组在这里重新定义一份,并声明为final,出了该方法,新定义的elementData就无效了
    final Object[] elementData = this.elementData;
    int r = 0, w = 0; //r:控制循环 w:纪录交集元素个数
    boolean modified = false;
    try {
        // 下面会用图解展示变化过程
        for (; r < size; r++)
            // conntains()判断 c集合是否包含elementData[r]元素
            if (c.contains(elementData[r]) == complement)
            // 这里complement用的非常精巧
            // 当complement为true的时候(也就是调用retainAll()时),w代表的是纪录交集元素个数
            // 当complement为false的时候(调用removeAll()时),w则代表的是无法删除的元素个数
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        // 当contains()出现异常时,r才会不等于size,这里是为了兼容AbstractCollection中contains()方法的实现
        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        //从w作为索引位置遍历逐个设置为null,让GC干掉
        if (w != size) {
            // clear to let GC do its work
            // 下面会用图解展示变化过程
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

下面用图解示例— e: 原集合[1,2,3,4,5] c: 指定集合[2,5,6]
看看以上代码两个for循环的变化过程:

removeAll(Collection<?> c) 调用 batchRemove(c, true) complement为true:

retainAll(Collection<?> c) 调用 batchRemove(c, false) complement为false:

从以上图解可以看到removeAll会删除掉两集合交集的所有元素,retainAll则会让原集合只保留两交集部分,它们都会返回true,只有当原集合没有任何改动的时候,才会返回false

2.5.4 小结

remove方法,在删除指定下标的元素时,从下标位置到末尾的元素都会往前移位,同时将最后一个元素设置为null,让GC清理
removeAll方法会删除掉原集合中所有包含指定集合的元素
retainAll方法会删掉原集合与指定集合的补集,只保留交集

2.6 改

对ArrayList改操作有set(int, E)indexOf(o) 方法(indexOf方法是获取元素的索引,这里归类为改操作),还有一个lastIndexOf(o) 方法原理与indexOf(o) 相似,下面分别分析前两个方法的源码:

2.6.1 set(int, E)

先上源码:

这里很容易看懂
rangeCheck(index) 这一步上面有阐述过,index的越界检查;
E oldValue = elementData(index); 定义一个oldValue未修改之前的元素,方法结束时作为返回值
elementData[index] = element; 将给定的新值赋值到index位置

2.6.2 indexOf(Object o)

源码:

indexOf(Object o) 方法传入指定元素获取该元素第一次出现位置的索引(ArrayList可放重复元素,所以这里为第一次出现的索引);源码对三种情况做了处理,指定值为null、指定值不为null、指定值不存在于该集合中,前两种情况使用了for循环,指定值不为null时使用Object的equals方法判断是否同一个元素,元素不存在于集合时,返回-1;

2.6.3 小结

改操作都是从头开始查找给定的元素,这里要注意,前面讲过ArrayList可以存入null值,所以null值也要考虑在内;indexOf和lastIndexOf方法在给定值不存在于集合时,返回-1

2.7 查

2.7.1 get()



这里说一下下标越界检查,上面源码可以看出rangeCheck只判断index >= size,那小于零的情况怎么办?作者在注释中说明了,index为负数的时候交给ArrayIndexOutOfBoundsException处理,ArrayIndexOutOfBoundsException继承自IndexOutOfBoundsException,它们都属于运行时异常


最后在返回元素的时候做了向下造型的处理

3. 总结

ArrayList 底层是数组实现
ArrayList 可以存放null值且值可重复
ArrayList 可以自动扩容,扩容因子为1.5倍(jdk1.7和jdk1.8)
ArrayList 查询效率很快,增删效率慢
ArrayList 实现了RandomAccess快速访问接口,for循环遍历最快
ArrayList 线程不安全,所有操作都不是原子性的,比如插入数据时,需要判断elementData容量是否满足,再进行对应位置赋值,多线程情况下会导致数据不一致的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值