Java集合类详解(1) -- 从JDK1.8源码看ArrayList

什么是ArrayList

ArrayList的底层是基于一个数组实现的,实际上ArrayList就是一个动态数组。

我们先直接上定义

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList的一些特性

  • ArrayList并不是线性安全的,在多线程环境下不能使用。
  • 每个ArrayList实例都有一个容量属性,该容量属性是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。
  • ArrayList实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList
    是支持快速访问、复制、序列化的。

ArrayList的属性

private static final int DEFAULT_CAPACITY = 10;

表示集合的默认大小

//空的数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//这也是一个空的数组实例,和EMPTY_ELEMENTDATA空数组相比是用于了解添加元素时数组膨胀多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

两个空数组实例

//对象数组
transient Object[] elementData;
//表示集合的长度
private int size;

在这里有必要讲解一下transient,其作用为,当对象的属性加上transient关键字后,该对象被序列化时,此属性不会被保存。

我们需要注意:

  • 当 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时将会清空 ArrayList
  • 当添加第一个元素时,elementData 长度会扩展为 DEFAULT_CAPACITY=10

ArrayList的构造函数

无参构造函数

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

此无参构造函数将创建一个 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 声明的数组,其初始容量为0,而不是10

注意,执行以下语句时,集合的长度为0

ArrayList list = new ArrayList();

有参构造函数

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

当传入参数大于0,则创建指定长度的数组,等于0则将elementData 指向空数组,小于0直接抛出异常。

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
}

其实相当于复制集合c到ArrayList中

ArrayList的一些方法

增加操作

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  //添加元素之前,首先要确定集合的大小
        elementData[size++] = e;
        return true;
}

如上所示,给数组增加元素之前,我们需要调用ensureCapacityInternal方法来确定集合的大小,若集合已满,我们需要进行扩容操作。

//这里的minCapacity 是集合当前大小+1
private void ensureCapacityInternal(int minCapacity) {
        //elementData 是实际用来存储元素的数组,注意数组的大小和集合的大小不是相等的,前面的size是指集合大小
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

我们看到这个方法内部又调用了两个方法

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        if (minCapacity - elementData.length > 0)
        	//扩容
            grow(minCapacity);
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        	//如果数组为空,则从size+1的值和默认值10中取最大的
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;//不为空,则返回size+1
    }

ensureExplicitCapacity调用的grow方法用于扩容,源代码如下

private void grow(int minCapacity) {
        int oldCapacity = elementData.length;//得到原始数组的长度
        int newCapacity = oldCapacity + (oldCapacity >> 1);//新数组的长度等于原数组长度的1.5倍
        if (newCapacity - minCapacity < 0)//当新数组长度仍然比minCapacity小,则为保证最小长度,新数组等于minCapacity
            newCapacity = minCapacity;
        //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 = 2147483639
        if (newCapacity - MAX_ARRAY_SIZE > 0)//当得到的新数组长度比 MAX_ARRAY_SIZE 大时,调用 hugeCapacity 处理大数组
            newCapacity = hugeCapacity(minCapacity);
        //调用 Arrays.copyOf 将原数组拷贝到一个大小为newCapacity的新数组(注意是拷贝引用)
        elementData = Arrays.copyOf(elementData, newCapacity);
}
    
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // 
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ? //minCapacity > MAX_ARRAY_SIZE,则新数组大小为Integer.MAX_VALUE
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

