ArrayList源码简析

Posted 热爱编程的大忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArrayList源码简析相关的知识,希望对你有一定的参考价值。

ArrayList源码简析


ArrayList 简介

ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。

ArrayList继承于 AbstractList ,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

  
  • RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • ArrayList 实现了 Cloneable 接口 ,即覆盖了函数clone(),能被克隆。
  • ArrayList 实现了 java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

Arraylist 和 Vector 的区别?

  1. ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;
  2. VectorList 的古老实现类,底层使用 Object[ ]存储,线程安全的。

Arraylist 与 LinkedList 区别?

先来说说两者的相同点吧:

  • 非线程安全

两者的不同点其实就是数组和链表的区别,这里不详细进行展开了,只是概述一番:

  • 底层数据结构不同
  • 查询,插入和删除元素复杂度不同
  • 内存空间占用不同

设计思路

ArrayList本质是一个动态数组的实现,如果要设计一个动态数组,我们需要考虑哪些方面呢?

  • 扩容 ! ! ! (扩容是动态数组是否高效的核心)
  • 数组默认大小,应该提供接口让用户能够按照业务需求规定初始动态数组的大小,这样可以减少频繁扩容带来的性能损耗。

数组的增删查改没有什么独特优化技巧,无法就是需要在插入前进行扩容判断而已。

ArrayList的核心属性也就是下面两个:

    //底层动态数组
    transient Object[] elementData;
    //动态数组内部元素数量
    private int size;

动态数组的长度不等于动态数组里面元素的数量,动态数组的长度称为容量,通常都是容量大于元素数量的。


初始化

ArrayList为我们提供了两种初始化方式,一种是由用户自定规定默认初始化的数组大小,另一种是初始化一个默认大小的数组:

    public ArrayList(int initialCapacity) 
        if (initialCapacity > 0) 
            //直接初始化一个大小为用户指定大小的数组(这里并没有采用懒加载)
            this.elementData = new Object[initialCapacity];
         else if (initialCapacity == 0) 
            //说明用户要初始化一个空数组--对于空数组的表示,ArrayList采用了一个共享空数组变量实例来表示
            //实际插入元素时,才会扩容初始化(懒加载)
            this.elementData = EMPTY_ELEMENTDATA;
         else 
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        
    

    public ArrayList() 
         //采用默认初始化方案,初始化一个默认大小的数组,这里也是采用了一个共享实例进行标记
         //实际是等到真正插入元素的时候,才会进行初始化(懒加载)
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    

这两个共享实例的类型虽说一致,但是含义却不同,核心作用都是进行标记,为了懒加载服务。

    private static final Object[] EMPTY_ELEMENTDATA = ;

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = ;

插入元素

插入元素分为两种情况:

  • 直接插入到数组尾部
  • 插入到指定索引位置处
    public boolean add(E e) 
        //扩容检查
        ensureCapacityInternal(size + 1); 
        //插入数组尾部
        elementData[size++] = e;
        return true;
    
    
    public void add(int index, E element) 
        //检查Index是否合法--这里大家自行查看源码即可
        rangeCheckForAdd(index);
        //扩容检查
        ensureCapacityInternal(size + 1); 
        //插入到数组指定位置处,这里需要将index开始的元素都后移一位,然后在index位置处插入当前元素
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);            
        elementData[index] = element;
        size++;
    

插入元素前需要先进行扩容检查,我们下面来看看。


