13 Star 66 Fork 72

大都督 / ArchGuide

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
01-java集合之ArrayList源码剖析.md 17.92 KB
一键复制 编辑 原始数据 按行查看 历史
王六六 提交于 2020-03-06 16:48 . 01-java集合之ArrayList源码剖析
  • 前言

  • 基本原理

  • 常用方法源码剖析

    • add()
    • add(index,element)
    • set()
    • get()
    • remove(index)
  • 数组扩容及元素拷贝

  • 总结

    • 优点
    • 缺点

前言

    ArrayList 是我们日常开发中使用非常频繁的一个集合,今儿我们通过源码来看看它底层是怎么来实现的,了解了解它的优缺点和真正适合的场景。

基本原理

    ArrayList ArrayList就是数组列表,主要用来装载数据,当我们装载的是基本类型的数据int,long,boolean,short,byte…的时候我们只能存储他们对应的包装类,它的主要底层实现是数组Object[]elementData。
    先来看一下ArrayList的内部组成源码:

    /**
     * Default initial capacity.
     * 默认容量
     */
    private static final  int DEFAULT_CAPACITY = 10 ;
    
    /**
     * Shared empty array instance used for empty instances.
     * 用于空实例的共享空数组实例。在初始化时,你传进去的初始化大小为0,那么就用这个来做的处理。
     */
    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.
     * 共享的空数组实例,用于默认大小的空实例,默认构造器使用的。
     */ 
    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; // non-private to simplify nested class access
    
    /**
     * The size of the ArrayList (the number of elements it contains).
     * 数组的大小
     * @serial
     */
    private  int size;
    
    再来看一下ArrayList的构造函数:

 “第一个”
     /**
     * Constructs an empty list with an initial capacity of ten.
     * 默认无参的构造函数,会给用来存储元素的数组赋值为一个空数组实例
     */
     //默认无参的构造函数, new ArrayList() 使用默认的构造函数来创建它,
     //那么它会直接使用一个空的 Object [],初始大小是0,
     //它有一个默认的初始化大小参数 DEFAULT_CAPACITY 是 10,
     //当添加第一个元素时,这个空数据的大小才是10,我们也可以认为它给我们创建的默认的数据大小就是10 
    public  ArrayList() { 
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
 “第二个”
    /**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
     //这个构造函数可以设置数组的容量,
     //一般来说是可以预估出来数据的大小,比如几十个,一百个,
     //那么我们在初始化的时候就把这个数字带进去,避免数据容量太小在后续插入数据的时候进行频繁的扩容,创建      //新的数组。
    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);
        }
    }
   
   
“第三个”
    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
     // 构造一个包含指定元素的列表
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

常用方法源码剖析

  • add()
    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    源码剖析:add()函数so easy,首先先校验一下当前ArrayList需不需要扩容,然后把数据的 size++ 位置设置为元素,最后返回true;

    来看一个案例解析:

/**
 * ArrayList
 */
public class ArrayListDemo {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<String>();

        stringList.add("张三");
        stringList.add("李四");
        stringList.add("王五");
    }
}

    最开始 elementData = [],是一个空的数组,size = 0,当 list.add("张三") 时,elementData[0] = "张三",size++ 后,size 的值就为 1 。

    然后 list.add("李四"),elementData[1] = "李四",当size++ 后,size 的值为 2,此时 elementData 的数据就是 ["张三","李四"] 。

    依次类推,当 list.add("王五") ,elementData[2] = "王五",添加了王五这个数据后,elementData = ["张三","李四","王五"] ,size = 3。


  • add(index,element)

