Java集合ArrayList实现原理——源码分析

来源:互联网 发布:网络推广任务 编辑:程序博客网 时间:2024/05/21 06:14

一,ArrayList简述


     ArrayList是实现了List接口的动态数组,动态数组是指它的大小是可变的。ArrayList实现了所有可选列表操作,并允许保存包括null在内的所有元素。ArrayList除了实现List接口,还提供了操作是内部用来存储列表的数组的大小的方法。
     每个ArrayList实例都有一个容量(capacity),该容量是用来表示存储元素的数组的大小。随着元素的增加,ArrayList的容量也会随之扩容,但这并不是说每次向ArrayList中增加一个元素,ArrayList的容量就会扩容,而是先会判断元素数量是否达到了最小容量,如果达到了才会去扩大容量(capacity)。扩容会带来数组拷贝的操作,所以当你知道具体的业务数据量的时候,指定一个初始的容量大小是很有必要的。对了,ArrayList默认的容量大小是10,所以在构造ArrayList对象时,可以指定一个初始化容量,这样可以减少扩容时数据的拷贝问题。当在添加大量元素前,可以使用ensureCapacity()方法来增加 ArrayList 实例的容量,这可以减少递增式再分配的数量。

 注意,ArrayList 实现不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表(从结构上改变列表是指增加元素、删除元素、改变了ArrayList容量。如果只是改变 了某个元素的值不属于该操作的范畴。),那么它必须保持外部同步。所以为了保证同步,最好的办法是在创建时完成,以防止意外对列表进行不同步的访问:
List list = Collections.synchronizedList(new ArrayList(…));

二,ArrayList源码解析


     想必,大家多ArrayList的使用都很熟悉了,它的一些基本操作我们就不讲了,我们主要看下它底层的实现。ArrayList是List接口的实现,它底层是采用数组来实现的,所以对它的操作也是基于对数组的操作。下面我们就通过源码来说明ArrayList的底层实现,PS:本文源码是基于OpenJDK 1.7。

2.1、ArrayList属性:

        在ArrayList中定义了四个属性,分别如下
          private static final int DEFAULT_CAPACITY = 10;//默认容量
          private static final Object[] EMPTY_ELEMENTDATA = {};//空ArrayList实例对应的数组
private transient Object[] elementData;//这就ArrayList真正存储元素的数组,ArrayList的容量就是该数组的长度。
         private int size;//ArrayList的大小,也是ArrayList中存储元素的数量

        这里有个需要注意的地方:elementData使用了transient关键字。通过源码你会发现ArrayList实现了Serializable接口,我们知道实现了Serializable接口的对象可以通过序列化操作实现持久化,但是使用了transient关键字的属性在序列化操作时是不会被持久化,同时在反序列化时,此属性也不会被恢复。也就是说在对ArrayList实例在反序列化操作后得到是实例中ArrayList中的elementData是为null。

2.2、构造方法以及主要的方法

     2.2.1、构造方法

  • ArrayList():此构造方法是没有指定初始容量的,这个时候ArrayList的容量就是默认大小——10
public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}
  • ArrayList(int initialCapacity):此构造方法指定了初始容量大小。
public ArrayList(int initialCapacity) {
   
super();
  if (initialCapacity < 0)
        
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    
this.elementData = new Object[initialCapacity];
}

  • ArrayList(Collection<? extends E> c) :此构造方法是构造一个包含指定集合元素的ArrayList实例,这些元素是按照该集合的迭代器返回的顺序排列的。
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    
 if (elementData.getClass() != Object[].class)
       elementData = Arrays.copyOf(elementDatasizeObject[].class);
}

     2.2.2、增加

     ArrayList提供了四个向其中增加元素的方法,分别是:add(E e)、add(int index,E element)、add(Collection<? extends E> c)、add(int index,Collection<? extends E> c)。我们一个个分析:
  • add(E e),此方法用于在列表的尾部添加一个元素,源码如下:
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
      此方法会先判断要不要扩容,然后再将元素添加到数组中。我们看下判断是否要扩容的方法:

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
//此方法会先列表是不是空的,如果是空,则比较当前minCapacity和默认容量(DEFAULT_CAPACITY)的大小,并将两者的中大的赋值给minCapacity。然后会调用ensureExplicitCapacity()来确定列表确切的容量。以下是ensureExplicitCapacity()的源码:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
//此方法首先会改变modCount变量的值,该变量定义在ArrayList的父类——AbstractList中,用于标记列表被修改的次数。

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

