手撕源码——ArrayList源码剖析

本文详细解析了ArrayList的内部实现,包括默认容量、构造方法、add方法的源码以及并发修改异常ConcurrentModificationException。重点关注了ArrayList在不同初始化条件下的行为,如默认容量、指定容量为0的情况,以及在并发操作时可能出现的问题。文章还探讨了快速失败机制和如何避免此类异常。
摘要由CSDN通过智能技术生成

变量名解释

//Default initial capacity. 
默认初始化长度
private static final int DEFAULT_CAPACITY = 10;
//Shared empty array instance used for empty instances.
空的集合实例(Object类型的数组)
private static final Object[] EMPTY_ELEMENTDATA = {};
//Shared empty array instance used for default sized empty instances. We distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when first element is added.
相较于EMPTY_ELEMENTDATA 、该变量也是一个空的集合实例,但他知道如果需要用到的话,
第一次该扩容多少:how much to inflate when first element is added.
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//The size of the ArrayList (the number of elements it contains).
ArrayList 集合的大小
private int size;
// Object类型的数组
transient Object[] elementData;

构造方法

构造方法1

/*
Constructs an empty list with the specified initial capacity.
Params:
initialCapacity – the initial capacity of the list
Throws:
IllegalArgumentException – if the specified initial capacity is negative
*/
构造指定容量的ArrayList集合。 initialCapacity:初始化容量
如果初始化容量为复数,抛出非法参数异常:IllegalArgumentException
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
            // 如果没有指定初始化大小,则将它视为一个空的集合实例
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

以上代码注意!
else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;}
如果初始化时,不指定大小,就会把该集合视为EMPTY_ELEMENTDATA
而上面我们讲过了EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的区别

DEFAULTCAPACITY_EMPTY_ELEMENTDATA 也是空的集合,但他知道该如何扩容
EMPTY_ELEMENTDATA也是空集合,但他不知道如何扩容!
因此,如果初始化ArrayList的时候指定大小为0,
它就会在需要用的时候临时通过grow方法(后面会讲到)去扩容,降低了效率!

构造方法2

// Constructs an empty list with an initial capacity of ten.
构造一个空集合,初始大小为10
public ArrayList() {
//  由此可知,ArrayList是一个【数组类型的集合】
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

由此可知,ArrayList的初始大小为10

构造方法3

创建ArrayList集合时,如果传入的参数为Collection集合
则会先将该集合转为Object类型的数组。
  public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        // 如果新数组大小不为0
        if ((size = a.length) != 0) {
        // 判断新数组的类型,和ArrayList的类型是否相同
        // 这里涉及到了jdk1.8的bug 6260652
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
            // 如果类型不同的话,重新开辟一个集合空间,把数据复制过来,大小为size
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            // 否则用空集合来代替
            elementData = EMPTY_ELEMENTDATA;
        }
    }

add方法源码剖析

