【深入理解java集合】-ArrayList实现原理

一、ArrayList简介

1、概述

ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。

ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。

ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。

每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。

注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。

2、关注要点

  • ArrayList使用的存储的数据结构
  • ArrayList的初始化
  • ArrayList是如何动态增长
  • ArrayList如何实现元素的插入和移除

二、ArrayList的源码分析

对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面我们来分析ArrayList的源代码:

1、属性

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 = {};     
//保存ArrayList中数据的数组    
private transient Object[] elementData;     
//ArrayList中实际数据的数量    
private int size;

...

}

从源码中我们可以发现,ArrayList使用的存储的数据结构是Object的对象数组。其实这也不能想象,我们知道ArrayList是支持随机存取的类似于数组,所以自然不可能是链表结构。

有个关键字需要解释:transient。 

Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。

我们都知道ArrayList对象是可序列化的,但这里为什么要用transient关键字修饰它呢?查看源码,我们发现ArrayList实现了自己的readObject和writeObject方法,所以这保证了ArrayList的可序列化。当写入到输出流时,先写入“容量”,再依次写出“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。

2、构造方法

ArrayList提供了三种方式的构造器,可以构造一个默认的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。

//指定容量大小的构造函数
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() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

   //用Collection来初始化ArrayList
    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;
        }
    }

老版本java默认空参初始化时创建一个容量为10的数组,Java1.8版本使用空数组初始化elementData。EMPTY_ELEMENTDATA 实际上就是一个共享的空的Object数组对象。初始化容量大小改为当第一次add的时候,这个数组就会被初始化一个大小为10的数组,个人认为与懒加载原理相同,节省资源消耗。

3、存储元素

ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)这些添加元素的方法。下面我们一一讲解:

// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。  
public E set(int index, E element) {  
   RangeCheck(index);  
 
   E oldValue = (E) elementData[index];  
   elementData[index] = element;  
   return oldValue;  
}    
// 将指定的元素添加到此列表的尾部。  
public boolean add(E e) {  
   ensureCapacity(size + 1);   
   elementData[size++] = e;  
   return true;  
}    
// 将指定的元素插入此列表中的指定位置。  
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。  
public void add(int index, E element) {  
   if (index > size || index < 0)  
       throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  
   // 如果数组长度不足,将进行扩容。  
   ensureCapacity(size+1);  // Increments modCount!!  
   // 将 elementData中从Index位置开始、长度为size-index的元素,  
   // 拷贝到从下标为index+1位置开始的新的elementData数组中。  
   // 即将当前位于该位置的元素以及所有后续元素右移一个位置。  
   System.arraycopy(elementData, index, elementData, index + 1, size - index);  
   elementData[index] = element;  
   size++;  
}    
// 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。  
public boolean addAll(Collection<? extends E> c) {  
   Object[] a = c.toArray();  
   int numNew = a.length;  
   ensureCapacity(size + numNew);  // Increments modCount  
   System.arraycopy(a, 0, elementData, size, numNew);  
   size += numNew;  
   return numNew != 0;  
}    
// 从指定的位置开始,将指定collection中的所有元素插入到此列表中。  
public boolean addAll(int index, Collection<? extends E> c) {  
   if (index > size || index < 0)  
       throw new IndexOutOfBoundsException(  
           "Index: " + index + ", Size: " + size);  
 
   Object[] a = c.toArray();  
   int numNew = a.length;  
   ensureCapacity(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;  
   }

当我们向一个ArrayList中直接添加一个对象或者Collection集合是,是采用末尾添加方式,只有通过下标向ArrayList中某个具体位置(末尾除外)添加数据元素时,才会通过System.arraycopy()方法,向后移动从index到length-1的所有元素,位移长度由数据个数决定。

当我们向一个ArrayList中添加数据的时候,首先会先检查数组中是不是有足够的空间来存储这个新添加的元素,minCapacity=ensureCapacity(size+1),或ensureCapacity(size+newNum),oldCapacity = elementData.length,即minCapacity与oldCapacity大小判断。如果有的话,那就什么都不用做,直接添加。如果空间不够用了,那么就根据原始的容量增加原始容量的一半, int newCapacity = oldCapacity + (oldCapacity >> 1),后面会具体讲解。

4、元素读取

// 返回此列表中指定位置上的元素。  
 public E get(int index) {  
    RangeCheck(index);  
  
    return (E) elementData[index];  
  }

由组数下标实现快速随机读取。

5、元素删除:

5.1 romove(int index)

// 移除此列表中指定位置上的元素。  
 public E remove(int index) {  
    RangeCheck(index);  
  
    modCount++;  
    E oldValue = (E) elementData[index];  
  
    int numMoved = size - index - 1;  
    if (numMoved > 0)  
        System.arraycopy(elementData, index+1, elementData, index, numMoved);  
    elementData[--size] = null; // Let gc do its work  
  
    return oldValue;  
 }

首先是检查范围,修改modCount,保留将要被移除的元素,将移除位置index之后的元素向前挪动一个位置,将list末尾元素置空(null),返回被移除的元素。

5.2 remove(Object o)

public boolean remove(Object o) {  
    // 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。  
    if (o == null) {  
        for (int index = 0; index < size; index++)  
            if (elementData[index] == null) {  
                // 类似remove(int index),移除列表中指定位置上的元素。  
                fastRemove(index);  
                return true;  
            }  
    } else {  
        for (int index = 0; index < size; index++)  
            if (o.equals(elementData[index])) {  
                fastRemove(index);  
                return true;  
            }  
        }  
        return false;  
    } 
}

