zoukankan      html  css  js  c++  java
  • 集合之ArrayList的源码分析

    转载请注明出处:http://www.cnblogs.com/qm-article/p/8833831.html

    一、介绍

       对于ArrayList,可以说诸位绝不陌生,可以说是在诸多集合中运用的最多一个类之一,那么它是怎样构成,怎样实现的呢,相信很多人都知道数组构成的,没毛病,如果遇到面试的时候,估计还会问,它的默认大小是多少?它是怎样扩容的?它的一个属性modCount有啥作用?线程安全性怎么样?。。。。等待一系列问题。下面就围绕着对该集合的运用来展开,如增删改查,这四个方面。在介绍之前,先来看看该集合的性质

    1、里面的元素是有序的(指的是添加的顺序和排列的顺序是一致的)

    2、可以添加重复元素

    3、增删慢,查询快

    4、内部采用了Object数组来存储元素,size为元素个数

    5、有线程安全问题

    二、增加

    先来贴下增加元素的源码

    1     public boolean add(E e) {
    2     
    3         ensureCapacityInternal(size + 1);  // Increments modCount!!
    4         elementData[size++] = e;
    5         return true;
    6     }

    添加某个元素,在使用默认构造函数的时候,且是第一次添加元素,第三行代码会触发初始化操作,即对数组扩容到默认大小10,点进去看,如下

     1 private void ensureCapacityInternal(int minCapacity) {
     2         //若使用的默认构造函数,且是第一次添加,则返回true,此时开始初始化话
     3         if (elementData == EMPTY_ELEMENTDATA) {
     4             minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//10
     5         }
     6      
     7         ensureExplicitCapacity(minCapacity);
     8     }
     9 
    10 
    11 private void ensureExplicitCapacity(int minCapacity) {
    12         modCount++;
    13 
    14         // overflow-conscious code
    15         if (minCapacity - elementData.length > 0)
    16             grow(minCapacity);
    17     }
    18 
    19 
    20 private void grow(int minCapacity) {
    21         // overflow-conscious code
    22         int oldCapacity = elementData.length;
    23         int newCapacity = oldCapacity + (oldCapacity >> 1);
    24         if (newCapacity - minCapacity < 0)
    25             newCapacity = minCapacity;
    26         if (newCapacity - MAX_ARRAY_SIZE > 0)
    27             newCapacity = hugeCapacity(minCapacity);
    28         // minCapacity is usually close to size, so this is a win:
    29         elementData = Arrays.copyOf(elementData, newCapacity);
    30     }

     如上可以看出扩容步骤:

    1、先判断有没有初始化,没有则初始化,有则跳过这步骤

    2、检查要扩容的大小和数组的长度哪个大,如果数组长度大,则还没到需要扩容的时候即跳过,进行添加元素同时size+1,反之,则进行下一步grow方法(这里说明下关于size和数组的大小,这两个是不同的概念,这两者关系必满足size<=length,size指的是数组里的元素个数,length指的是数组长度)

    3、grow(),该方法是ArrayList扩容的关键方法,在它里面有这几个逻辑判断(注意此时意味着minCapacity超过了数组的长度length)

                      1、获取数组长度,为扩容大小提供值

         2、默认扩大为原数组长度的1.5倍(这里面有精度运算,不一定真的恰好是1.5倍),你扩容后的长度newCapacity它的值为原数组长度+该长度向左移1个长度

                           (即除以2),如,原来是10,则扩容后为10+10>>1=15。

           3、扩容后长度再去和传进来的参数minCapacity去比较大小,取最大值,将最大值赋值为扩容后的长度。

                      4、扩容后的长度再去和MAX_ARRAY_SIZE做上一步操作,该值为Integer.MAX_VALUE - 8,可以知道ArrayList里数组最大的长度就是

                           Integer.MAX_VALUE,这步在hugeCapacity里

           5、利用工具类Arrays去扩容(实际是先new了一个数组,再利用System类的元素复制将元素移动到新数组里,再返回,至于为啥不用for循环,因为这个类的

                           效率比for循环高,是一个本地方法)

    以上就是扩容步骤,

    扩容完之后,就开始添加元素了elementData[size++] = e;同时size增一。

    对于ArrayList方法其它add重载方法,就不一一介绍了,扩容步骤都是一样的,

    三、移除

     同样,先贴下代码

     1 public E remove(int index) {
     2         rangeCheck(index);//当index>=size会抛异常
     3 
     4         modCount++;
     5         E oldValue = elementData(index);
     6 
     7         int numMoved = size - index - 1;
     8         if (numMoved > 0)
     9             System.arraycopy(elementData, index+1, elementData, index,
    10                              numMoved);
    11         elementData[--size] = null; // clear to let GC do its work
    12 
    13         return oldValue;
    14     }

    先去检查移除的位置的正确性,或许会有人问,怎么不去检查index是否大于0?其实继续往下看就知道了,在第五行代码,若为负数的话,则会直接抛异常ArrayIndexOutOfBoundsException。

    若index在正常范围内,则去判断要不要移动元素,即index后面的所有元素都往前移动一步,最后将size-1这个位置的元素设为null,来帮助垃圾回收,返回旧值。。

    对于它的重载函数,remove(Object o),代码如下,要注意的是,它移除的是从数组下标为0开始往后找的第一个元素,再利用fastRemove来进行移除(和上一个remove(int)比较就是少了index判断),最后返回true,移除成功,fasle移除失败

     1 public boolean remove(Object o) {
     2         if (o == null) {
     3             for (int index = 0; index < size; index++)
     4                 if (elementData[index] == null) {
     5                     fastRemove(index);
     6                     return true;
     7                 }
     8         } else {
     9             for (int index = 0; index < size; index++)
    10                 if (o.equals(elementData[index])) {
    11                     fastRemove(index);
    12                     return true;
    13                 }
    14         }
    15         return false;
    16     }

    该集合还提供大规模的移除,不过用的不多(提供给子类和同包类用的)。贴下代码

     1 protected void removeRange(int fromIndex, int toIndex) {
     2         modCount++;
     3         int numMoved = size - toIndex;
     4         System.arraycopy(elementData, toIndex, elementData, fromIndex,
     5                          numMoved);
     6 
     7         // clear to let GC do its work
     8         int newSize = size - (toIndex-fromIndex);
     9         for (int i = newSize; i < size; i++) {
    10             elementData[i] = null;
    11         }
    12         size = newSize;
    13     }

    具体原理和之前移除一个元素一样,不过多叙述

    四、查找

     前面的介绍说该集合查找快,让我们,它比增删快在哪?

    1 public E get(int index) {
    2         rangeCheck(index);
    3 
    4         return elementData(index);
    5     }

    代码非常精简,检查了下index的正确性,然后直接返回,因为它是一个数组,可以直接定位下标。相比增删,少了移动元素,少了新创建数组。必然快,而对于LinkedList这个集合,却和ArrayList相反,关于LinkedList这个集合,在后面的博文中会出现,到时候会做一个比较。

    五、修改

    1 public E set(int index, E element) {
    2         rangeCheck(index);
    3 
    4         E oldValue = elementData(index);
    5         elementData[index] = element;
    6         return oldValue;
    7     }

     同样先检查index的正确性,然后直接改变原index位置的值,返回旧值。

    六、modCount的作用

     细心的人会发现,每一次的增删都有modCount这个变量的出现,并且每次都是+1,至于为什么进行“改查”modCount却不改变,这是因为,该集合是一个数组,如果进行增加或删除,则size必然改变,那么在遍历过程中,肯定找不到正确的结果或者直接数组越界异常,这都是我们不想要的,改查,只是改变元素值,而不是元素个数。在了解这个变量之前,先来了解下,快速失败(fail-fast)和安全失败(fail-safe)的两个概念.

        fail-fast 快速失败,产生于遍历集合过程中,如某个线程对集合集合遍历时,有另外线程对该集合做修改操作,则会抛出ConcurrentModificationException异常,其作用是用来检测错误,并不一定发生。

     fail-safe 安全失败,在遍历集合,并不是直接在原集合中操作,而是先复制一个集合,在复制的集合中操作,这样就不会产生ConcurrentModificationException异常。缺点是改变后不能看到改变后的结果。

    其中安全失败里提到的复制集合,并不是直接将原来的集合赋给新集合,如list_1=list_2,这样只是把内存中的引用地址给了list_1,如果list_1也进行增删操作,那么list_2也能得到相应的结果,即两个集合操作都是对同一个堆内存的地址里的数据进行操作,前面说的复制是另开辟一个堆内存。(切记)。

    在了解这两个概念后,应该可以猜的出来,modCount的作用就是用来fail-fast的,具体体现在如下

    查看ArrayList源码可知,它有一个内部类Itr(实现了Iterator),这个类的作用就是使ArrayList集合可以迭代操作,该类有这几个方法,如下图

     

     1 private class Itr implements Iterator<E> {
     2         int cursor;       // index of next element to return
     3         int lastRet = -1; // index of last element returned; -1 if no such
     4         int expectedModCount = modCount;
     5 
     6         public boolean hasNext() {
     7             return cursor != size;
     8         }
     9 
    10         @SuppressWarnings("unchecked")
    11         public E next() {
    12             checkForComodification();
    13             int i = cursor;
    14             if (i >= size)
    15                 throw new NoSuchElementException();
    16             Object[] elementData = ArrayList.this.elementData;
    17             if (i >= elementData.length)
    18                 throw new ConcurrentModificationException();
    19             cursor = i + 1;
    20             return (E) elementData[lastRet = i];
    21         }
    22 
    23         public void remove() {
    24             if (lastRet < 0)
    25                 throw new IllegalStateException();
    26             checkForComodification();
    27 
    28             try {
    29                 ArrayList.this.remove(lastRet);
    30                 cursor = lastRet;
    31                 lastRet = -1;
    32                 expectedModCount = modCount;
    33             } catch (IndexOutOfBoundsException ex) {
    34                 throw new ConcurrentModificationException();
    35             }
    36         }
    37 
    38         final void checkForComodification() {
    39             if (modCount != expectedModCount)
    40                 throw new ConcurrentModificationException();
    41         }
    42     }

    在建立该类的实例,首先会初始化几个成员变量,cursor和、astRet、expectedModCount,值依次为0,-1,modCount。

    hasNext(),是用来判断集合有没有下一个元素,该代码是直接判断当前指针cursor和元素个数size是否相等。

    next(),先会去检查modCount的值和expectedModCount值是否相等,如果不等,则直接抛出异常,看到这,或许大家就明白了modCount的作用了吧,modCount值要改变必然要对集合进行增或删操作。之后就取当前指针cursor位置的值。

    remove(),该方法也是首先会去判断集合有没有被改变,之后进行相应操作。

    七、最后

     对于ArrayList的线程安全问题,可以采用工具类Collections,该类有一个方法synchronizedList(List),可保证线程安全,但效率可能有点低,

    还有一个方法就是采用CopyOnWriteArrayList类来操作数据。

    ------------------------------------------------------------------------------------------------------------------------------------华丽的分界线---------------------------------------------------------------------------------------------------------------------------------------

    以上就是个人对ArrayList集合的见解,若有不足或错误之处,还望指正,

  • 相关阅读:
    SDUT 2143 图结构练习——最短路径 SPFA模板,方便以后用。。 Anti
    SDUT ACM 1002 Biorhythms 中国剩余定理 Anti
    nyist OJ 119 士兵杀敌(三) RMQ问题 Anti
    SDUT ACM 2157 Greatest Number Anti
    SDUT ACM 2622 最短路径 二维SPFA启蒙题。。 Anti
    二叉索引树 区间信息的维护与查询 Anti
    SDUT ACM 2600 子节点计数 Anti
    UVA 1428 Ping pong 二叉索引树标准用法 Anti
    2010圣诞Google首页效果
    Object
  • 原文地址:https://www.cnblogs.com/qm-article/p/8833831.html
Copyright © 2011-2022 走看看