ArrayList源码剖析

1,概述

先来简单聊聊对ArrayList的理解,后续再去源码中求证。

首先底层的数据结构是数组。因为是数组,所以缺点和优点都很明显:

缺点1:因为数组是定长的,所以当超过数组长度,就会数组扩容和元素拷贝,这个很消耗性能。

缺点2:当随机插入或者删除数据的时候,肯定会伴随着大量的数据移动。这个肯定也会很消耗性能。

优点:同样是因为数组,基于角标读取数据,所以随机读写性能非常高。

基于上述,所以ArrayList适合的场景应该是读多写少的场景

2,ArrayList核心成员变量

    /**
     * #默认的数组大小:10
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空对象数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
*默认的空对象数组
*/ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /**
*实际存储的对象数组
*/ transient Object[] elementData; // non-private to simplify nested class access /** * 数组大小 * * @serial */ private int size;

3,构造函数

3.1   ArrayList()

    public ArrayList() {
#初始化的时候将数组指向一个默认的空对象数组。也就是说ArrayList初始化的时候数据并不是10,那么为啥经常听到默认的大小是10呢?下面会看到
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }

 

3.2 ArrayList(int initialCapacity)

    public ArrayList(int initialCapacity) {
#如果传参大于0,则会初始化一个指定大小的数据。
if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }

 

3.3 ArrayList(Collection<? extends E> c)

    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; } }

4,核心方法

4.1 add(E e)

    public boolean add(E e) {
#判断是否需要扩容等操作,核心在这个方法。 ensureCapacityInternal(size
+ 1); // Increments modCount!!
#元素赋值
elementData[size++] = e; return true; }

那就继续看看ensureCapacityInternal()方法都有啥操作

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

calculateCapacity()方法:

  #这个方法如果是无参构造函数刚传入的时候,返回10;如果是有参构造函数,传入size+1
private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }

 ensureExplicitCapacity()方法:

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

        // #从这里就明显看出来了。如果传入的是无参构造函数的时候,会进行扩容,扩容的数组大小就是10了,这就是为啥有人说初始化的数组大小是10。另外就是每次都判断一下,size+1大于数组长度,也要进行扩容。
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
grow()方法:可以大胆的猜测了,这个方法一个就是实现了数组的扩容,一个就是实现了元素拷贝了。
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
#这里表明数组扩容的大小是原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 这一段话就是将数组扩大1.5倍,然后将原来的元素再放入新的数组中 elementData = Arrays.copyOf(elementData, newCapacity); }

 add()方法需要学习到的点:

1,无参构造函数并不是开始就给了一个长度为10的数组,而是在add()方法执行的时候,判断扩容的时候初始化了一个长度为10的数组。

2,数组扩容的大小是1.5倍:newCapacity = oldCapacity + (oldCapacity >> 1)。记得HashMap的数组扩容是2倍。

3,学学Arrays工具类的使用。将数组扩容到多大,然后将元素赋值到新数组的方法:elementData = Arrays.copyOf(elementData, newCapacity);

4.2 get(int index)

    public E get(int index) {
#判断角标是否越界 rangeCheck(index); #通过数组角标获取值
return elementData(index); }

get()方法很简单,没啥特别的关注

4.3  add(int index,E element)

    public void add(int index, E element) {
#判断角标是否越界 rangeCheckForAdd(index); #这个方法在add()见过了,功能就是判断是否需要扩容,如果扩容就需要把之前的元素赋值到新的数组中去。 ensureCapacityInternal(size
+ 1); // Increments modCount!!
#这个方法是一个native方法,它的功能是在index角标位置处,将元素后移一位,也就是把index角标的位置腾出来。可以看到这些数据的大量移动都是消耗性能的 System.arraycopy(elementData, index, elementData, index + 1,size - index);
#将元素插入到index角标处 elementData[index]
= element; size++; }

add(int index,E element)方法需要学习的点:

1,在指定角标插入数据,往往就伴随着数据的移动了,数组越长,移动的数据就越多。这都是很消耗性能的。

2,学着用System.arraycopy(elementData, index, elementData, index + 1,size - index)方法。

 

    public static void main(String[] args) {
//        String[] arr = {};
//        arr=Arrays.copyOf(arr,10);
//        System.out.println(arr.length);
        String arr[] = new String[10];
        int index = 3;
        //{"a","b","c","e","f"}
        arr[0] = "a";
        arr[1] = "b";
        arr[2] = "c";
        arr[3] = "e";
        arr[4] = "f";
        System.arraycopy(arr, index, arr, index + 1, 2);
        arr[index] = "d";
        for (String ss : arr) {
            System.out.println(ss);
        }

    }

 

效果:

 5,小结

其他还有remove(),set()方法等等就不详细说了,都是挺简单的,就是围绕着操作这个数组,然后数据是否需要移动,是否需要扩容等等。总而言之,使用ArrayLIst最好事先知道大概量,给个预定值,减少它的数组扩容和元素移动。

posted @ 2019-08-27 00:58 一缕清风007 阅读(...) 评论(...) 编辑 收藏