ArrayList源码解析

2018-09-10  本文已影响0人  代码potty

看了一周的SpringMVC请求的过程,突然对老伙计,ArrayList这些我们使用比较多的集合类产生了兴趣,下面开始解析ArrayList源码

ArrayList 继承了AbstractList类
实现的接口有:List, RandomAccess, Cloneable, java.io.Serializable

首先ArrayList有四个静态域
1、serialVersionUID:序列化和反序列需要用到的id
2、DEFAULT_CAPACITY:默认的容量大小
3、EMPTY_ELEMENTDATA:是一个静态的空数组
4、DEFAULTCAPACITY_EMPTY_ELEMENTDATA:作用同EMPTY_ELEMENTDATA 一样。区别是这里表明创建这个类的时候没有指定capacity而是使用默认的DEFAULT_CAPACITY 。

transient Object[] elementData;//临时存放元素的地方,不加入序列化
ArrayList有三种构造函数:
第一种无参构造函数:

    public ArrayList() {
       this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

第二种是设置容量大小的构造函数

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

第三种是直接传入一个集合作为参数:

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

前面两种构造方法都比较简单,第三个构造方法有点不一样的地方,首先第一个if语句,将elementData.length赋值给了size,然后进行里层的if语句判断,为什么还要进行类型判断呢,注释中解释道
c.toArray might (incorrectly) not return Object[] (see 6260652),查过这个代码后,发觉问题源自参数,
如果是ArrayList.toArray()方法,返回的数组就是Object[]类型,但是Arrays.asList(...).toArray,返回的数组呢
Arrays.asList(...)返回的是class java.util.Arrays$ArrayList类型,这种类型的toArray()方法返回的是实际数据的类型,String类型的,那么toArray()方法就会返回String类型,就是因为有这种情况,所以在里层加多了一个判断,将elementData类型转换成Object[]类型。

在看过数据的增加的时候,印象最深的当属对于容量的处理工作,当数组大小超过默认的容器大小后,那么需要执行扩容语句,扩容涉及的部分:
public void ensureCapacity(int minCapacity)
private static int calculateCapacity(Object[] elementData, int minCapacity)
private void ensureCapacityInternal(int minCapacity)
private void ensureExplicitCapacity(int minCapacity)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
private void grow(int minCapacity)
private static int hugeCapacity

看到这些方法,最直观的是不是ensureCapacity是public,而其他的方法都是private,实际上,ensureCapacity方法在内部源码中是没有调用到的,这个方法是提供给用户进行设置容量大小的。他的代码如下:

    public void ensureCapacity(int minCapacity) {
            int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                // any size if not default element table
                ? 0
                // larger than default for default empty table. It's already
                // supposed to be at default size.
                : DEFAULT_CAPACITY;
    
            if (minCapacity > minExpand) {
                ensureExplicitCapacity(minCapacity);
            }
        }
    
        private static int calculateCapacity(Object[] elementData, int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            return minCapacity;
        }
    
        private void ensureCapacityInternal(int minCapacity) {
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }
    
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

如果调用该方法的集合是空集,那么就会默认给这个集合内的elementData初始化大小10的容量,否则容量为0,在下边的增加数据的代码块中,都出现了ensureCpacityInternal()方法的调用,最终完成扩容工作的是ensureExplicitCapacity()方法。

扩容数组:

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    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);
       }

这个方法的思路就是,先将原来的容量增加原来容量的一般,然后跟传入的容量进行比较,如果小于传入的容量,那么就将传入的容量赋值给扩容的容量,然后进行第二次判断,判断扩容的容量是否会大于数组的最大容量,如果大于数组最大的容量,那么将会有两个选择,如果传入的容量大小大于数组最大容量,则将扩容的容量赋值为整型的最大值,如果传入的容量大小小于数组最大容量,则将扩容的容量赋值为数组最大的容量。

增删
1、增加
public boolean add(E e)
public void add(int index, E element)
public boolean addAll(Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c)
增加的方法四种,刚刚说过,四种都调用了扩容判定的方法

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
    
    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++;
        }
    
    
    public boolean addAll(Collection<? extends E> c) {
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacityInternal(size + numNew);  // Increments modCount
            System.arraycopy(a, 0, elementData, size, numNew);
            size += numNew;
            return numNew != 0;
        }
    
    public boolean addAll(int index, Collection<? extends E> c) {
            rangeCheckForAdd(index);
    
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacityInternal(size + numNew);  // Increments modCount
    
            int numMoved = size - index;
            if (numMoved > 0)
                System.arraycopy(elementData, index, elementData, index + numNew,
                                 numMoved);
    
            System.arraycopy(a, 0, elementData, index, numNew);
            size += numNew;
            return numNew != 0;
        }       