扩容检查

  • 确保当前数组大小能够再塞下n个元素
    //这里minCapacity=size+n,add方法传入的n=1,而addAll方法传入的n就不确定是多大了
    private void ensureCapacityInternal(int minCapacity) 
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    
  • 处理采用默认大小数组情况下的懒加载问题
    private static int calculateCapacity(Object[] elementData, int minCapacity) 
        //如果动态数组初始化的时候采用的是默认大小,然后构造方法只是打了个标记,还没有进行初始化
        //如果下面这个条件满足,说明此时应该是第一次调用add或者addAll方法,size此时等于0
        //而minCapacity的大小此时就是等于n,如果调用add方法,那么n=1,否则n是不确定的
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) 
            //如果调用的是addAll方法,并且n大于10,那么进行真正初始化的时候,数组大小采用n的大小
            //否则在n<10的情况,还是采用默认大小10
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        
        return minCapacity;
    
  • 判断当前数组是否可以再放下n个元素,如果可以就不进行扩容
    private void ensureExplicitCapacity(int minCapacity) 
        modCount++;

        //minCapacity=size+n
        //length=size+数组剩余空闲容量
        //下面这个等式等价于: n>数组剩余空闲容量
        if (minCapacity - elementData.length > 0)
           //扩容
            grow(minCapacity);
    
  • 扩容
    private void grow(int minCapacity) 
        //拿到当前数组的容量
        int oldCapacity = elementData.length;
        //当前数组容器的1.5倍作为新容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //判断上面通过默认1.5倍扩容方式得到的新容量是否满足当前再插入n个元素的需求
        //minCapacity=size+n   newCapacity=size+剩余空闲容量大小,因为扩容了,所以空闲容量大小更大了
        if (newCapacity - minCapacity < 0)
           //如果不满足,一般都是调用了addAll方法,此时新容量就等于minCapacity大小
            newCapacity = minCapacity;
        //如果数组元素过多直接超过最大限制,那么需要进行处理
        //这部分内容比较简单,大家就自行看看源码吧    
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //拿到扩容后的新数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    

ensureCapacity

ensureCapacity方法是ArrayList提供的方法,旨在插入大量元素前,用户通过调用该方法提前扩容到对应的大小,避免插入过程中频繁扩容。

    public void ensureCapacity(int minCapacity) 
        //如果当前数组采用默认大小进行初始化的,那么当前用户指定的扩容大小必须要大于默认大小,否则没必要扩容
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            ? 0
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) 
           //该方法内部还会再次判断,如果用户指定的扩容大小小于当前数组的容量,那么也没有必要进行扩容操作。
            ensureExplicitCapacity(minCapacity);
        
    

System.arraycopy() 和 Arrays.copyOf()方法

ArrayList底层的add,remove,grow等涉及到对底层数组具体的操作,都是由上述工具类提供的API来完成的,下面简单介绍一下ArrayList中用到的相关API:

  • System.arraycopy() 方法
    // 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义
    /**
    *   复制数组
    * @param src 源数组
    * @param srcPos 源数组中的起始位置
    * @param dest 目标数组
    * @param destPos 目标数组中的起始位置
    * @param length 要复制的数组元素的数量
    */
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
  • Arrays.copyOf()方法
    public static int[] copyOf(int[] original, int newLength) 
    	// 申请一个新的数组
        int[] copy = new int[newLength];
	// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    

Arrays.copyOf()方法主要是为了给原有数组扩容


迭代器

ArrayList本身的源码并没有太多难点,但是容易被大家忽略的是ArrayList内部提供的Iterator,就是因为这个Iterator,才有了令很多人头疼的ConcurrentModificationException并发修改异常出现,下面就来看看Itr迭代器是如何实现的:

private class Itr implements Iterator<E> 
        //下一个要访问的元素下标---如果构造Itr的时候不传入,默认为0
        int cursor;      
        //上一个要访问的元素下标
        int lastRet = -1; 
        //代表对ArrayList修改次数的期望值,初始值为modCount
        int expectedModCount = modCount;

        Itr() 
        //是否还有下一个元素   
        public boolean hasNext() 
            return cursor != size;
        

        //获取下一个元素
        public E next() 
            //并发修改检查
            checkForComodification();
            //获取下一个要访问元素下标
            int i = cursor;
            //下标越界检查
            if (i >= size)
                throw new NoSuchElementException();
            //拿到当前数组元素集合
            Object[] elementData = ArrayList.this.elementData;
            //在获取元素期间有其他元素对数组进行了删除操作,会再次产生下标越界,说明出现了并发修改
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            //下一个要访问元素下标加一
            cursor = i + 1;
            //通过下标很长访问
            //大家思考: 如果是在获取元素其他增加了元素,那么这里通过下标获取到的和一开始期望的就不一致了
            //这里侧面说明,ArrayList提供的Itr也非线程安全的
            return (E) elementData[lastRet = i];
        

        public void remove() 
            if (lastRet < 0)
                throw new IllegalStateException();
             //并发修改检查   
            checkForComodification();

            try 
                //lastRet在不为-1的情况下,代表着当前元素
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                //更新修改次数
                expectedModCount = modCount;
                //产生越界说明出现并发操作情况
             catch (IndexOutOfBoundsException ex) 
                throw new ConcurrentModificationException();
            
        
        ....
        //只有调用了迭代器提供的remove和add方法才会更新expectedModCount的值
        //否则可以知道,在使用迭代器遍历当前list的期间,如果直接调用list提供的add和remove方法
        //那么便会更新modCount的值,而迭代器这边的expectedModCount并没有被更新
        //所以再次通过迭代器获取元素时,就会抛出异常了
        final void checkForComodification() 
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        
    