从中我们易得

  1. 当通过 ArrayList() 构造一个空集合,初始长度是为0的,第 1
    次添加元素,会创建一个长度为10的数组,并将该元素赋值到数组的第一个位置。
  2. 第 2 次添加元素,集合不为空,而且由于集合的长度size+1是小于数组的长度10,所以直接添加元素到数组的第二个位置,不用扩容。
  3. 第 11 次添加元素,此时 size+1 = 11,而数组长度是10,这时候创建一个长度为10+10*0.5 = 15的数组(扩容1.5倍),然后将原数组元素引用拷贝到新数组。并将第 11 次添加的元素赋值到新数组下标为10的位置。
  4. 第 Integer.MAX_VALUE - 8 = 2147483639,然后
    2147483639%1.5=1431655759(这个数是要进行扩容) 次添加元素,为了防止溢出,此时会直接创建一个
    1431655759+1 大小的数组,这样一直,每次添加一个元素,都只扩大一个范围。
  5. 第 Integer.MAX_VALUE - 7 次添加元素时,创建一个大小为 Integer.MAX_VALUE
    的数组,在进行元素添加。
  6. 第 Integer.MAX_VALUE + 1 次添加元素时,抛出 OutOfMemoryError 异常。

我们看到上面方法调用时用到了一个属性modCount

protected transient int modCount = 0;

该变量是定义在 AbstractList 中的。记录对 List 操作的次数。主要使用是在 Iterator时,防止在迭代的过程中集合被修改。

删除元素

删除指定索引的元素

public E remove(int index) {
        rangeCheck(index);//判断给定索引的范围,超过集合大小则抛出异常

        modCount++;
        E oldValue = elementData(index);//得到索引处的删除元素

        int numMoved = size - index - 1;
        if (numMoved > 0)//size-index-1 > 0 表示 0<= index < (size-1),即索引不是最后一个元素
            //通过 System.arraycopy()将数组elementData 的下标index+1之后长度为 numMoved的元素拷贝到从index开始的位置
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; //将数组最后一个元素置为 null,便于垃圾回收

        return oldValue;
}

删除首次出现的元素

public boolean remove(Object o) {
        if (o == null) {//如果删除的元素为null
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {//不为null,通过equals方法判断对象是否相等
            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;
}

修改元素

把索引index处的元素替代为element

public E set(int index, E element) {
        rangeCheck(index);//判断索引合法性

        E oldValue = elementData(index);//获得原数组指定索引的元素
        elementData[index] = element;//将指定所引处的元素替换为 element
        return oldValue;//返回原数组索引元素
}

检查索引合法性的函数

private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

查找元素

根据索引查找元素

public E get(int index) {
         rangeCheck(index);
 
         return elementData(index);
}

根据元素查找索引

public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
}

回收多余的内存,令集合的数组大小刚好调整为集合元素的大小。

public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = Arrays.copyOf(elementData, size);
        }
}

在ArrayList的实现中大量使用了Arrays.copyof()和System.arraycopy()方法,下面我们对其进行一定的讲解。

Arrays.copyof()

public static <T> T[] copyOf(T[] original, int newLength) {  
    return (T[]) copyOf(original, newLength, original.getClass());  
}

明显调用了另外一个方法,其中最后一个参数指明要转换的数据的类型。下面我们分析其源码

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);  
    System.arraycopy(original, 0, copy, 0,  
                     Math.min(original.length, newLength));  
    return copy;  
}

该方法实际上是在其内部又创建了一个长度为newlength的数组,调用System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。

System.arraycopy()

该方法被标记了native,调用了系统的C/C++代码,在JDK中是看不到的,但在openJDK中可以看到其源码。该函数实际上最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。

ArrayList和Vector的区别之处

  • ArrayList在内存不够时默认是扩展50% + 1,Vector是默认扩展1倍
  • Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销
  • Vector提供indexOf(obj, start)接口,ArrayList没有

对ArrayList的总结

  • 在确定ArrayList的元素数量之后使用ArrayList较好,否则建议使用LinkedList,因为若不确定元素数量可能会不断发生数组扩容,此操作非常耗时。
  • ArrayList基于数组实现,可以通过下标索引直接查找元素,但进行增加或删除操作时需要大量移动元素,故查找效率高,增加或删除效率低。
  • ArrayList中允许元素为null,故在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理。

参考:Java集合—ArrayList的实现原理
JDK1.8源码(五)——java.util.ArrayList 类

你可能感兴趣的:(Java集合类)