Java集合框架ArrayList 源码剖析

继承结构和层次关系

我们看一下ArrayList的继承结构:

ArrayList extends AbstractList

AbstractList extends AbstractCollection

所有类都继承Object 所以ArrayList的继承结构就是上图这样。

总体介绍

ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。

size(), isEmpty(), get(), set()方法均能在常数时间内完成,add()方法的时间开销跟插入位置有关addAll()方法的时间开销跟添加元素的个数成正比。其余方法大都是线性时间。

为追求效率,ArrayList没有实现同步(synchronized),如果需要多个线程并发访问,用户可以手动同步,也可使用Vector替代。

类中的属性

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

构造方法

ArrayList有三个构造方法:

无参构造方法 

/**
    * Constructs an empty list with an initial capacity of ten.  这里就说明了默认会给10的大小,所以说一开始arrayList的容量是10.
    */
//ArrayList中储存数据的其实就是一个数组,这个数组就是elementData,在123行定义的 private transient Object[] elementData;
public ArrayList() {  
    super();        //调用父类中的无参构造方法,父类中的是个空的构造方法
    this.elementData = EMPTY_ELEMENTDATA;//EMPTY_ELEMENTDATA:是个空的Object[], 将elementData初始化,elementData也是个Object[]类型。空的Object[]会给默认大小10,等会会解释什么时候赋值的。
}

有参构造函数一

public ArrayList(int initialCapacity) {
    super(); //父类中空的构造方法
    if (initialCapacity < 0)    //判断如果自定义大小的容量小于0,则报下面这个非法数据异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity]; //将自定义的容量大小当成初始化elementData的大小
}

方法剖析

set()

既然底层是一个数组ArrayList的set()方法也就变得非常简单,直接对数组的指定位置赋值即可。

public E set(int index, E element) {
rangeCheck(index);//检验索引是否合法
E oldValue = elementData(index);//旧值
elementData[index] = element;//赋新值
return oldValue;//返回旧值
}

get()

get()方法同样很简单,唯一要注意的是由于底层数组是Object[],得到元素后需要进行类型转换。

public E get(int index) {
    rangeCheck(index);// 检验索引是否合法(只检查是否大于size,而没有检查是否小于0)
    return (E) elementData[index];//注意类型转换
}
add()方法//默认直接在末尾添加元素
/**
* 把元素添加到集合的末尾
*/
public boolean add(E e) {
    //确定数组容量是否足够,size是数据个数,所以+1. 
    ensureCapacityInternal(size + 1); 
    //在数组中正确的位置上添加元素e,并size++
    elementData[size++] = e;
    return true;
}

ensureCapacityInternal(xxx); 确定内部容量的方法

//用于确定数组容量
private void ensureCapacityInternal(int minCapacity) {
    
    //判断初始化elementdata是否为一个空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        
        //如果是,相当于是空数组没有长度,则获取“默认的容量”和“传入参数”两者之间的最大值
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

    }
    //确认实际的容量,判断是否需要进行扩容操作
    ensureExplicitCapacity(minCapacity);
}

ensureExplicitCapacity(xxx);

//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    //记录修改次数
    modCount++;

    //如果最小大小 减 数组长度 大于0 -> 进行数组扩容
    if (minCapacity - elementData.length > 0)
        //实际进入扩容机制
        grow(minCapacity);
}

void add(int,E);在特定位置添加元素,也就是插入元素add(int index, E e)需要先对元素进行移动,然后完成插入操作,也就意味着该方法有着线性的时间复杂度。

/**
* 在此列表中的指定位置插入指定的元素。 
*先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
*再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
*/
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    
    //arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    elementData[index] = element;
    size++;
}

rangeCheckForAdd(index) 

    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)   //插入的位置肯定不能大于size 和小于0
//如果是,就报这个越界异常
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

grow(xxx); arrayList核心的方法,能扩展数组大小的真正秘密。

