Java 集合 ArrayList 源代码分析(带着问题看源码)

今天学习下ArrayList的源代码,不一样于其余人写的博客,不少都是翻译源代码中的注释,而后直接贴到文章中去。小编打算换一种书写风格,带着问题看源码可能收获会更大,本文将围绕着下面几个问题展开讨论。java

1、问题产生

  • 一、为何ArrayList集合中存储元素的容器声明为transient Object[] elementData;程序员

  • 二、既然ArrayList能够自动扩容,那么它的扩容机制是怎样实现的?数组

  • 三、调用ArrayListiterator()返回的迭代器是怎样的?bash

  • 四、采用ArrayList的迭代器遍历集合时,对集合执行相关修改操做时为何会抛出ConcurrentModificationException,咱们该如何避免?运维

  • 五、当集合扩容或者克隆时免不了对集合进行拷贝操做,那么ArrayList的数组拷贝是怎么实现的?oop

  • 六、ArrayList中的序列化机制post

小编对ArrayList源码大概浏览了以后,总结出以上几个问题,带着这些问题,让咱们一块儿翻开源码解决吧!学习

2、问题解答

一、为何ArrayList集合中存储元素的容器声明为transient Object[] elementData;

ArrayList是一个集合容器,既然是一个容器,那么确定须要存储某些东西,既然须要存储某些东西,那总得有一个存储的地方吧!就比如说你须要装一吨的水,总得有个池子给你装吧!或者说你想装几十毫升水,总得那个瓶子或者袋子给你装吧!区别就在于不一样大小的水,咱们须要的容器大小也不相同而已!ui

既然ArrayList已经支持泛型了,那么为何ArrayList源码的容器定义为何还要定义成下面的Object[]类型呢?this

transient Object[] elementData;

其实不管你采用transient E[] elementData;的方式声明,或者是采用transient Object[] elementData;声明,都是容许的,差异在于前者要求咱们咱们在具体实例化elementData时须要作一次类型转换,而此次类型转换要求咱们程序员保证这种转换不会出现任何错误。为了提醒程序员关注可能出现的类型转换异常,编译器会发出一个Type safety: Unchecked cast from String[] to E[]警告,这样讲不知道会不会很懵比,下面的代码告诉你:

public class MyList<E> {
    // 声明数组,类型为E[]
    E[] DATAS;
    // 初始化数组,必须作一次类型转换
    public MyList(int initialCapacity) {
    	DATAS = (E[]) new Object[initialCapacity];
    }
    public E getDATAS(int index) {
    	return DATAS[index];
    }
    public void setDATAS(E[] dATAS) {
    	DATAS = dATAS;
    }
}
复制代码

上面的代码在1处咱们声明了E[]数组,具体类型取决于你传入E的实际类型,可是要注意,当你对DATAS进行初始化时,你不能像下面这样初始化:

E[] DATAS = new E[10]; // 这句代码将报错

也就是说,泛型数组是不能具体化的,也就是不能经过new 泛型[size];的方式进行具体化,那么怎么解决呢?有两种方式:

  • 一、进行前面说的作一次转换,但不推荐

    就像上面代码所展现的,咱们能够初始化成Object[]类型以后再转换成E[],但前提是你得保证此次转换不会出现任何错误,一般咱们不建议这样子写!

  • 二、直接声明为Object[]

    这种方式也是ArrayList源码的定义方式,那么咱们来看看ArrayList是怎么初始化的:

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 此处直接new Object[],不会出现任何错误
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
复制代码

可是有一点还须要注意,但你调用ArrayListtoArray方法将集合转换为对象数组时,有可能出现意想不到的结果,具体可参考小编的另一篇博文。

[ArrayList 其实也有双胞胎,但区别仍是挺大的!]

总结: 总的来讲,咱们要知道泛型数组是不能具体化的,以及其解决办法!你可能会很好奇我为何没有讲transient,这个小编放到下面序列化反序列化时讲。

二、既然ArrayList能够自动扩容,那么它的扩容机制是怎样实现的?

