ArrayList源码学习笔记

ArrayList作为我们平常使用最多的容器之一,有必要深入了解下它了。在查看其源码之前,我们先抛出以下问题,带着问题我们更具有目的性。
1、ArrayList的本质是什么?
2、ArrayList的使用是否会出现OOM?
3、ArrayList是否线程安全?
4、ArrayList的使用过程中,做了哪些事情?
5、ArrayList的效率如何,适用于哪些场景?
其实针对第一个问题,我们看真正存储我们数据的容器就可以猜测得到了。

transient Object[] elementData;

是的,其实ArrayList就是一个可以自增长的数组,这点在我们分析第4点的时候会得到验证。那么既然是数组,其本质就是在内存中分配一段连续的区域来进行存储的。请注意,是一段连续的内存区域,如果不能够连续,即使有在多的区域也无法满足我们的要求。从而我们可以推断出第二个问题的答案,其实ArrayList使用是会导致OOM的,我们可以进行如下验证:

new Thread(){
            @Override
            public void run() {
                super.run();
                List<String> list = new ArrayList<>();
                List<String> list1 = new ArrayList<>();
                for (int i = 0; i < 1000000; i++){
                    list1.add("I am "+i);
                }
                while (true){
                    list.addAll(list1);
                    list.add("a");
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("size:"+list.size());
                }
            }
        }.start();

上面的代码很简单,就是在线程里尝试不断的让ArrayList添加数据,最终会抛出如下异常:

java.lang.OutOfMemoryError: Failed to allocate a 307546884 byte allocation with 4194304 free bytes and 136MB until OOM

很粗暴的感觉有木有?有的朋友可能会说你这样搞个死循环,最后肯定会出问题丫。是的,我们不是也是想知道它出的是什么问题么,当看到OutOfMemoryError的时候,不是已经验证了ArrayList会导致OOM这一结论。只是为什么,我们还需要进一步的考究。好的,是时候上正菜了,先看其构造函数。

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
//无参构造函数  elementData数组赋值为默认的一个空长度的数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
private static final Object[] EMPTY_ELEMENTDATA = {};
//initialCapacity为初始容量
public ArrayList(int initialCapacity) {
//当构造的时候,传进来一个用户希望的初始容量
        if (initialCapacity > 0) {
        //大于0 则构造一个initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
        //等于0 则赋值为一个空长度的数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
        //如果小于0,则抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
//以集合的方式来构造ArrayList
public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            //只是这里,如果传进来的非Object[].class类型,需要进行一下转换
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

以上的代码整体都很简单,就是初始化一个ArrayList对象,构造方法参数不一样,执行的流程略微有一些差别,但最终的目的不都是为elementData 数组申请内存。好的,数组已经有了,我们看下其操作,这里我们主要看常用的add、remove,其实其余的都类似,只是各自负责的功能不一样。

//直接添加元素,添加在数组的末尾
public boolean add(E e) {
		//这个方法实际是做扩容检测以及扩容的,看后面的代码解释  size是实际数组里面已经填充了数据的大小
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //赋值
        elementData[size++] = e;
        return true;
    }
//在指定位置添加元素
public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
		//这个方法实际是做扩容检测以及扩容的,看后面的代码解释 size是实际数组里面已经填充了数据的大小
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //将index之后的数据向后移动
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //赋值
        elementData[index] = element;
        size++;
    }
private static final int DEFAULT_CAPACITY = 10;
private void ensureCapacityInternal(int minCapacity) {
//如果是默认的数组,则重新定义其长度大小然后调用ensureExplicitCapacity
//比如初始是0,现在add,则minCapacity为1 最终取值后minCapacity=DEFAULT_CAPACITY  10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
private void ensureExplicitCapacity(int minCapacity) {
//modCount在每次操作ArrayList的时候都会++,防止在迭代器遍历的时候进行数据操作 那样的话会报java.util.ConcurrentModificationException异常
        modCount++;
        // overflow-conscious code
        //判断需求的长度是否大于当前数组长度,如果大于则说明需要扩容
        if (minCapacity - elementData.length > 0)
        //真正进行扩容操作的方法
            grow(minCapacity);
    }
private void grow(int minCapacity) {
        // overflow-conscious code
        //数组当前的长度
        int oldCapacity = elementData.length;
        //新的长度 为旧长度+旧长度>>1 实际就是每次扩容一半大小 比如10 则扩容到15
        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:
        //执行数组扩容,并将之前的数据拷贝过去 具体执行者为Arrays类的静态方法copyOf
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
public static byte[] copyOf(byte[] original, int newLength) {
        byte[] copy = new byte[newLength];
      	//拷贝数组数据的执行者,我们平常做数组拷贝不也这样用么?
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

上面的代码就是add操作的流程代码。先判断是否需要扩容,如果不需要则回到add方法将数据赋值给数组的下标为size+1的数据。如果需要扩容,则进行扩容,每次扩容大小为1.5倍,然后在执行赋值操作。扩容需要做数组复制操作,如果add的不是数组末端,也需要做数组移动操作。接下来,我们在看下remove操作。

public E remove(int index) {
//边界条件判断
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        modCount++;
        //数组index下标的值
        E oldValue = (E) elementData[index];
        //进行下标界定并移动数组 比如现在有10个值,将第5个值移除,那么需要将后面的值依次向前移动一位
        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;
    }
//这个方法最终调用的关键方法fastRemove
public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
//可以看到这个方法最终也是执行了数组移动
private void fastRemove(int index) {
        modCount++;
        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
    }

ArrayList进行数据移除,其实就是在数组中,找到对应的下标或者对应的数据,然后依次将后面的数据向前移动一位,之前在对最后一位的数据赋空,注意这里的最后一位是指数组里面实际有数据的大小,由size进行记录。
好了,看完上面的关于初始化、添加、移除的代码,有没有一种其实就是在对数组进行的操作,本质上与我们操作数据并没有什么区别,区别在于进行了封装,可以进行自动扩容,数组的移动也在调用对应的方法之后自动完成,基于上面的分析我们可以来总结下我们在开始提出的问题了。

1、ArrayList的本质是什么?
其本质就是对数组进行的封装,能实现数组的扩容,在操作的时候会对数组进行对应的移动,这一切的封装只是为了方便我们的操作。
2、ArrayList的使用是否会出现OOM?
我们在开始就验证了这个问题,是会出现OOM的。因为其本质是使用了数组,而数组是内存中一段连续的区域。在扩容中,如果找不到我们所需求的那样一段连续的内存区域,则会出现OOM。
3、ArrayList是否线程安全?
我看到不管add或者remove,在操作过程中并没有synchronized关键字的修饰,也没有锁的出现,那么我们可以断定ArrayList是非线程安全的。
4、ArrayList的使用过程中,做了哪些事情?
在add的时候会进行数组扩容,每次为1.5倍,之后进行数组的复制,在将值添加进去。
remove的时候会将待移除数据之后的数据全部向前移动一位,在将最后一位值赋空。
5、ArrayList的效率如何,适用于哪些场景?
从前面的问题可以总结出ArrayList实际就是对数组的操作,如果添加操作,数组的扩容会对效率有一定的影响。如果添加在非末端,那么还需要额外进行一次数组的移动过程,效率是有所牺牲的。移除如果在末端,不受影响,如果在非末端,需要进行一次数组移动过程,有一定的影响。ArrayList更适用于访问次数多(访问实际相当于对数组的访问,直接可以通过下标进行访问,而且快),添加、移除少操作(特别是随机位置的添加、移除)的场景。否则,则需考虑另外的如LinkedList。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值