minCapacity在add时ArrayList需要的最小长度

    public boolean add(E e) {
    // Ctrl+B跟进ensureCapacityInternal方法:
   /*
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        如果需要的最小长度minCapacity 比当前集合长度大,则跳到扩容方法grow
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
*/

grow

先说两个变量的概念:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
MAX_ARRAY_SIZE :最大集合长度。
为什么等于Integer.MAX_VALUE - 8;呢?
看源码注释
The maximum size of array to allocate. Some VMs reserve some header
 words in an array. Attempts to allocate larger arrays may result
  in OutOfMemoryError: Requested array size exceeds VM limit
——>有些虚拟机需要在数组头文件中保存数组的长度(需要8个字节)
因为java不知道以后这个集合会在哪个VM虚拟机运行。
而不同虚拟机计算方式不同。
有的虚拟机需要在数组中用8个字节来保存数组大小,则最大长度只能到Integer.MAX_VALUE - 8
有的虚拟机不需要额外空间保存数组大小,那么它最大可以扩容到Integer.MAX_VALUE

理解后,看下面源码
/*
Increases the capacity to ensure that it can hold at least the number of elements specified by the minimum capacity argument.
Params:
minCapacity – the desired minimum capacity
这句注释很重要!增加容量!【确保可以满足最小需要的空间minCapacity 】
*/
private void grow(int minCapacity) {
        // overflow-conscious code
        // 先储存一下旧的长度
        int oldCapacity = elementData.length;
        // 新长度 = 旧的长度+ 旧的长度二进制右移一位(即1.5倍扩容)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果新计算出的长度-最小需要长度<0
        // 什么意思呢,就是说,经过一次扩容(1.5倍)之后,还是不满足你需要的长度
        if (newCapacity - minCapacity < 0)
        // 那么就将把集合扩容到你需要的长度吧minCapacity
        //  不多扩容,扩多了浪费空间。
            newCapacity = minCapacity;
            //  如果扩容后的长度 - MAX_ARRAY_SIZE  >0
       // 也就是说,扩容后的长度 ,超过了  最大集合的长度!
       // 那么跳转到hugeCapacity方法,赋予它最大上限的长度(给他能给的最大长度)
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

//------------hugeCapacity---------

    private static int hugeCapacity(int minCapacity) {
    // 如果最少需要的长度minCapacity <0,则抛出OOM
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
            // 否则,三元运算符
  如果minCapacity 大于集合最大值,也就是说,已经到了集合最大值了,
  你还要再往里面加元素?可能你用的虚拟机牛X一些,可以扩容到Integer.MAX_VALUE吧
  那我就给你Integer.MAX_VALUE这么大的空间,这是我的极限了。
  否则,给他集合最大值(Integer.MAX_VALUE-8return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
      --->这是一个机制,为了防止同一个代码,在不同虚拟机中,
  由于有的虚拟机需要在数组头存储8个字节的长度信息,
  而造成其支持的数组最大值只能为MAX_ARRAY_SIZE,而抛出异常
比如一个数组,大小是Integer.VALUE, 在VM1中,没问题
放到VM2中执行,就出问题了
//----------------------

        // minCapacity is usually close to size, so this is a win:
        接着上面的 if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
       根据上面计算出的新的数组大小
       COPY一个新的数组,把数据elementData迁移过来
        
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //  确保数组还支持加入一个元素后
        // 将新元素加入进去,index右移一位
        elementData[size++] = e;
        //  返回成功
        return true;
    }

并发修改异常ConcurrentModificationException

并发修改异常是什么》?
简单地说,并发–> 说明是多线程进行操作
修改异常—> 说明在修改的时候出现了异常
总之:就是在用迭代器对集合遍历元素的时候,通过集合是不能修改元素的。

修改和查询不能同时进行,类似于Mysql中的事务——>幻读

这里用到了一个快速失败机制:failFast
创建迭代器对象时 将全局的modCount赋值给迭代器的局部变量expectedModCount
在迭代的过程中modCount!=expectedModCount快速抛出异常

测试代码


   private static List list = new ArrayList();

   public static void main(String[] args) {
       List list = new ArrayList<>();


       LinkedList<Object> linkedList = new LinkedList<>();
       for(int i=1;i<=11;i++){
           linkedList.add(i);
       }

       Collections.synchronizedList(list);
       new ThreadIterator(list).start();  // 迭代器遍历
       new ThreadAdd(list).start();    //  添加
   }

修改线程

    private List list;

    public ThreadAdd(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; i <  100; i++) {
            System.out.println("循环添加第:"+i);
            try {
                Thread.sleep(5);
                list.add(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

迭代器遍历线程

    private List list;

    public ThreadIterator(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        while(true){
            for (Iterator iteratorTmp = list.iterator(); iteratorTmp.hasNext();){
                iteratorTmp.next();
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

测试结果:
在这里插入图片描述
前面还没问题,在第7次的时候出现了问题,为什么呢?
这里涉及到了一个全局变量:modCount 和 expectedModCount
在这里插入图片描述
源码这里可以看到,他都标注出来了,在add新增元素的时候,会执行modCount++

 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

看迭代器的源码
在这里插入图片描述

如果checkForComodification();检查后发现预期值和当前值不同,则抛出异常
也就是说,我在迭代器遍历的时候,会检查两个modcount的值。
而如果这期间加入了新增操作,即modcount++,从而导致值不同,就会出现异常
在这里插入图片描述
记录一下大佬更全的ArrayList 源码解析
https://mp.weixin.qq.com/s/jkbE21bPvQxJxyGrtRzF3g
链表源码解析
https://mp.weixin.qq.com/s/y0XawJzb7jowQ607CK3urA
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Binary H.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值