ArrayList 源码分析!!!

ArrayList 继承体系结构分析

在这里插入图片描述
从源码中可以画出以下 ArrayList 的继承体系结构图,便于分析。


在这里插入图片描述
从这张结构图中可以看到抽象类 AbstractList<E> 实现了 List<E> 接口,而它的子类 ArrayList<E> 又实现了一遍,这样做难道不是多此一举吗 ??思考一下它为什么要这样设计 ??

这样设计是为了在使用动态代理时,被代理的类必须直接实现接口,不能由父类间接实现,否则在方法返回一个对象时无法向下转型为该接口的类型。

关于动态代理的具体介绍,可以参考:设计模式之代理模式 — 静态代理与动态代理


接口是对行为的规范,在 Collection 接口中规定了一些集合中通用的方法,如:add()、addAll()、remove()、removeAll()、size()、isEmpty()、containsAll() 等
在这里插入图片描述
AbstractCollection<E> 部分源码

public abstract Iterator<E> iterator();

public abstract int size();

public boolean add(E e) {
    throw new UnsupportedOperationException();
}
    
public boolean isEmpty() {
    return size() == 0;
}

public boolean contains(Object o) {
    Iterator<E> it = iterator();
    if (o==null) {
        while (it.hasNext())
            if (it.next()==null)
                return true;
    } else {
        while (it.hasNext())
            if (o.equals(it.next()))
                return true;
    }
    return false;
}

在抽象类 AbstractCollection<E> 中,实现了 Collection<E> 接口的大部分方法,部分方法如:iterator()、size() 方法没有实现,而是把它们交给子类去实现;add() 方法直接抛出异常,需要子类重写;同时在其它方法的实现中大都使用了这两个没有实现的方法,思考这里用到了什么设计模式???

这里用到了设计模式中的模版方法模式,在抽象类 AbstractCollection<E> 中那些已经实现的方法相当于模版方法,而那些没有实现的方法如:size()、iterator() 是抽象方法需要子类实现,add() 方法相当于钩子方法,需要子类重写。

AbstractList<E> 和 AbstractCollection<E> 一样,都是使用了模板方法,具体就不在这里赘述了。

下面正式分析 ArrayList<E> 源码

ArrayList<E> 源码分析

构造器和 add(E e) 方法

ArrayList 有 3 个构造器,一般以下两个用的比较多
在这里插入图片描述
在这里插入图片描述
ArrayList 底层使用 Object[ ] 数组存储数据,当使用无参构造器时,默认是空数组。

当第一次调用 add() 方法时,size 是成员变量,记录数组中元素个数,初始为 0;这里将 size+1 然后调用 ensureCapacityInternal() 方法。并将 1 传进去。
在这里插入图片描述
calculateCapacity() 方法作用:当容器数组为默认的空数组时,返回默认容量大小与 minCapacity 的最大值,否则直接返回 minCapacity。此时返回默认容量大小 10
在这里插入图片描述
然后调用 ensureExplicitCapacity() 方法,并将 10 作为参数传进去,此时当前数组长度为 0,所以调用 grow() 方法,并将 10 传进去
在这里插入图片描述
grow() 方法是数组扩容的核心方法!!!
在这里插入图片描述
在这里插入图片描述
从 grow() 方法中我们可以得知:

  1. newCapacity = oldCapacity + oldCapacity / 2,数组增长因子为 0.5

  2. 当 minCapacity 值在 oldCapacity 与 newCapacity 之间时,数组的最终扩容大小为 newCapacity;

  3. 当 minCapacity 值在 newCapacity 与 MAX_ARRAY_SIZE 之间时,数组的最终扩容大小为 minCapacity;

  4. 当 minCapacity 值大于 MAX_ARRAY_SIZE 时,数组最终大小为 Integer.MAX_VALUE
    在这里插入图片描述
    因此,当使用无参构造器创建 ArrayList 对象时,数组容量默认大小为 10


总结:

  1. 在创建 ArrayList 对象时,最好使用带参构造器指定初始化容量,避免在 add() 操作时消耗 CPU,因为在数组进行复制时消耗是非常巨大的

  2. 数组默认大小容量为 10

  3. 数组的增长因子为 0.5

remove(int index) 方法

在这里插入图片描述
在这里插入图片描述
在 remove() 方法中,我们主要关注两个方法,一是 rangeCheck() 检查下标越界问题,这没啥好说的,还有一个是 System.arraycopy() 调用的是本地方法。

当移除的是数组中最后一个元素是,直接将最后一个元素设为 null,否则进行数组拷贝,将数组中的元素往前挪一位

add(int index, E element)

在这里插入图片描述
和前面类似,这里就不重复赘述了!!!

批量删除 batchRemove(Collection<?> c, boolean complement)

在这里插入图片描述
不管是 retainAll 还是 removeAll 都会调用 batchRemove 方法
在这里插入图片描述
在这里插入图片描述
以 removeAll() 为例:removeAll 中参数 complement 为 false

removeAll() 求差集,在该方法中 r 代表读指针,w 代表写指针。r 和 w 初始都为 0,w 总是小于等于 r

在第一个 for 循环中,r 遍历数组中所有元素个数(注意:元素个数 size ≤ 数组长度 length)

if (c.contains(elementData[r]) == complement)
	elementData[w++] = elementData[r];

每当 c.contains(elementData[r]) == false 时,if 为 true,便将读指针 r 指向的元素赋值给写指针 w 指向的位置。

当读指针 r 循环到 size 时便跳出循环,此时写指针 w 指向的位置之后的数组中的元素最终在 finally 语句块中都将被赋值为 null,modCount 值也会被修改。

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

从源码中我们可以发现批量删除不会改变数组大小,但 modCount 值仍会被修改。

modCount

在这里插入图片描述
modCount 是 AbstractList<E> 中的成员变量,记录这个列表在结构上被修改的次数。

从源码中可以发现每当进行 add 或 remove 操作时 modCount 值都会随着改变

关于在 foreach 中增加或删除元素

在这里插入图片描述
在这里插入图片描述
从调试中我们可以发现 foreach 的本质是使用了 iterator 迭代器
在这里插入图片描述
在 ArrayList 内部有一个内部类实现了 Iterator<E> 接口,每当使用 foreach 时都会返回该迭代器对象
在这里插入图片描述
当返回该对象时,游标 cursor 初始值为 0,expectedModCount = modCount,当调用 hasNext() 方法时,判断 cursor 值是否与数组中元素个数相等,不等则返回 true
在这里插入图片描述
调用 next() 方法时,首先检查数组元素个数是否被修改过
在这里插入图片描述
因此当 foreach 中内部第一次增加或删除元素时,modCount 会改变,而 expectedModCount 的值没变,当下一次进入 foreach 时,就会抛出并发修改异常。
在这里插入图片描述

get 和 set 方法非常简单

在这里插入图片描述

三、总结

  1. ArrayList 底层使用数组实现,默认初始容量为 10,增长因子为 0.5,最好自己指定初始化容量。

  2. 适合随机查找和遍历,不适合插入和删除。因为当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。

  3. 当数组大小不满足需要增加存储能力时,可以自动扩容。

发布了42 篇原创文章 · 获赞 11 · 访问量 3817

猜你喜欢

转载自blog.csdn.net/weixin_44584387/article/details/104508757