//此方法就是扩容数组的方法,但是在扩容数组前肯定是有个逻辑判读的。首先会定义个变量newCapacity表示新的容量大小,它的初始值为数组elementData长度的1.5倍。
//然后将newCapacity和最小容量、最大数组长度的大小进行比较,得到newCapacity的最终的值。得到最终的容量大小后就会去进行数组拷贝动作,Array.copyOf()方法最终
//是会调用System.arraycopy(),System.arraycopy()是个本地方法,执行效率会比较高。Max_ARRAY_SIZE和hugeCapacity()的定义如下
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
   
if (minCapacity < 0) // overflow
      throw new OutOfMemoryError();
   
return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;
}

  • add(int index,E element) 此方法其实和add(E e)方法类似,只是多了index是否正确的判断和对原数组的移位处理,移位处理就是为了空出指定的index,然后添加元素,具体代码如下:
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++;
}

  • add(Collection<? extends E> c)、add(int index,Collection<? extends E> c)。第一个表示在列表尾部开始添加集合c中的元素,第二个方法表示在指定的index添加集合c中的元素到列表。这两个方法原理其实和前面的两个add方法都是一样的,他们实际上都是对底层的数组进行操作,这里就不过多的去讲解了,感兴趣的同学可以去看下他们的源码,在这我也不贴出来了。对了,一般我看源码的地方是grepcode,这个网站很多Java源码,可以多看看。

     2.2.3、删除

          ArrayList提供了remove(Object obj)、remove(int index)、removeAll(Collection c)、removeRange(int fromIndex,int toIndex)、retainAll(Collection c )、clear()。
          remove(Object obj)方法是移除列表中首次出现的指定的元素,如果元素存在于列表中的话。remove(int index),移除指定位置的元素。removeAll(Collection c),将集合c中包含的元素从列表中移除。removeRange(int fromIndex, int toIndex),将列表中索引在fromIndex(包括该索引)和toIndex(不包括该索引)之间的元素从列表中移除。retainAll(Collection c),只保留集合c中含有的元素,也就是说将列表中除集合c中包含的元素以外的元素全从移除。clear(),将列表中的元素全部清除。我们分析其中的几个:
  • remove(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;
}
        从代码中可以看出,remove(int index)方法中,首先会判断是否越界,然后改变modCount的值,这个值的含义我们在上面也提过;然后计算需要移动多少位(numMoved的值),再执行数组拷贝的操作;最后改变列表元素数量size值,并置空数组elementData的最后一个元素。
  • remove(Object obj),用于删除列表中首次出现的指定元素obj,如果列表中存在该元素的话。
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;
}
//此方法首先会判断要删除的元素是不是为null。如果为null的话,找到数组中第一个为null的元素,然后快速删除,并返回true。如果不为null,也是找到数组中第一个和指定对象o相等的元素,然后快速删除,并返回true。如果两种情况下都没有找到指定的的元素o ,那么最终返回的就为false,表示没有找到要删除的元素。下面看下快速删除——fastRemove(int index)方法,其实这个方法的思想和remove(int index)的实现是一样的,最终都是通过数组的移位来实现的。

/*
 * Private remove method that skips bounds checking and does not
 * return the value removed.
 */
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
}
  • removeAll(Collection c)、removeRange(int fromIndex,int toIndex)、retainAll(Collection c )、clear()这几个方法的具体实现就不多说了,原理都差不多,感兴趣的同学可以去看下源码。

     2.2.4、查找

          ArrayList中查找是通过get(int index)来获得指定位置的元素的,我们知道ArrayList的底层实现是数组,所以get(int index)的方法的实现就很简单了。先会判断给定的索引是否越界,然后返回数组elementData指定索引下的元素。
public E get(int index) {
    rangeCheck
(index);
  return elementData(index);
}


三,总结
     ArrayList的底层实现是数组,对ArrayList的添加、删除、修改、查找实际上都是对ArrayList中的数组进行相应的操作。ArrayList的容量(大小)可以实现动态改变的原因是,底层会去动态改变实现ArrayList 的那个数组的大小。改变ArrayList容量的操作也叫扩容,扩容的规则是不一定的,OpenJdk中默认是原有元素的1.5倍,但是这个不是标准,如有具体的需要完全是可以自己重新定义这个扩容的倍数的。最后建议看ArrayList的源码的时候可以在本子上画一画常用方法的实现图,主要是数组拷贝那一块,这样理解起来比较直观方便。 
0 0
原创粉丝点击