Java基础-ArrayList源码详解

  1. 实现原理
    从构造方法很容易看出,ArrayList底层使用数组实现,不同于LinkedList底层通过链表实现。数组接口具有查找快,增删慢的特性。
    在这里插入图片描述
  2. 类结构
    在这里插入图片描述
    ArrayList是Java集合Collection一个重要的实现类,也是开发工作中使用最频繁的类之一。
    AbstractList抽象类,继承抽象类,抽象类实现了一些集合类共有的方法,ArrayList继承后就不需要自己再去实现共性的方法,只需要关注实现自身特性的实现,这是一种模板设计模式。
    List接口,第一次看的时候很费解,既然ArrayList已经继承了AbstractList,而AbstractList又实现了List,已经隐士的实现了List的接口,那为什么还要多此一举再次直接实现List呢?后来发现可能会影响动态代理,如果我们想要获取具有隐式接口继承的对象的所有接口,从而来创建动态代理。那么唯一的办法就是通过反射遍历类的继承,并找出超类的所有接口。通过显式的声明接口,可以更容易获得该类继承的所有接口。
    RandomAccess接口:RandomAccess从名称可见表示可以随机访问,所有的List实现都支持随机访问的,只是基于基本结构的不同,实现的速度不同罢了,这里的快速随机访问,那么就不是所有List集合都支持了(LinkedList就没有实现该接口)。当一个List拥有快速访问功能时,其遍历方法采用for循环最快速。而没有快速访问功能的List,遍历的时候采用Iterator迭代器最快速。从下图接口的源码注释也能证实这一点。在这里插入图片描述
    Cloneable和Serializable,这个两个接口就不过多描述了,前者是支持clone必须要求,后者是序列化的要求。
  3. 构造方法
    无参构造,创建一个空数组。这个空数组区别与构造参数为0的空数组,从源码的注释可以看出,主要是为了区别新增第一个元素时候的不同逻辑处理。后面我们看add方法时分析。
    在这里插入图片描述
    在这里插入图片描述
    初始化集合长度构造方法
    如果入参定义集合长度大于0,那就初始化指定长度的数组。
    如果初始化长度为0,那就赋值一个空数组,这个数组区别于上面空参构造方法的空数组。
    在这里插入图片描述
    在这里插入图片描述
    通过已存在的集合构造方法
    该构造方法不允许传入的集合为空,否则会报空指针错误。
    传入的数组长度为0,则创建一个空数组。
    在这里插入图片描述
    elementData.getClass() != Object[].class这一段代码是为了解决toArray的一种bug,源码注释可见bug6260652
    toArray方法返回的Object数组,其getClass方法可能不会返回Object[].class
    在这里插入图片描述
  4. 新增
    新增的逻辑很简单,首先确定数组有没有足够的容量了,没有就扩容,然后将元素赋值给数组的下一个空位。重点就是如何初始化和扩容的,下面我们继续看源码。下面所有逻辑均针对新增第一个元素进行分析,非首个元素新增的逻辑基本一样。
    在这里插入图片描述
    计算数组的最小容量
    其中入参elementData为构造初始化时候指定的空数组(上面提到有两种情况),(新增第一个元素进行分析)minCapacity这里为1
    下面的逻辑就能看出前面说到的两种空数组的不同逻辑了,如果是无参构造为默认空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA),这时改方法返回的就是默认容量(DEFAULT_CAPACITY)大小10,否则就返回当前入参1。
    在这里插入图片描述
    计算过最小需要的容量后,接下来判断当前数组的长度是不是满足这个最小容量,(新增第一个元素进行分析)针对空数组,现在需要进行扩容。其中modCount++,相当于记录当前集合的版本,集合的某些操作过程中(如序列化和迭代器相关操作)都会判断这个版本,如果操作前后的版本不相等会抛出ConcurrentModificationException异常。
    在这里插入图片描述
    现在我们来看下重点逻辑扩容
    定义一个新变量存储当前集合中数组的长度。
    使用移位运算计算出新的数组长度值,很多人说是1.5,其实这个1.5是不准确的,例如当前minCapacity=1,移位后是1+0=1,又比如7(0111)右移后为3(0011)7+3=10。只有当minCapacity刚好为2的整数倍时,扩容后的数组长度才是扩容前的1.5倍。
    if (newCapacity - minCapacity < 0)不必困惑这个判断,由于int是有最大值的(2147483647),上一步的计算可能会出现负数,这个判断是很有必要的。如下图当oldCapacity超过1431655766后,这个判断始终为true。我们现在假设一种情况,现在数组长度就是1431655766,并且数据满了size也是这个大小1431655766。下次调用add方法时这个方法的入参size++就是1431655766+1=1431655767,即这个方法的入参minCapacity就是1431655767,此时oldCapacity也就是1431655766,由于判断为true会将minCapacity赋值给newCapacity ,也就是说超过这个值后每次扩容都是数组长度增加1(扩容之前是1431655766,扩容之后是1431655767),也就是当集合长度超过这个值后每次新增都会扩容,实际开发中这样的大集合可能比较少见,但个人觉得这个需要优化。
    在这里插入图片描述
    在这里插入图片描述
    if (newCapacity - MAX_ARRAY_SIZE > 0),接下来我们看这个判断,只有当重新分配的数组大小大于Integer.MAX_VALUE - 8才成立。
    第一种情况:当前数组长度oldCapacity=1431655765,(看上面的移位计算)扩容后newCapacity=2147483647(即Integer.MAX_VALUE),此时的minCapacity=1431655765+1=1431655766,那么此时最终的newCapacity=MAX_ARRAY_SIZE。
    第二种情况:假设我们现在数组长度就是oldCapacity=MAX_ARRAY_SIZE,执行add后minCapacity就是MAX_ARRAY_SIZE+1,又上面说的此时newCapacity刚好也是MAX_ARRAY_SIZE+1,那么此时最终的newCapacity=Integer.MAX_VALUE。
    下图在定义MAX_ARRAY_SIZE时,注释明确说明了某些虚拟机需要存储数组的头部信息(一般是数组长度),超过改长度可能会导致OOM。注意是某些,并不是所有虚拟机都是。那也就是说会不会OOM是虚拟机内部去判断的(这个也不确定,希望大佬指导)。
    但是这里有个地方没明白, if (minCapacity < 0) 按理说这个逻辑永远不会成立才对,因为前面已经判断了if (minCapacity - elementData.length > 0),也就是说minCapacity不可能是负数呀。
    在这里插入图片描述
    在这里插入图片描述
  5. 指定索引新增
    和直接新增有一点区别,多一步索引的校验,校验逻辑也很简单索引不能为负,不得大于集合大小,否则抛出索引越界异常。
    在这里插入图片描述
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值