首先通过代码可以看到,当移除成功后返回true,否则返回false。remove(Object o)中通过遍历element寻找是否存在传入对象,一旦找到就调用fastRemove移除对象。为什么找到了元素就知道了index,不通过remove(index)来移除元素呢?因为fastRemove跳过了判断边界的处理,因为找到元素就相当于确定了index不会超过边界,而且fastRemove并不返回被移除的元素。下面是fastRemove的代码,基本和remove(index)一致。

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; // Let gc do its work  
 }

5.3 removeRange(int fromIndex,int toIndex)

protected void removeRange(int fromIndex, int toIndex) {  
     modCount++;  
     int numMoved = size - toIndex;  
         System.arraycopy(elementData, toIndex, elementData, fromIndex,  
                          numMoved);  
   
     // Let gc do its work  
     int newSize = size - (toIndex-fromIndex);  
     while (size != newSize)  
         elementData[--size] = null;  
}
执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。

执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。

6、容量动态增长

从上面介绍的向ArrayList中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。下面我们一步一步来解析源码:

public boolean add(E e) {
    
    //添加元素之前,先调用ensureCapacityInternal方法,判断是否需要扩容
    ensureCapacityInternal(size + 1); // Increments modCount!! 
    
    //这里看到ArrayList添加元素的实质就相当于为数组末尾赋值
    elementData[size++] = e; 
    return true;
} 

6.1 ensureCapacityInternal的实现如下:

得到最小扩容量,要么是10,要么是大于10的最小容量minCapacity

private void ensureCapacityInternal(int minCapacity) {
     if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 获取默认的容量10和传入参数最小容量的较大值
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
     }

     ensureExplicitCapacity(minCapacity); 
} 

DEFAULT_CAPACITY为:10

当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。

这也就实现了当我们不指定初始化大小的时候,添加第一个元素,数组会扩容为10,或者认为ArrayList最小容量为10。

6.2 ensureExplicitCapacity

判断是否需要扩容


private void ensureExplicitCapacity(int minCapacity) { 
    modCount++; 
    
    // overflow-conscious code 
    if (minCapacity - elementData.length > 0) 
        //调用grow方法进行扩容,调用此方法代表已经开始扩容了
        grow(minCapacity); 
} 

我们来仔细分析一下:

  • 当我们要 add 进第1个元素到 ArrayList 时,elementData.length 为0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法。

  • 当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入 (执行)grow(minCapacity) 方法。

  • 添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。

直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容。

6.3 grow

private void grow(int minCapacity) { 
    // oldCapacity为旧容量,newCapacity为新容量
    int oldCapacity = elementData.length; 
    
    //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
    //位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
    int newCapacity = oldCapacity + (oldCapacity >> 1); 
    
    //检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量
    if (newCapacity - minCapacity < 0) 
        newCapacity = minCapacity; 
    // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
    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); 
} 