有时候,咱们得保证当增长水的时,原来的容器也能够装入新的的水而不至于溢出,也就是ArrayList的自动扩容机制。咱们能够想象,假如列表大小为10,那么正常状况下只能装10个元素,咱们很好奇在此以后调用add()方法时底层作了什么神奇的事,因此咱们看看add()方法是怎么实现的:

// 增长一个元素
public boolean add(E e) {
    // 确保内部容量大小,size指的是当前列表的实际元素个数
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;
    return true;
}
复制代码

从上面方法能够看出先判断内部容量是否足够知足size + 1个元素,若是能够,就直接elementData[size++] = e;,不然就须要扩容,那么怎么扩容呢?咱们到ensureCapacityInternal()方法看看,这里有一点很重要,请记住下面的参数:

  • minCapacity永远表明增长以后实际的总元素个数
  • newCapacity永远表示列表可以知足存储minCapacity个元素列表所须要扩容的大小
// 校验内部容量大小
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 这个方法只有首次调用时会用到,否则默认返回 minCapacity
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 这里若是成立,表示该ArrayList是刚刚初始化,尚未add进任何元素
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
// 扩容判断
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 判断是否须要扩容,elementData.length表示列表的空间总大小,不是列表的实际元素个数,size才是列表的实际元素个数
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
复制代码

上面会判断集合是否刚刚初始化,即尚未调用过add()方法,若是成立,则将集合默认扩容至10,DEFAULT_CAPACITY的值为10,取最大值。最后一个方法的grow()成立的条件是容器的元素大于10且没有可用空间,即须要扩容了,咱们再看看grow()方法:

private void grow(int minCapacity) {
    // 获取旧的列表大小
    int oldCapacity = elementData.length;
    // 扩容以后的新的容器大小,默认增长一半 ..............................1
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 若是扩容一半以后还不足,则新的容器大小等于minCapacity.............................2
    if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
    // 若是新的容器大小比MAX_ARRAY_SIZE还大,
    if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);
    // 数组拷贝操做
    elementData = Arrays.copyOf(elementData, newCapacity);
}
// 最大不能超过Integer.MAX_VALUE
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
    	throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
复制代码

上面1>>表示右移,也就是至关于除以2,减为一半,2处可能调用addAll()方法时成立。

下面咱们列举几种状况:

ID 状况描述 调用add()? 调用addAll(size)? + size大小 执行结果
1 列表刚初始化 初始化一个长度为10的列表,即容器扩容至10个单位
2 列表实际元素个数为10,实际大小也为10,此时调用add操做 容器扩容至15,容器元素个数为11,即有4个位置空闲
3 列表实际元素个数为10,列表长度也为10,此时调用addAll操做 是 + 5 容器扩容至15,没有空余
4 列表实际元素个数为5,列表长度为10,此时调用addAll()操做 是 + 10 容器扩容至15,没有空余

总结:

扩容机制以下:

  • 一、先默认将列表大小newCapacity增长原来一半,即若是原来是10,则新的大小为15;
  • 二、若是新的大小newCapacity依旧不能知足add进来的元素总个数minCapacity,则将列表大小改成和minCapacity同样大;即若是扩大一半后newCapacity为15,但add进来的总元素个数minCapacity为20,则15明显不能存储20个元素,那么此时就将newCapacity大小扩大到20,刚恰好存储20个元素;
  • 三、若是扩容后的列表大小大于2147483639,也就是说大于Integer.MAX_VALUE - 8,此时就要作额外处理了,由于实际总元素大小有可能比Integer.MAX_VALUE还要大,当实际总元素大小minCapacity的值大于Integer.MAX_VALUE,即大于2147483647时,此时minCapacity的值将变为负数,由于int是有符号的,当超过最大值时就变为负数

小编认为,上面第3点也体现了一种智慧,即当同样东西有可能出错时,咱们应该提早对其作处理,而不要等到错误发生时再对其进行处理。也就是咱们运维要作监控的目的。

三、调用ArrayListiterator()返回的迭代器是怎样的?