这里我要讲的是System.arraycopy()的调用,该方法的参数顺序为:拷贝数组,开始位置,拷入数组,开始位置,长度。这也是我遇到的第一个native方法,在底层源码中看不到native的实现,拷贝的时候要注意拷贝的长度计算,跟删除略有不同。

2、删除
public E remove(int index)
public boolean remove(Object o)
private void fastRemove(int index)
删除涉及这三个方法的使用

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

删除中,可以看到在长度上,增加是length = size -index ,而删除是length = size -index -1
这个方面自己算个例子就很容易明白了,这边要注意的是,删除一个object的时候,要判断变量是否为空,为空直接使用==null的方式,不为空要使用equals()方法进行判断,还有一个点是,删除完数据后,要记得将位置置为null,让java的gc机制回收不用的内存空间

讲完ArrayList对于增删的处理后,下边讲下这两个方法
1、public boolean removeAll(Collection<?> c)
2、public boolean retainAll(Collection<?> c)
第一个方法的代码为:

    public boolean removeAll(Collection<?> c) {
            Objects.requireNonNull(c);
            return batchRemove(c, false);
        }

第二个方法的代码为:

    public boolean retainAll(Collection<?> c) {
            Objects.requireNonNull(c);
            return batchRemove(c, true);
        }

可以看到,首先这两个方法先判断传入的集合进行了非空检测,检测完后返回的是batchRemove()方法的返回值,不同的是第二个参数设置一个为true,一个为false。
为了更容易理解,我先在这边介绍一下这两个方法的功能
第一个方法是移除list集合中有关集合c的元素
第二个方法是移除list集合中除集合c元素外的其他所有元素
也就是两个方法互为补集

下边来看下代码块的实现

    private boolean batchRemove(Collection<?> c, boolean complement) {
            final Object[] elementData = this.elementData;
            int r = 0, w = 0;
            boolean modified = false;
            try {
                for (; r < size; r++)
                    if (c.contains(elementData[r]) == complement)
                        elementData[w++] = elementData[r];
            } finally {
                // Preserve behavioral compatibility with AbstractCollection,
                // even if c.contains() throws.
                if (r != size) {
                    System.arraycopy(elementData, r,
                                     elementData, w,
                                     size - r);
                    w += size - r;
                }
                if (w != size) {
                    // clear to let GC do its work
                    for (int i = w; i < size; i++)
                        elementData[i] = null;
                    modCount += size - w;
                    size = w;
                    modified = true;
                }
            }
            return modified;
        }

首先创建了一个数组用来接收临时数组,然后分别创建了两个整型,一个是r,一个是w,整型r的作用类似于for循环第一个参数new出来的变量,用于遍历整个数组,整型w的作用是计算经过操作后,筛选出来的元素的个数,还有个布尔类型modified,默认为false,判断方法执行是否成功的元素。
所以r一般是大于w的,我们看到在finally代码块里有两个if语句,第一个判断语句r!=size,遍历整个数组的情况下,r是等于size的,只有出现异常,无法再继续执行下去的时候r!=size,将r后边为处理的数据加到w的后边。第二个判断语句w!=size,如果w等于size,说明筛选出来的元素是整个数组,那么就没有需要剔除的元素,也就是没有修改集合,返回默认的false,如果w!=size,则将List数组的w之后包括w全部置为null,让GC回收。

代码接着往下走,我们发现到了序列化和反序列化的方法,ArrayList都已经实现了Serializable接口,为何还要自己写序列化和反序列化方法呢,看代码:

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
            // Write out element count, and any hidden stuff
            int expectedModCount = modCount;
            s.defaultWriteObject();
    
            // Write out size as capacity for behavioural compatibility with clone()
            s.writeInt(size);
    
            // Write out all elements in the proper order.
            for (int i=0; i<size; i++) {
                s.writeObject(elementData[i]);
            }
    
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }
    
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            elementData = EMPTY_ELEMENTDATA;
    
            // Read in size, and any hidden stuff
            s.defaultReadObject();
    
            // Read in capacity
            s.readInt(); // ignored
    
            if (size > 0) {
                // be like clone(), allocate array based upon size not capacity
                int capacity = calculateCapacity(elementData, size);
                SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
                ensureCapacityInternal(size);
    
                Object[] a = elementData;
                // Read in all elements in the proper order.
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }       

发现了什么,序列化和反序列化中的循环变量是以size为准的,在ArrayList中,有两个变量容易弄混,一个是size,一个是elementData.length,第一个是数组中实际拥有元素的个数,第二个是数组的长度,数组长度是大于等于数组拥有元素个数的,所以如果自己写序列化和反序列化语句,那么可以节省这部分的内存开销。

