java集合系列三:ArrayList源码解析

你应该先阅读java集合系列一:前传

接着Collection简单了解之后,开始学习ArrayList,这个集合在工作中比较常用,理解它更便于我们写出更优秀的代码

在这里插入图片描述

介绍

  • ArrayList是通过动态数组实现的
  • 允许存储Null值,这个所有通用的实现都可以存null值
  • ArrayList是线程不安全的 毕竟很多情况下都不是多线程操作,锁会增加性能损耗,且不必要的同步可能导致死锁,需要同步集合可以参考java.util.concurrent包提供的并发实现
  • 具有fail-fast机制(可以规避 比如ListIterator/CopyOnWriteArrayList)
  • 查询快
  • ArrayList有初始容量10,它指的是ArrayList在必须增长之前可以容纳的元素数量
  • 如果你需要在集合开头的位置添加或删除元素,你应该考虑使用LinkedList,而不是ArrayList
  • ArrayList有List特有的迭代器 ListIterator
  • 初始默认扩容容量10,在必须增长之前扩容为原容量的1.5倍

这些总结在下面都会通过源码来验证!

继承关系

  • AbstractList:AbstractList在上一篇文已经讲解过了
  • List:没发现有什么用.毕竟它的父类AbstractList已经实现了List,文末再说这个问题
  • RandomAccess:List实现的标记接口,表示支持快速随机访问,这里不多谈论这个了,如果有需要文末再单独讨论吧
  • Cloneable:可被克隆
  • Serializable:可被序列化

ArrayList成员变量

//默认容量 可在创建ArrayList时通过构造函数指定
private static final int DEFAULT_CAPACITY = 10;

//创建一个长度为0的存放数据的空数组 它很少使用到 当你new一个ArrayList(0)时,它会创建	
//这个很少用到 如果你使用它,它的扩容会更频繁 应该没人用...
private static final Object[] EMPTY_ELEMENTDATA = {};

//创建一个长度为0的存放数据的空数组  
//当添加第一个元素时 如果elementData指向自己则扩容为DEFAULT_CAPACITY
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//存放数据的数组
transient Object[] elementData; // non-private to simplify nested class access

//ArrayList的实际数据存储数量 就是size()返回的结果
private int size;

常用方法解析 所有代码都不是完整的源码,而是精简过后的源码!结合源码食用更佳

add(E e)

我说这个方法是最常用的没人反对吧? 下面是主要功能

 	//第一次add默认扩容DEFAULT_CAPACITY 也就是10  minCapacity是当前数组的length+1的长度
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //modCount记录的是 这个列表在结构上被修改的次数(这句话总结的很到位 --)
    modCount++;
    //判断是否需要扩容 elementData.length就是当前数组的长度
    if (minCapacity - elementData.length > 0)
        //主要扩容的方法
        grow(minCapacity);
    //扩容方法 每次扩容为原容量的1.5倍
    private void grow ( int minCapacity){
        //获取当前数组长度 假设是默认容量已经存满 需要扩容 这里就是10 
        int oldCapacity = elementData.length;
        //newCapacity = 10>>1 = 5 + 10 = 15
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //在elementData = EMPTY_ELEMENTDATA时,也就是你在创建ArrayList时指定了默认容量为0 会<0
        //如果你使用了 EMPTY_ELEMENTDATA 需要多次扩容后才能达到1.5倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //防止溢出  通常也达不到int的最大取值范围..
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 返回扩容后的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

remove(int index)

作用:移除列表中指定位置的元素。将任何后续元素向左移动(从它们的索引中减去一个)

 	//这个列表在结构上被修改的次数(这句话总结的很到位 --)
    modCount++;
    //根据索引直接获取该位置的元素用来在最后返回给调用者
    E oldValue = elementData(index);
    // 得到需要移位的数据长度  -1是为了去除会被数组copy覆盖的那一个元素  
    int numMoved = size - index - 1;
    //如果数组中zise = 1 直接置为null  那么也没有必须再将index后的元素移位了 
    if (numMoved > 0)
        //从删除的index+1的位置复制所有元素前移一位
        System.arraycopy(elementData, index+1, elementData, index,
                numMoved);
    //将数组最后一位置为null
    elementData[--size] = null; // clear to let GC do its work
    //返回给调用者删除的元素
    return oldValue;

可能有人(其实就是我自己)又要捣鼓为什么elementData[–size] = null,最后一位被置为null,而不是减少数组的长度,那么可不可以再获取到这个null,理论上来说这个位置目前存的是null,原数据会被回收,实际上是没错,但是如果你通过get(index)会直接越界,原因就是–size,虽然数组没有被减少但是size却被减少了,每次get或者remove时都会经过另一个方法判断,如下:

  private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
 }