/**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

    源码剖析:rangeCheckForAdd() 先校验一下当前index是否越界,ensureCapacityInternal()校验当前ArrayList需不需要扩容,System.arraycopy()数组的移动,然后数组当前index赋值,数组大小加1。

    来看一个案例解析:

stringList.add(1,"赵六");

    首先 rangeCheckForAdd(1) 会对传进来的 index 为 1 进行是否越界的处理 。接下来执行 ensureCapacityInternal(size + 1) 操作,是做数组扩容的,具体数据怎么扩容,此篇文章后面会解答。然后来到了比较重要的代码 ,对数组的数据进行移动

 System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);

    此时的index = 1 ,size = 3 , 我们来转化一下:

System.arraycopy(elementData, 1, elementData, 1 + 1,
                         3 - 1);
                         
=>

System.arraycopy(elementData, 1, elementData, 2,
                         2);

    那么这段代码表示的意思就是说 :

    elementData 这个数组,从第1位开始(第二个元素)拷贝,拷贝2个元素,把他们拷贝到 elementData 这个数组(还是原来的这个数组)从第2位开始(第三个元素)。

    然后 elementData[index] = element ,把 elementData[1] 设置成 “赵六”,

    最后 elementData 的数据就是 ["张三","赵六","李四","王五"]。

    最后再 size++ 后,size的值 为 4。


  • set()

    /**
     * Replaces the element at the specified position in this list with
     * the specified element.
     *
     * @param index index of the element to replace
     * @param element element to be stored at the specified position
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        rangeCheck(index);

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

   源码剖析:

        1)rangeCheck(index) 校验当前传进来的index是否数据越界,

        2) E oldValue = elementData(index) 先把当前index的元素拿出来,elementData[index] = element 数组当前index赋新传入的值,

        3)返回之前index的旧元素;

    来看一个案例解析:

String strTemp =  stringList.set(2,"马七")

   首先先进行 rangeCheck(index),检查给的索引是否越界,此时数组的个数是4,传入的索引下标是2,数组index没有越界,

   然后 E oldValue = elementData(index) 这段代码会先把索引为 2 这个位置的数据 “李四” 取出来,那么 oldValue = “李四”;

   再然后把 elementData[2] = “马七”,此时 elementData = ["张三",“赵六”,“马七”,"王五"],最后把 oldValue 的数据 “李四” 返回。


  • get()

    /**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

   源码剖析:

       首先先校验index 是否越界,然后直接调用 elementData(index) 方法,这个方法就是调用数组的取值。由于 elementData[index] 是直接通过数组直接定位这个元素,然后直接获取这个元素的,这个也是ArrayList 性能最好的一个操作,优点。

   案例:

String strTemp1 = stringList.get(0);

   根据index定位直接返回"张三"


  • remove(index)

    /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

   源码剖析:

       1)首先还是会校验索引是否越界,

      2)然后取出索引位置的数据,

      3)然后根据删除位置来计算需要移动元素的数量,

      4)如果移动需要移动元素的数量大于0 ,则使用 System.arraycopy 进行元素的移动,

      5)然后再把最后一个元素设置为 null,

      6)最终返回 删除的元素。

    案例:

 String strRemoveTemp = stringList.remove(1);

   remove(1),当前数组的长度 size 是 4 , oldValue = elementData(1) = "赵六"。

    numMoved = size - index - 1,numMoved = 4 - 1 -1 = 2,相当于要把后面的 “马七”,“王五” 都要往前面移动一位。

    接下来是 System.arraycopy(elementData, index + 1, elementData, index, numMoved) ,转换过来则为 System.arraycopy(elementData, 2, elementData, 1, 2)。

    意思是说把 elementData 数组中,从 index = 2 的位置开始的元素,一共有 2 个,拷贝到 elementData (还是原来的数组),从 index = 1 开始,进行拷贝。

拷贝前:elementData["张三","赵六","马七,"王五"]
拷贝后:elementData["张三","马七,"王五","王五"]

    然后再执行 elementData[--size] = null ,转化过来就是 elementData[3] = null ,把数组最后一个位置设置为 null。

    最后得到的 elementData = ["张三","马七","王五"]。

    最后再返回 oldValue “赵六”。


数组扩容及元素拷贝

    ArrayList 里面最关键的一块儿,就是扩容及元素的拷贝。

   假设说我们现在用的数组是默认的大小,也就是10 ,现在数据里面已经装满了10个元素,此时数组的size =10,capacity = 10。

   此时我们要调用 add() 方法 插入一个元素,插入第11个元素的时候是不行的,因为数据已经满了,此时就需要扩容。

   我们在 add( ) 方法中可以看到这样一行代码 :

ensureCapacityInternal(size + 1);  // Increments modCount!!

    此时 ensureCapacityInternal(11),minCapacity = 11。

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    调用 calculateCapacity(elementData, minCapacity) 后得到 minCapacity = 11

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    进入到 ensureExplicitCapacity(),会发现 minCapacity - elementData.length 是大于 0 的,所以会进入到 grow() 方法里面,这里面就会进行扩容。

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    首先

    oldCapacity = 10,

    newCapacity = oldCapacity + (oldCapacity >> 1),

    oldCapacity >> 1 相当于 oldCapacity/2 也就是 10 / 2 =5,

    那么 newCapacity = 15。

    此时新的数组 就变成了可以容纳15个元素的数据,老的数组是 10个。最后使用 Arrays.copyOf() 工具方法来完成老数组到新数组的拷贝。

    看到这里你就会发现数组扩容它是怎么来扩的,是老数据的大小+ 老数组大小 >> 1(相当于除以2) 来计算出来新数组的 大小,再使用 Arrays.copyOf() 来实现数据的拷贝。


总结

    优点:

        基于数组来实现,他在随机获取数组里的某个元素的时候,性能很高,他可以基于他底层对数组的实现来快速的随机读取到某个元素,直接可以通过内存地址来定位某个元素。

    缺点:

        1、若我们要频繁的向ArrayList 里面塞数据,会导致它频繁的数据扩容,会导致系统性能下降,所以说不要频繁的向ArrayList 插入数据。

        2、数组你要是往数组的中间加一个元素,要把数组中那个新增元素后面的元素全部往后面挪动一位,所以说,如果你是往ArrayList中间插入一个元素,性能比较差,会导致他后面的大量的元素挪动一个位置。

    使用场景:

        1、如果你不会频繁的在里面插入一些元素,不会导致频繁的元素的位置移动、数组扩容,就是有一批数据,查询出来,灌入ArrayList中,后面不会频繁插入元素了,主要就是遍历这个集合,或者是通过索引随机读取某个元素,使用ArrayList还是比较合适的。

        2、若果你涉及到了频繁的插入元素到list中的话,尽量还是不要用ArrayList,数组,定长数组,长度是固定的,元素大量的移动,数组的扩容+元素的拷贝。

1
https://gitee.com/dadudu1024/ArchGuide.git
git@gitee.com:dadudu1024/ArchGuide.git
dadudu1024
ArchGuide
ArchGuide
master

搜索帮助