JDK8-ArrayList源码分析

由于ArrayList底层是基于Object[]实现的,所以随机读的效率很高,插入和删除需要移动整个数组,效率很低.

和Vector不同,ArrayList不是线程安全的.可以理解为Vector就是线程安全的ArrayList…

 

属性介绍:

    /**

     * Default initial capacity.

     */

    private static final int DEFAULT_CAPACITY = 10; //默认大小为10,实际上调用无参构造函数初始化时,存放数据的数据是个空数组,调用add时才会真正初始化.

 

    /**

     * Shared empty array instance used for empty instances.

     */

    private static final Object[] EMPTY_ELEMENTDATA = {};//输入initcapacity为0,或者传入的集合为空时,会指向该数组.

 

    /**

     * 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.

     */

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//调用无参构造函数时,使用该数组

 

 

    /**

     * The array buffer into which the elements of the ArrayList are stored.

     * The capacity of the ArrayList is the length of this array buffer. Any

     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA

     * will be expanded to DEFAULT_CAPACITY when the first element is added.

     */

    transient Object[] elementData; //真正存放数据的数组

 

    /**

     * The size of the ArrayList (the number of elements it contains).

     *

     * @serial

     */

    private int size; //数组元素个数

 

构造方法:

initcapacity不为0时初始化elementData,否则让elementData引用指向其他两个空的数组.

 

 

add()分析:

         大致是先判断下当前elementData数组能否容纳下新增的元素,如果空间不够的话,需要先扩容,然后再插入元素.方法分析如下:

首先看下当时elementData数组大小是否足够容纳新增的元素,不够的话数组需要扩容.点到ensureCapacityInternal()方法里面看下.

先计算下当前数组的capacity,入参是当前elementData数组 和 size+1.

         可以看出如果当前数组是调用无参构造函数创建的ArrayList的话,那么返回的minCapacity是10,所以之前说的,就算无参构造函数只有第一次初始化的时候才会创建数组,相当于有个lazy init的过程. 

         否则的话,返回的就是size+1

        

         计算完毕之后先modCount++一把,表示对这个ArrayList进行过操作了.如果elementData空间确实不够的话,需要先grow(minCapacity)对数组进行扩容.

        

扩容规则如下:

         首先确定newCapacity为原先的1.5倍.

         如果还是小于 minCapacity,那么就将minCapacity赋值给newCapacity.

         如果此时capacity已经超过了ArrayList的最大容量Integer.MAX_VALUE – 8,那么newCapacity最大为Integer.MAX_VALUE.

         然后调用Arrays.copyOf(elementData, newCapacity)对原先旧的数组进行复制操作,底层调用的是System.arraycopy,这是个native方法.

 

         所以,要尽量避免扩容,数组的复制非常耗费性能!而且旧的数组需要GC!

contains(Object o)分析:

         底层调用的是indexOf(o)方法,实际上就是遍历数组元素,找到第一个匹配的元素的下标,没找到就返回-1, -1表示没有不包含.

 

 

clone()分析:

         ArrayList中的clone实际上是浅拷贝…参考下面这张图, 因为深拷贝不仅要拷贝对象本身,还要考本对象包含的引用锁指向的所有对象.

         如果elemeData中存放的是其他引用的话(如下面的teacher),clone拷贝的只是引用,没有把里面的元素给拷贝过来!

深拷贝要把对象的所有应用都要赋值一遍,所以这里的clone()还是浅拷贝,因为父类的teacher对象还是同一个.

 

 

remove(int index)分析:

移除指定位置的元素,然后将该元素之后的元素集体前移一位,所以如果移除的元素靠前并且数组长度比较大的时候,比较耗性能.

         remove(object o)类似,只不过它是移除遇到的第一个相匹配的元素.

 

clear():

         将elementData数组中的元素都置为null,并且size置为0.但是数组还是占了那么大的空间!

 

addAll(int index, Collection<? extends E> c):

         将集合c插入到index的位置.

         先检查下数组空间是否需要扩容.然后将[Index,*]的元素向后移动N位,N=c.length.

         最后再将c中的数据插入到elementData中.

 

removeAll和retainAll:

         都给一个集合参数c

removeAll删除elementData中所有在集合c中出现过的元素

         retainAll保留所有在集合c中出现的元素

         底层调用的都是batchRemove()!

        

         batchRemove实际上就是拿到elementData的引用,然后根据是remove或者retain将对应位置的元素赋值为null. 并修改相应的size和modcount.

 

 

subList():

         内部有构造了另外一种叫做SubList的数据结构,实际上操作的还是elementData.

 

foreach():

         JDK8新增的lambda表达式,实际上是对elementData中的每个元素应用Consumer类中的accept方法,方法中实际的内容是用户输入的,例如:

         list.foreach(x -> x+””)

 

spliterator():

jdk8为了适应并行操作而新增一个方法,类似于fork/join场景,把一个任务分成多个任务

待分析!

 

replaceAll():

         对elementData中的每个元素应用function,对每个元素做一次转换,感觉不如用map

 

 

 

sort():

         调用的是Arrays.sort(),里面用到了不止一种排序算法.

LegacyMergeSort.userRequested的意思就是是否使用jdk6以前的经典算法 – 归并排序,归并排序的主要思想是排序两个已经有序的数组(在注释中标记了该算法后续会被remove).需要设置System.setProperty("java.util.Arrays.useLegacyMergeSort", "true"); 

TimSort.sort其实也是本质也是归并排序+插入排序,只是进行改进了.算法考虑的是现实中输入进来的数据不是随机无序的,而是部分有序的.所以它处理的不是一个个数字,而是一个个分区,每个分区叫一个”run”.针对输入进来的run这个序列,每次拿一个run出来进行归并,每次归并会将两个run合并成一个run.合并时为了提高效率,运行的是二分插入算法: 先用二分查找算法/折半查找算法找到插入位置,然后再插入.

 

排序概念的一些补充:

冒泡排序和选择排序的异同:

         冒泡每次比较发现较小的元素在后面,就要交换两者响铃的元素.而选择排序的改进在于:先不急着调换位置,先记下这个最小的所在位置,扫描一趟完毕之后,才进行数组的交换.

         所以两者比较的次数是一样的,选择只需要进行一次真正的交换,而冒泡可能要多次.前者是稳定的,后者是非稳定的.(后者参考: 5 8 5 2 9 – 两个5的稳定性被破坏了)

 

冒泡排序和插入排序的异同:

         两者也很像, 时间复杂度相同,并且都是稳定的.

         冒泡是两两比较,将最大的元素移到最后,相当于是保证最右边有序. 而插入是从第二个元素开始,将当前元素插入到前面的有序数组中,相当于是保证最左边有序.

 

归并排序和快速排序的异同:

        两者都采用了”分而治之”的思想,即首先把代排序的数组分成两种组,然后分别对这两组排序,最后把结果合并起来.

 

排序算法的稳定性:

         稳定性是指保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。

         如果仅仅是对数值类型排序,那么稳定性是无意义的。但是对于对象中的字段进行排序,而你又需要顺序不变,那么稳定性就很重要了。

 

 

参考:

https://www.cnblogs.com/happyframework/p/3455798.html (冒泡/插入/选择 排序讲解)

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值