我们再来通过例子探究一下grow() 方法 :

  • 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 hugeCapacity 方法。数组容量为10,add方法中 return true,size增为1。
  • 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。
  • 以此类推······

我们可以看到grow方法将数组扩容为原数组的1.5倍,调用的是Arrays.copy方法。

在jdk6及之前的版本中,采用的还不是右移的方法

int newCapacity = (oldCapacity * 3)/2 + 1;

现在已经优化成右移了。

注意源码中通过if (newCapacity - MAX_ARRAY_SIZE > 0)控制集合的最大容量,MAX_ARRAY_SIZE= Integer.MAX_VALUE – 8。

6.4 hugeCapacity()

从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //对minCapacity和MAX_ARRAY_SIZE进行比较
        //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
        //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
        //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

6.5 ensureCapacity方法

ArrayList 源码中有一个 ensureCapacity 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢?

/**
    如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
     *
     * @param   minCapacity   所需的最小容量
     */
    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);
        }
    }

最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量从新分配的次数

我们通过下面的代码实际测试以下这个方法的效果:

public class EnsureCapacityTest {
	public static void main(String[] args) {
		ArrayList<Object> list = new ArrayList<Object>();
		final int N = 10000000;
		long startTime = System.currentTimeMillis();
		for (int i = 0; i < N; i++) {
			list.add(i);
		}
		long endTime = System.currentTimeMillis();
		System.out.println("使用ensureCapacity方法前:"+(endTime - startTime));

		list = new ArrayList<Object>();
		long startTime1 = System.currentTimeMillis();
		list.ensureCapacity(N);
		for (int i = 0; i < N; i++) {
			list.add(i);
		}
		long endTime1 = System.currentTimeMillis();
		System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1));
	}
}

运行结果:

使用ensureCapacity方法前:4637
使用ensureCapacity方法前:241

通过运行结果,我们可以很明显的看出向 ArrayList 添加大量元素之前最好先使用ensureCapacity 方法,以减少容器增量从新分配的次数(1.5倍阶梯增长)。

6.6 其他

ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:

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

由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length和size相同,节省空间。

7、转为静态数组

ArrayList有两个转化为静态数组的toArray方法。

7.1 toArray()

第一个,调用Arrays.copyOf将返回一个数组,数组内容是size个elementData的元素,即拷贝elementData从0至size-1位置的元素到新数组并返回,返回的数组length就等于size实际存储的元素数量。

/**
*返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。
*因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。
*/
public Object[] toArray() {  
      
    return Arrays.copyOf(elementData, size);  

}

7.2 toArray(T[] a)

第二个,如果传入数组的长度小于size,返回一个新的数组,大小为size,类型与传入数组相同。所传入数组长度与size相等,则将elementData复制到传入数组中并返回传入的数组。若传入数组长度大于size,除了复制elementData外,还将把返回数组的第size个元素置为空。

/**
     * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 
     *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 
     *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 
     *如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。
     *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) 
     */
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()函数可能会抛出"java.lang.ClassCastException"异常,但是调用toArray(T[] contents)能正常返回T[]。toArray()会抛出异常是因为toArray()返回的是Object[]数组,将Object[]转换为其它类型(比如将Object[]转换为Integer[])则会抛出"java.lang.ClassCastException"异常,因为java不支持向下转型。解决该问题的办法是调用<T> T[] toArray(T[] contents),而不是Object[] toArray()。

8、ArrayList遍历方式

ArrayList支持三种遍历方式,下面我们逐个讨论:

8.1 Iterator迭代器

Integer value = null;
Iterator it = list.iterator();
while (it.hasNext()) {
    value = (Integer)it.next();
}

8.2 随机访问

通过索引值去遍历。由于ArrayList实现了RandomAccess接口,所以它支持通过索引值去随机访问元素。

Integer value = null;
int size = list.size();
for (int i = 0; i < size; i++) {    
value = (Integer)list.get(i); 
}

8.3 通过for循环遍历

Integer value = null;for (Integer integ : list) { 
   value = integ;
}

通过测试,遍历ArrayList时,使用随机访问(即通过索引号访问)效率最高,而使用迭代器的效率最低。

猜你喜欢

转载自blog.csdn.net/qq_42022528/article/details/82842402