再之后的代码就是关于迭代器方面的,这部分我还没研究,暂时不讲。纵观ArrayList源码,通篇有两个要特别注意的点,就是Arrays.copyOf()和System.arraycopy()
copyOf()的作用是拿来扩容用的
arraycopy()的作用是进行数组内元素移位用的
这是二者最大区别,并且这两个方法也不是一个类的方法
copyOf()是Arrays的方法,arraycopy()是System的方法

说到这里,就有必要说一下ArrayList的toArray()方法,看代码:

    public Object[] toArray() {
            return Arrays.copyOf(elementData, size);
        }       

这个是不带参数的toArray(),内部是通过Arrays.copyOf来实现的,比较简单

    public <T> T[] toArray(T[] a) {
            if (a.length < size)
                // Make a new array of a's runtime type, but my contents:
                return (T[]) Arrays.copyOf(elementData, size, a.getClass());
            System.arraycopy(elementData, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }

而这个带有数组类型参数的toArray(),就相对复杂点,但是对比两个方法,可以看出来无参的toArray()方法是使用Arrays.copyOf(),有参的toArray()方法是通过System.arraycopy()和Arrays.copyOf()进行实现的,我解释一下有参的方法实现。首先List.toArray(T),,这个方法是将list的数据拷贝到T中,并且返回的也是T的类型,先判断T和List的长度,如果拷入数组T的长度小于拷贝数组List的长度,则调用Arrays.copyOf()方法,直接将拷贝数组进行类型转换,如果拷入数组T的长度大于拷贝数组List的长度,则调用System.arraycopy()将拷入数组T的0~List.length范围数据置为List,然后list.length的位置置为null,这样其实可以达到区分拷贝前后数据的作用,测试代码如下:

    public void testGoods() {
            ArrayList<String> list = new ArrayList<>(Arrays.asList("s","t","r"));
    
            String[] str = {"q","w","r","c","z"};
    
            str = list.toArray(str);
    
            for (String s :str)
                System.out.println(s);
    
        }

效果图:

QQ图片20180910223854.png

如果我将str的数据删除到只剩下一个数据,效果图如下:

image.png

这两种情况通过代码就很直观的显示出来了。

总结
1、如果在声明ArrayList的时候不设初始值,那么ArrayList会默认设置一个容量为10的数组,但是ArrayList的大小还是为0的

2、ArrayList可以看成是一个动态的数组,相比较与数组来说,ArrayList可以用ensureCapacityInternal方法自动扩容,在向ArrayList添加元素的时候,ArrayList会先检测现在数组的容量是否足够,若不够,ArrayList会将数组的容量扩大为原来的1.5倍,如果还不够,就用传进来的参数作为数组的容量。如果我们在知道存储元素多少的时候,尽量给ArrayList设定一个初始容量,这样就可以减少ArrayList的自动扩容,减少数组元素的移动来提高程序的性能。

3、ArrayList在增加或删除元素的时候,都需要将数组里面的元素移动到另一个数组中,这是非常耗时间的,所以遇到需要对元素进行频繁的删除和添加的集合时,这时候选用LinkedList要比ArrayList好很多,如果遇到在集合中查找元素比较多的操作时,ArrayList又是一个不错的选择,因为ArrayList直接可以用下标就可以获取到元素。

4、在读ArrayList源码的时候要注意ensureCapacityInternal扩容方法和System.arraycopy(original, 0, copy, 0,length)方法。

问题解析:
来讲一下ArrayList中遇到的一个问题,就是关于addAll(int index,Collection<? extends E> e)方法
该方法第一行代码就是对index进行的判断

    public boolean addAll(int index, Collection<? extends E> c) {
            rangeCheckForAdd(index);
    
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacityInternal(size + numNew);  // Increments modCount
    
            int numMoved = size - index;
            if (numMoved > 0)
                System.arraycopy(elementData, index, elementData, index + numNew,
                                 numMoved);
    
            System.arraycopy(a, 0, elementData, index, numNew);
            size += numNew;
            return numNew != 0;
        }       
    private void rangeCheckForAdd(int index) {
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }       

可以看到第一个方法对于index的判断,index大于size或者小于0则抛出异常,也就是说size>=index>=0
我当时的疑问出于size怎么可以等于index,因为index是数组的下标,肯定比个数少一,然后在大佬的指点下,发觉,如果我的index==size,那么就是在数组尾插入集合C,这样是符合标准的,但是index==size,那么原数组就不需要移动了,如果没有加上长度的判定,那么会出现BUG,怪自己粗心。

参考链接:
https://blog.csdn.net/wangbiao007/article/details/52474756
http://www.cnblogs.com/ITtangtang/p/3948555.html#toArray
https://blog.csdn.net/weixin_39148512/article/details/79234817
https://blog.csdn.net/u011518120/article/details/52026076
https://www.cnblogs.com/gxl1995/p/7534171344218b3784f1beb90d621337.html

上一篇下一篇

猜你喜欢

热点阅读