Java集合源码分析<一>:ArrayList的源码分析

一、底层结构

  ArrayList底层基于动态数组实现,所以在元素个数改变时存在扩容和缩容这样重新规划数组大小的机制。在ArrayList的源码中,维护Object[] elementData数组来存储元素,elementData被transient修饰,不可参与序列化,ArrayList是动态可变的,所以elementData数组长度并不代表ArrayList实际的元素个数,size才是显示实际元素个数,在未指定初始容量的情况下,会使用默认容量DEFAULT_CAPACITY(DEFAULT_CAPACITY = 10),另外,还维护了两个空数组EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

// ArrayList.java 115 行

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; // 元素个数

二、构造方法

ArrayList提供个三个构造方法,分别是无参,指定初始容量和指定包含元素集合

// ArrayList.java 151 行

// 初始化initialCapacity长度的数组
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);
    }
}

// 初始化数组为空数组,在添加元素方法中会默认数组长度为10
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 初始化一个包含c集合所有元素的ArrayList,数组长度为size
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;
    }
}

三、添加元素和自动扩容机制

  ArrayList提供了四个添加方法add(E e),add(int index, E element),addAll(Collection<? extends E> c),addAll(int index, Collection<? extends E> c)用于各种使用需求,但是其方法实现大致相同,分为指定位置插入和尾部插入,以指定位置插入的addAll(int index, Collection<? extends E> c)为例,实现过程如下图:

addAll涉及两次数组的复制,第一次后移插入下标之后的元素,空出位置,第二次复制插入元素到数组中,源码如下:、

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index); // 越界检测

    Object[] a = c.toArray(); 
    int numNew = a.length; // 获取插入元素的个数
    ensureCapacityInternal(size + numNew);  // 确保数组容量

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

添加方法都涉及两个阶段,扩容和数组移动复制,以add(E e)方法为例,首先会计算新数组所需最小容量,也就是(size + 1)的计算,当然,对于addAll,这个也就变成(size + c.toArray().length),之后,所以添加方法都会调用ensureCapacityInternal方法处理数组容量,保证插入的元素可以进入到数组,就是elementData[size++] = e的操作。

// ArrayList.java 461 行

public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

ensureCapacityInternal方法中是ArrayList动态数组实现的核心机制,首先,会使用calculateCapacity计算所需最小容量,这里的设计是为了同时满足add(E e)和addAll(Collection<? extends E> c)的调用,如果第一次添加元素,addAll的参数c元素个数多于默认初始容量DEFAULT_CAPACITY,那么数组初始化长度就是minCapacity,如果是add第一次添加,那么就是默认使用DEFAULT_CAPACITY(这里只限于elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就是未使用指定容量的构造函数)

// ArrayList.java 223 行

// 计算所需最小容量
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) {
    // 集合修改次数 +1
    modCount++;

    // 如果所需最小容量大于数组现有长度,则扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity); // 扩容
}

扩容机制实现

上面图中描述了扩容过程,扩容方法grow(int minCapacity)中,实现了两部分内容,分别是计算新数组的长度和旧数组到新数组的复制,数组长度计算中,newCapacity = oldCapacity + (oldCapacity >> 1),也就是原来的1.5倍长度,之后就是对于新数组长度超出最大长度的处理,ArrayList中限定最大长度为MAX_ARRAY_SIZE,值为Integer.MAX_VALUE - 8, 这里有个为什么减8的问题?源码的解释就是说,有些Java虚拟机的数组会保留一些位置来存储数组信息,如length等,分配最大Integer.MAX_VALUE会导致OOM错误。hugeCapacity方法会在minCapacity超过MAX_ARRAY_SIZE时,尝试分配Integer.MAX_VALUE,当然只会在某些虚拟机中成功,一般都会OOM。

// ArrayList.java 248行

// 最大数组长度 = 2^31 - 9
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
    // 旧数组长度
    int oldCapacity = elementData.length;
    // 新数组长度,为旧数组的1.5倍,向下取整
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 1.5倍的新数组长度仍不能满足所需最小容量,则新数组长度以最小容量为准
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 新数组长度超过最大值MAX_ARRAY_SIZE,则通过hugeCapacity重新计算最大新数组长度
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 数组复制
    elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // int类型位运算中溢出则为负数(符号位是1),抛出OOM
        throw new OutOfMemoryError();
    // 如果所需最小容量minCapacity 大于 MAX_ARRAY_SIZE, 返回最大值Integer.MAX_VALUE = 2^31 - 1,
    // 反之最大 MAX_ARRAY_SIZE
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

数组复制通过Arrays.copyOf()方法实现,该方法有基于System.arraycopy()方法实现,实现了将旧数组的元素复制到新数组中并返回的过程,至此,ArrayList扩容完成。

System.arraycopy()的实现过程如上图:

