ArrayList源码浅析

Posted darkclouds

tags:

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

也快要秋招了,博客也没有任何的代码,只有几个遇到的问题记录,所以就写些吧,顺便复习下,如果有哪块写的有问题,欢迎大家批评指正。

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    //默认的一个初始化数组大小
    private static final int DEFAULT_CAPACITY = 10;
    //这两个都是空数组,主要是用来初始化容器或者判断某些给的形参数组是否为空
    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //实际存放对象的数组
    transient Object[] elementData; 
    //当前的数组中存放的对象的个数
    private int size;
    
    
    
    
    //我们平时最常用的无参构造方法
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    //比较少用的有参构造方法,直接将给的形参容器复制到我们的arraylist中
    public ArrayList(Collection<? extends E> c) {
        //将容器转化为数组
        elementData = c.toArray();
        //容器是否不是空的,给size(也就是当前的arraylist中对象的个数)中赋值。
        if ((size = elementData.length) != 0) {
            //判断字节码对象是否相等,也就是类型是否一致,字节码对象只会在第一次加载这种类时加载字节码,且只加载一次
            if (elementData.getClass() != Object[].class)
                //将内置的数组赋值为该容器转化的数组
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //将内置的数组赋值为空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
    
    //我们平时常用的add方法
    public boolean add(E e) {
        //确保容量足够,因为每次添加一个对象,所以给的参数是size+1
        //这个方法调用的是最关键的地方,在这之后会调用一个方法将容器的容量扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //上面已经确保了容量足够,且没有异常
        //所以直接给当前数组的不是null的那个最末尾的下表的下一个赋值
        //注意:size是真正的当前存储的对象的个数
        //而elementData这个数组是很有可能有一部分位置是空的,也就是null
        elementData[size++] = e;
        return true;
    }
    
    //确定容量方法,其内调用了一个确定最小容量和一个确定详细容量两个方法
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    //这个方法内就是判断下elementData是否是空的,是空的就直接判断这个size+1是否比默认的初始容量10大
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //比较minCapacity是否比10大
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
        //总之就是返回一个最小的容量数值
    }
    
    //该方法确定当前的容量是否足够,并决定是否增加容量
    private void ensureExplicitCapacity(int minCapacity) {
        //modCount用来记录修改次数,在迭代器内判断是否出现并发修改异常,这个先不管
        modCount++;

        //上面也说过了,elementData很可能是有一部分是null
        //所以还需要判断一下,elementData是否足够大
        //一般添加一个元素,elementData.length和size是一样大的
        //但是不排除我们会使用addAll(Collection<?> c)方法
        if (minCapacity - elementData.length > 0)
            //容量不够,增加容量,将calculateCapacity方法确定的最小容量传入
            grow(minCapacity);
    }
    //增长容量方法
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //(oldCapacity >> 1)是右移操作,相当于除2
        //但是这个是最快的,直接将命令给cpu的,我们平时除2也可以这样,能稍微快点,上过组成原理的同学应该知道
        //这里就是得到一个elementData.length的1.5倍的值,准备和minCapacity比较出一个合理的值
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果这个1.5倍的值大于最小容量,那么我们直接取值最小容量就可以
        //没必要用1.5倍的值,数组是固定大小,会浪费内存
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //MAX_ARRAY_SIZE的定义为private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        //MAX_ARRAY_SIZE已经很大,接近int的极限,如果比这个值还要大
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            //得出一个容量值,这个值会非常大
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //这句英文说这个这最小容量通常接近于我们elementData的实际存储对象个数
        //大概他觉得我们最长使用的是add(E e)这样的添加很少的元素对象,不会一次添加大量元素对象
        //System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));
        //这个Arrays.copyOf之后会调用上面这一句代码
        //original是elementData,newLength是newCapacity,copy是完成扩容后的数组
        //最后会return copy,也就是完成了这一次扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    //比较出一个合适的容量值的方法
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError
                ("Required array size too large");
        //这里Integer.MAX_VALUE这个值可能会报错
        //因为jvm会有一个默认的内存大小,这个值为21亿的MAX_VALUE可能会超过这个内存
        //和数组下标越界异常类似,但是这回是内存不够,可以调整jdk默认内存解决
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    
    //知道了add(E e)方法后,其他方法也大相径庭
    
    //
    public E remove(int index) {
        //检查是否超出边界范围,只有一句判断是否判处数组越界异常
        //private void rangeCheck(int index) {
        //    if (index >= size)
        //        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        //}
        rangeCheck(index);
        //同add方法一样,modCount用来记录修改次数
        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            //其实ArrayList的改变大小就是靠System.arraycopy这个本地方法完成的
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //清除数组对那个删除对象的引用,垃圾回收器才会回收那个没有引用标记的对象
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

}

 上面提到了两个可能会有问题的地方,一个是Integer.MAX_VALUE这个值会使得jvm内存不够分配报异常,一个是modCount记录修改次数,也可能报异常

先说第一个地方

     try {
            int[] a=new int[Integer.MAX_VALUE-2];
            System.out.println(a.length);
        } catch (java.lang.OutOfMemoryError e) {
            e.printStackTrace();
        }
        try {
            int[] b=new int[Integer.MAX_VALUE];
            System.out.println(b.length);
        } catch (java.lang.OutOfMemoryError e) {
            e.printStackTrace();
        }

这个代码执行后会报错误,Java heap space是内存快到极限时抛出,Requested array size exceeds VM limit是已经在内存中找不到该内存位置了,解决方法是调整jvm默认分配内存大小

技术图片

 

第二个地方modCount,先看这么一段代码

     ArrayList<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            list.remove(0);
        }

运行后,会 抛出java.util.ConcurrentModificationException,这样的一个异常,为什么会抛出这样的一个异常呢?并没有多个线程同时修改集合啊

技术图片

 

 先看下Iterator<Integer> iterator = list.iterator()中的iterator是什么

技术图片

 

 继续往下,看到一个内部类

技术图片

 

 这个类中的next方法,可以看到调用了checkForComodification方法,下面的if (i >= elementData.length) throw new ConcurrentModificationException();是为了防止多线程修改

技术图片

 

 调用的checkForComodification方法

技术图片

 

 到这里知道了,每次ArrayList去增删改元素对象的时候,改变了modCount,而其内部类对象的expectedModCount只是修改前的一个modCount的值,

所以调用next方法时,如果使用ArrayList的增删改,那么就会抛出这么一个异常,解决方法是使用迭代器的remove方法

技术图片

 

 可以看到,这个remove方法中,expectedModCount和modCount每次都会同步下,所以不会报异常了,

但是它只有remove方法,如果想要使用其它方法可以使用list.listIterator()获取一个更加强大的迭代器

 

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

ArrayList源码浅析

ArrayList源码浅析(jdk1.8)

jdk1.7 ArrayList源码浅析

JDK源码--ArrayList浅析

Collections.synchronizedList CopyOnWriteArrayListVector介绍源码浅析与性能对比文末福利

由LintCode问题子集出发,浅析ArrayList的拷贝问题