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

一,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的源码的时候可以在本子上画一画常用方法的实现图,主要是数组拷贝那一块,这样理解起来比较直观方便。 

猜你喜欢

转载自blog.csdn.net/sd_zhuzhipeng/article/details/51262460