private class ListItr extends Itr implements ListIterator<E> 
      //其他方法不是不重要,而是限于篇幅原因,这里挑出典型讲清楚即可
       ...
       //通过迭代器向集合增加一个元素  
       public void add(E e) 
       //并发修改检查
            checkForComodification();

            try 
                int i = cursor;
                ArrayList.this.add(i, e);
                cursor = i + 1;
                lastRet = -1;
                expectedModCount = modCount;
             catch (IndexOutOfBoundsException ex) 
                throw new ConcurrentModificationException();
            
        
    

ArrayList迭代器部分不算特别难,但是通过分析其中典型源码,我们也可以明确使用迭代器时的一些坑,当然大多数情况下,我们都不会直接使用迭代器,而是间接使用它,例如使用增强for循环遍历集合的时候,查看编译过后的java代码可以知道,本质还是利用迭代器进行的遍历,如果我们在增强for循环中对List集合进行add和remove操作,便会抛出ConcurrentModificationException异常:

public class Main 
    public static void main(String[] args) 
        ArrayList<Integer> list=new ArrayList<>(20);
        list.add(1);
        list.add(2);
        list.add(3);
        for (Integer ele : list) 
            list.add(1);
        
    

解决方法有如下两种:

  • 调用迭代器提供的add和remove方法
public static void main(String[] args) 
    // 创建集合对象
    List list = new ArrayList();

    // 存储元素
    list.add("I");
    list.add("love");
    list.add("you");

    ListIterator lit = list.listIterator();
    while (lit.hasNext()) 
        String s = (String) lit.next();
        if ("love".equals(s)) 
            // add 、remove 都是可以的
            lit.add("❤");
        
        System.out.print(s + " ");
    

    System.out.println();

    for (Object l : list)
        System.out.print(l + " ");
    


//运行结果
I love you
I love ❤ you 
  • 集合遍历元素,集合修改元素(普通for)
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class Demo2 
    public static void main(String[] args) 
        //创建集合对象
        List list = new ArrayList();

        //存储元素
        list.add("I");
        list.add("love");
        list.add("you");

        for (int x = 0; x < list.size(); x++)
            String s = (String)list.get(x);
            if ("love".equals(s))
                list.add("❤");
            
            System.out.print(s + " ");
        
    


//运行结果
I love you ❤ 

iterator.remove() 的弊端:
Iterator 只有 remove() 方法,add 方法在 ListIterator 中有
remove 之前必须先调用 next,remove 开始就对 lastRet 做了不能小于 0 的校验,而l astRet 初始化值为 -1
next 后只能调用一次 remove,因为 remove 会将 lastRet 重新初始化为 -1


以上是关于ArrayList源码简析的主要内容,如果未能解决你的问题,请参考以下文章

专业吸猫20年!一对一直播源码,Java观察者模式案例简析

Android 开发也要懂得数据结构 - ArrayList源码

LinkedList插入数据效率不一定比ArrayList高,源码分析+实验对比

ArrayList源码分析-jdk11 (18.9)

永久修复尾部:无法观看“log/development.log”:设备上没有剩余空间

LinkedList 源码分析(JDK 1.8)