// Arrays.java 3208行

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    // 创一个与原数组类型相同的数组
    T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength);    
    // 数组复制 参数:原数组、复制开始位置、新数组、新数组插入开始位置、复制的元素个数,也叫偏移量
    // Math.min方法主要防止越界异常
    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));             
    return copy;
}

四、删除元素和手动缩容

ArrayList删除元素的操作和数组相同,除了删除最后一个元素,其他下标处删除都要涉及删除位置以后的数组前移操作,这个操作也是通过System.arraycopy()方法实现,只不过没有产生新的数组。

// ArrayList.java 495行 

public E remove(int index) {
    rangeCheck(index); // 下标越界检测
    // 集合修改次数 +1 
    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    // 实现数组前移
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
                        
    elementData[--size] = null; // 删除元素引用,交由GC处理

    return oldValue;
}

ArrayList并不会自动缩容,因为无法判断缩容的条件,但是提供手动缩容的方法trimToSize(),通过该方法,可以使得当前ArrayList的元素个数size和数组长度length相同。

// ArrayList.java 194行

public void trimToSize() {
    modCount++;
    // 元素个数小于数组长度触发
    if (size < elementData.length) {
        // size == 0 直接置为空数组,反之,生成一个size长度的数组
        elementData = (size == 0) ? EMPTY_ELEMENTDATA  : Arrays.copyOf(elementData, size);
    }
}

五、modCount变量和迭代器

在ArrayList的增删等操作中,会一直有一个modCount++的操作,modCount是代表当前集合的修改次数,是为了配合集合中的迭代器Iterator存在的。Iterator是Java集合提供的一个可以安全遍历集合的方式,继承了Iterable接口的集合都会通过iterator()方法去获取迭代器。

@Test
public void IteratorDemo() {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);

    for (int i = 0; i < list.size(); i++) {
        list.remove(i);
    }
    System.out.println(list.size());
}    

如上测试代码,遍历list并删除元素,但是最后结果却是2,这说明,这样遍历ArrayList,并不能实现需求,但是同时又没有抛出异常,当使用迭代器时如下:

@Test
public void IteratorDemo() {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);

    Iterator<Integer> it = list.iterator();
    while (it.hasNext()) {
        Integer next = it.next();
        it.remove();
    }
    System.out.println(list.size());
}

显然,使用迭代器可以很好的实现,所以来分析一下迭代器的源码。在ArrayList中有两个迭代器类,分别是Itr和ListItr,  ListItr比Itr多出了previous的操作和add方法,也就是说ListItr在遍历过程中可获取到当前上一个元素,也能在遍历过程中完成add元素的操作。

// ArrayList.java 846行

private class Itr implements Iterator<E> {
    int cursor;       // 下一个元素的下标, 从0开始
    int lastRet = -1; // 当前返回元素的下标,没有就是-1
    int expectedModCount = modCount; // 预期修改次数expectedModCount 和 实际修改次数modCount

    Itr() {}
    // 判断是否有下一个元素
    public boolean hasNext() {
        return cursor != size;
    }
    // 返回下一个元素
    @SuppressWarnings("unchecked")
    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; // 游标 + 1
        return (E) elementData[lastRet = i]; // 刷新返回元素的下标
    }
    // 删除元素
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet; //刷新指针的位置
            lastRet = -1; //刷新指针的位置
            expectedModCount = modCount; // 刷新预期修改次数
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    // jdk1.8提供的遍历实现,支持函数式接口 
    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

迭代器的遍历类似于游标指针,通过hashNext方法和next方法,实现游标的移动,当通过迭代器增删元素时,游标的位置也会发生刷新。Itr迭代器中,有三个成员变量cursor、lastRet、expectedModCount,cursor是游标指向的下一个元素下标,默认是0,lastRet是游标当前返回元素的下标,默认-1,expectedModCount是预期修改次数,创建迭代器时赋值modCount,next方法控制游标的移动,所以,遍历过程,只有执行next方法,才能移动。在next方法中,有个checkForComodification()方法,当前expectedModCount  != modCount时,抛出ConcurrentModificationException异常,这意味在遍历过程中ArrayList发生了修改操作(不包括set方法),这种抛出异常的处理机制被称为fail-fast(快速失败)机制,所以集合中的modCount是为了配置迭代器实现快速失败机制而存在的。使用迭代器删除时,会同时刷新expectedModCount ,也就不会再抛出 ConcurrentModificationException。ListItr继承与Itr,相关机制和Itr相同,就不多赘述。

Iterable、Iterator、iterator()的区别?

Iterable接口是包含方法iterator()方法,1.8以后增加了用于for each循环的forEach方法,Collection继承Iterable接口,Iterator接口就是iterator()的返回,是迭代器的接口,迭代器对象都实现了该接口。

 

posted @ 2019-07-10 16:39  哲雪君!  阅读(6206)  评论(0编辑  收藏  举报