set(int index, E element)

作用:用指定的元素替换列表中指定位置的元素。返回被替换的数据

	E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;

没写注释 原因是一目了然 ,注意set不会触发modCount++,因为set不属于结构上的修改

get(int index)

return elementData(index);

增删改查结束

ArrayList-ConcurrentModificationException

这个涉及到一个ArrayList并发修改的问题,或者说所有继承AbstractList对象的集合都有这个并发修改的问题(没有一个一个看源码,简单看了几个都有),比如Vector/LinkedList等
有几个问题:

  • 为什么会触发该异常
  • 如何正确处理此异常,或者说让程序避开此异常

第一个问题,为什么会触发该异常
首先需要知道Iterator内部维护了一个expectedModCount,这个变量的作用是就是在你调用iterator()方法时记录下modCount的数量,当程序迭代时会判断expectedModCount是否与modCount一致,代码如下:

final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

如果不一致 直接抛出ConcurrentModificationException,在上面的源码中我们分析add与remove方法时可以看到modCount++,在程序迭代中如果触发了modCount++,那么在下一次迭代next方法时如下:

	 public E next() {
        checkForComodification();
    }

迭代器循环和for-each循环(底层也是迭代器)在迭代中如果修改了元素都会触发此异常

第二个问题,如何正确处理此异常,或者说让程序避开此异常

1. 使用Iterator的remove方法,原理很简单,代码如下:

	 //先检查两个变量的值是否一致 不一致抛出异常
    checkForComodification();
    //调用ArrayList的remove方法 注意此时modCount++ 被触发了
    ArrayList.this.remove(lastRet);
    //重新赋值 这样下一次next方法checkForComodification()时将不会受到影响 
    expectedModCount = modCount;		

2. 使用listIterator()方法
它继承自Iterator,允许在任意方向上遍历列表,比如向前或向后,并在Iterator的基础上扩展了add/set等不会触发并发异常的方法,原理实现和上面的remove一样就不贴代码

**3.**使用普通的for循环

**4.**使用CopyOnWriteArrayList等更高度封装的集合


扩展知识:

  • 可能有人就会捣鼓(其实还是我自己)ArrayList为什么要实现List接口毕竟他的父类AbstractList已经实现List了,折腾了一会,自己按照Collection的设计写了个demo,在简单的使用中没有发现它重复实现的意义,然后想到是不是在其他使用中可能存在特殊意义,比如提升某些场景下的使用效率如代理或反射等,后来才发现是作者的mistake,连接https://stackoverflow.com/questions/2165204/why-does-linkedhashsete-extend-hashsete-and-implement-sete 当然是否如这个链接所说也无法求证, 如果是链接所说这样那我有个问题 为什么Vector等其他集合也会和父类实现同样的接口呢,它们并不是同一个作者来编写,这可能是设计的规范,而这些作者需要遵循这个规范,可能是为了提升可读性或像我说的有其他特殊作用,当然可以暂时这样认为(毕竟stackoverflow上都这么说),强迫症找不到答案会很难受,舒服…
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值