源码:常用集合类 之 ArrayList


0 前言

在开发过程中下面这段代码不知道写了多少次,是时候拨开它的外壳,看看内部构造了。

List<T> list = new ArrayList<>();

本文的代码片段均来自jdk1.8源码。

1 属性的声明

声明
每个变量都有详细注释,简单说一下:
DEFAULT_CAPACITY:默认容量为10,常量
EMPTY_ELEMENTDATA:空数组,常量
DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默认容量的空数组,常量
elementData:Object[]类型的引用,ArrayList的元素就是存在这个引用指向的实例中,用transient修饰表示不参与序列化
size:用来表示含有元素的数量

2 构造方法

无参构造
无参构造很简单就是让elementData指向默认的空数组实例。
构造方法1
指定一个初始容量值来创建ArrayList。如果初始容量值n大于0,elementData为一个大小为n的数组,如果为0就是空数组EMPTY_ELEMENTDATA,小于0则不合法抛异常。这里可以看到EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA用在不同的地方。
构造方法2
最后这个构造方法,将Collection中的元素数组取出(toArray)给elementData,如果这个数组为空elementData还是指向EMPTY_ELEMENTDATA,如果不为空并且类型不为Object[]时,底层通过调用本地方法返回一个Object[]数组。

3 添加 add

添加
添加一个元素的时候首先调用了ensureCapacityInternal方法,计算容量
扩容

a 假设使用无参构造的,elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这时候添加第一元素时,ensureCapacityInternal方法中的minCapacity参数为1,调用calculateCapacity方法计算出的容量为默认大小10。
b 如果使用第二个构造方法指定了大小n,则直接返回n。

紧接着计算的容量大小作为参数,调用ensureExplicitCapacity方法,该方法使modCount值+1,后面会详细说这个变量。
然后判断计算的容量大小是否已经超出了elementData数组大小范围,超过则需要调用grow方法扩容。

最后回到add方法,将元素添加到数组对应位置中,这里假设第一次调用add方法,则元素放在数组下标0的位置,然后size自增,表示有一个元素。

4 扩容 grow

扩容
来看看扩容的过程,用例子好理解一点:
a 假设new ArrayList()创建的对象,那么根据上面add方法介绍可以知道,这时候计算出的容量为10,也就是这个方法的入参minCapacity为10,。先获取原有容量oldCapacity,由于elementData是个空数组,那么oldCapacity为0,newCapacity也0(位运算右移1位即为原值的0.5倍,再加上原值就是1.5倍),小于10,那么新容量就为10,最后使用Arrays.copyOf构建一个大小为10的Object数组。
b 假设容量为10的ArrayList这时候要添加第11个元素,新容量为15。
c hugeCapacity方法中显示最大容量为int最大值0x7fffffff。

总结:每次调用add方法先会判断list容量是否足够,不够的话先扩容,然后再放入元素。另外如果使用无参构造方法实例对象时,第一次添加元素时,才会创建一个容量为10的数组,然后将元素放入。以后每次扩容总是变为原容量的1.5倍,直到达到最大容量2^31-1。

5 Arrays.copyOf

很多地方都用到了这个方法,下面贴出了方法内部实现:
在这里插入图片描述
在这里插入图片描述可以看到又创建了copy数组,使用System方法将原数组中元素复制到copy数组中
在这里插入图片描述System.arraycopy是一个native修饰的方法,表示调用本地方法实现,就是这个方法实现用C/C++实现,JVM运行通过加载共享库,完成库中本地方法调用。
srcPos表示要从原数组那个下标位置开始copy,destPos表示copy到目标数组从哪个下标位置开始,length表示copy元素的个数。
在这里插入图片描述

6 指定位置添加 add

在这里插入图片描述
知道了System.arraycopy方法执行结果,这段代码就很好理解了。先检查指定的下标是否在有效范围,然后检查是否需要扩容,然后将原数组index的位置之后的元素全部向后移一位,类似下面的表示,最后将新元素添加到index位。
在这里插入图片描述
如果想更新index位置的元素需要使用set(index,e)方法,之前在项目开发时候搞错了这两个方法导致的bug,用了半天时间才定位到,o(╥﹏╥)o都是教训。

7 删除 remove

在这里插入图片描述
在这里插入图片描述
根据下标删除,同样还是检查下标是否有效,然后将数组对应位置元素取出,最后index之后的元素整体向前移动一位。将空位补齐。可以用下图表示:
在这里插入图片描述
在这里插入图片描述
再看根据元素删除,这里是在遍历元素过程中找到相同元素的下标然后根据下标删除。
如果不是删除数组最后一个元素,这两种删除方式,每次都涉及到数组的中index位置之后的每个元素都移动一次。
总结:ArrayList中的remove方法和add(index)方法每次操作(除特殊情况),就会涉及分部元素的移动,这就是为什么总说ArrayList的修改效率慢的原因。如果知道元素位置,调用get(index)直接取出元素,所以说读取效率快。(与LinkedList对比)

8 modCount是什么?

最初看了源码中注释也还是有点懵懵的,最后看了一下Java中modCount的用法,fail-fast机制这篇文章。大致意思是这个参数表示list结构上被修改的次数,结构上的修改指的是那些改变了list的长度大小或者使得遍历过程中产生不正确的结果的其它方式。在线程不安全的集合中,在某些方法中,初始化迭代器时会给这个modCount赋值,如果在遍历的过程中,一旦发现这个对象的modCount和迭代器存储的modCount不一样,就会报错。举个栗子:

        ArrayList<Double> list = new ArrayList<>();
        list.add(1d);
        list.add(2d);
        list.add(3d);
        list.add(4d);

        for (Double aDouble : list) {
            if (aDouble%2 == 0) {
                list.remove(aDouble);
            }
        }

这段我曾亲手写出来的代码,运行报错如下:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)

for-each底层也是使用迭代器遍历元素的,从报错信息可以看到是调用Itr中的方法抛异常的,这里我摘了部分代码:
在这里插入图片描述在这里插入图片描述

创建Itr对象时,先记录modCount的值,每遍历到一个元素时都会检查值是否发生改变,如果改变则抛出异常。就拿我的代码来说remove掉元素时,modCount值已经发生变化,取下一个元素时就会抛异常(名称为并发修改异常)。也就是说terator正在遍历的list时,不能修改list。
改用fori遍历或者以下方式可以规避异常:

		Iterator<Double> iterator = list.iterator();
        while(iterator.hasNext()) {
            if (iterator.next() %2 == 0) {
                iterator.remove();
            }
        }

最后说一下,ArrayList是线程不安全的,如果有线程安全问题可以用Vector,内部实现与ArrayList相似,其中方法都用synchronized修饰,所以效率肯定低。jdk1.5之后提供的CopyOnWriteArrayList类是线程安全的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值