咱们都知道全部集合都是Collection接口的实现类,又由于Collection继承了Iterable接口,所以全部集合都是可迭代的。咱们经常会采用集合的迭代器来遍历集合元素,就像下面的代码:

ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
// 获取集合的迭代器对象
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String item = iter.next();
    System.err.println(item);
}
复制代码

咱们能够经过调用集合的iterator()方法获取集合的迭代器对象,那么在ArrayList中,iterator()方法是怎么实现的呢?

public Iterator<E> iterator() {
    return new Itr();
}
复制代码

超级简单,原来是新建了一个叫Itr的对象那么这个Itr又是什么呢?打开源码咱们发现Itr类实际上是ArrayList的一个内部类,定义以下:

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;......................... 1
    Itr() {}
    public boolean hasNext() {...}// 具体实现被我删除了
    public E next() {...}
    public void remove() {...}
    public void forEachRemaining(Consumer<? super E> consumer) {...}
    final void checkForComodification() {...}
}
复制代码

该迭代器实现了Iterator接口并实现了相关方法,提供咱们对集合的遍历能力。总结:ArrayList的迭代器默认是其内部类实现,实现一个自定义迭代器只须要实现Iterator接口并实现相关方法便可。而实现Iterable接口表示该实现类具备像for-each loop迭代遍历的能力。

四、采用ArrayList的迭代器遍历集合时,对集合执行相关修改操做时为何会抛出ConcurrentModificationException,咱们该如何避免?

上面第3小节咱们查看了ArrayList迭代器的源代码,咱们都知道,若是在迭代的过程当中调用非迭代器内部的remove或者clear方法将会抛出ConcurrentModificationException异常,那究竟是为何呢?咱们一块儿来看看。首先这里设计两个很重要的变量,一个是expectedModCount,另外一个是modCount,expectedModCount在集合内部迭代器中定义,就像上面第三小节源码1处所示,modCountAbstractList中定义。就像第三小节1处所看到的,默认二者是相等的,即expectedModCount = modCount,只有当其不想等的状况下就会抛出异常。真的是不想等就抛异常吗?咱们来看看迭代器内部的next方法:

public E next() {
    // 在迭代前会对两个变量进行检查
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}
// 具体检查
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
复制代码

能够看出确实是当它们二者之间不想等时就报错,问题来了,那么何时会致使它们不想等呢?不急,咱们来看看ArrayListremove方法:

public E remove(int index) {
    rangeCheck(index);
    // 这里会修改modCount的值
    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;
}
复制代码

能够看出当调用remove()方法时确实是修改了modCount的值,致使报错。那咱们怎么作才能不报错有想在迭代过程当中增长或者删除数据呢?答案是使用迭代器内部的remove()方法。

总结:

迭代器迭代集合时不能对被迭代集合进行修改,缘由是modCountexpectedModCount两个变量值不想等致使的!

五、当集合扩容或者克隆时免不了对集合进行拷贝操做,那么ArrayList的数组拷贝是怎么实现的?

ArrayList中对集合的拷贝是经过调用ArrayscopyOf方法实现的,具体以下:

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());.................2
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    // 在建立新数组对象以前会先对传入的数据类型进行断定
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}
复制代码

最后还调用了Systemarraycopy方法。

六、ArrayList中的序列化机制

第一小节咱们知道ArrayList存储数据的定义方式为:

transient Object[] elementData;
复制代码

咱们会以为很是奇怪,这是一个集合存储元素的核心,却声明为transient,是否是就说就不序列化了?这不科学呀!其实集合存储的数据仍是会序列化的,具体咱们看看ArrayList中的writeObject方法:

writeObject

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);
    
    // 这个地方作一个序列化操做
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
复制代码

从上面的代码中咱们能够看出ArrayList实际上是有对elementData进行序列化的,只不过这样作的缘由是由于elementData中可能会有不少的null元素,为了避免把null元素也序列化出去,因此自定义了writeObjectreadObject方法。

谢谢阅读,欢迎评论,共同探讨~

相关文章
相关标签/搜索