/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
     // oldCapacity为旧容量,newCapacity为新容量
    
    //将扩充前的elementData大小给oldCapacity
    int oldCapacity = elementData.length;
	
    //将oldCapacity 右移一位,其效果相当于oldCapacity/2
    //newCapacity就是1.5倍的oldCapacity
    int newCapacity = oldCapacity + (oldCapacity >> 1);

     //检查新容量是否大于最小需要容量,若小于最小需要容量,那么就把最小需要容量当作数组的新容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    /**
     * 检查新容量是否超出了ArrayList所定义的最大容量,
     * 若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
     * 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
    */
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //比较minCapacity和 MAX_ARRAY_SIZE
        newCapacity = hugeCapacity(minCapacity);
    
    //新的容量大小已经确定好了,就copy数组,改变容量大小
    elementData = Arrays.copyOf(elementData, newCapacity);
}

hugeCapacity();

//这个就是上面用到的方法,很简单,就是用来赋最大值。
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

由于Java GC自动管理了内存,这里也就不需要考虑源数组释放的问题。

空间的问题解决后,插入过程就显得非常简单。

总结

正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。

当我们调用add方法时,实际上的函数调用如下:

说明:程序调用add,实际上还会进行一系列调用,可能会调用到grow,grow 会调用hugeCapacity。

addAll()

addAll()方法能够一次添加多个元素,根据位置不同也有两个把本,一个是在末尾添加的addAll(Collection<? extends E> c)方法,一个是从指定位置开始插入的addAll(int index, Collection<? extends E> c)方法。跟add()方法类似,在插入之前也需要进行空间检查,如果需要则自动扩容;如果从指定位置插入,也会存在移动元素的情况。

addAll()的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关。

remove()

remove()方法也有两个版本,一个是remove(int index)删除指定位置的元素,另一个是remove(Object o)删除第一个满足o.equals(elementData[index])的元素。删除操作是add()操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋null值。

remove(int):删除指定位置上的元素

public E remove(int index) {
    rangeCheck(index);//检查index的合理性

    modCount++;//增加修改次数
    E oldValue = elementData(index);//通过索引直接找到该元素

    int numMoved = size - index - 1;//计算要移动的位数。
    if (numMoved > 0)
        //这个方法也已经解释过了,就是用来移动元素的。
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。
    elementData[--size] = null; // clear to let GC do its work
    //返回删除的元素。
    return oldValue;
}

remove(Object):这个方法可以看出来,arrayList是可以存放null值得

//通过元素来删除该元素,就依次遍历,如果有这个元素,就将该元素的索引传给fastRemobe(index),使用这个方法来删除该元素,
//fastRemove(index)方法的内部跟remove(index)的实现几乎一样,这里最主要是知道arrayList可以存储null值
public boolean remove(Object o) {
    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;
}

关于Java GC这里需要特别说明一下,有了垃圾收集器并不意味着一定不会有内存泄漏对象能否被GC的依据是是否还有引用指向它,上面代码中如果不手动赋null值,除非对应的位置被其他元素覆盖,否则原来的对象就一直不会被回收。

clear():将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,所以叫clear

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

总结::remove函数用户移除指定下标的元素,此时会把指定下标到数组末尾的元素向前移动一个单位,并且会把数组最后一个元素设置为null,这样是为了方便之后将整个数组不被使用时,会被GC,可以作为小的技巧使用。

indexOf()方法
// 从首开始查找数组里面是否存在指定元素
public int indexOf(Object o) {
    if (o == null) { // 查找的元素为空
        for (int i = 0; i < size; i++) // 遍历数组,找到第一个为空的元素,返回下标
            if (elementData[i]==null)
                return i;
    } else { // 查找的元素不为空
        for (int i = 0; i < size; i++) // 遍历数组,找到第一个和指定元素相等的元素,返回下标
            if (o.equals(elementData[i]))
                return i;
    } 
    // 没有找到,返回空
    return -1;
}

1)arrayList可以存放null。2)arrayList本质上就是一个elementData数组。3)arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是grow()方法。4)arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全删除集合中的元素。5)arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果6)arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。

  • 30
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

气宇轩昂的固执狂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值