zoukankan      html  css  js  c++  java
  • ArrayList源码分析

    ArrayList的声明

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

    泛型声明,继承于AbstractList,实现了若干个接口。

    AbstractList是List的虚基类不多说,List接口是Colloection的子接口。

    RandomAccess是List所实现的标记接口,用来表明其支持快速(通常是固定时间)随机访问。

    随机访问我的理解就是通过索引(index)进行访问。

    Cloneable也是标记接口,表示可以合法调用clone()方法而不抛出异常,clone()也会正常执行,复制所有自断。

    Serializable表示可以被序列化和反序列化,在io中用来传递数据有用。

    ArrayList的域

    复制代码
     1     private static final long serialVersionUID = 8683452581122892189L;
     2   
     3     private static final int DEFAULT_CAPACITY = 10;
     4     
     5     private static final Object[] EMPTY_ELEMENTDATA = {};
     6 
     7     private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
     8 
     9     transient Object[] elementData; // non-private to simplify nested class access
    10 
    11     private int size;
    复制代码

    serialVersionUID是和Serializable接口配套使用的,用来确保序列化和反序列化的正常运行。

    DEFAULT_CAPACITY默认初始化容量。容量是指尚未扩充前,最大存储数据的多少。

    EMPTY_ELEMENTDATA 和DEFAULTCAPACITY_EMPTY_ELEMENTDATA 基本都是用作初始化时赋予的空的数组,区别只是在于添加首个元素的时候进行区分,参照下面。

    elementData数组用作存储数据。

    size表示已经占用了多少数据。

    发现elementData是用transient修饰的,意思是不参与序列化过程,为什么要这样设计呢?

    序列化过程中其实是调用了private void writeObject(java.io.ObjectOutputStream s)方法,

    因为ArrayList中的elementData其实并不是全部都存放数据的,仅仅存放了size个数据,那如果全部用作序列化和反序列化会导致效率变低,

    于是就只把实际存在的数据进行序列化就能使效率变高。

    ArrayList的构造方法

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

    无参的构造方法,将elementData设为空的数组,没有其他附加操作。

    复制代码
     1 public ArrayList(int initialCapacity) {
     2         if (initialCapacity > 0) {
     3             this.elementData = new Object[initialCapacity];
     4         } else if (initialCapacity == 0) {
     5             this.elementData = EMPTY_ELEMENTDATA;
     6         } else {
     7             throw new IllegalArgumentException("Illegal Capacity: "+
     8                                                initialCapacity);
     9         }
    10     }
    复制代码

    带有一个int型参数的构造方法,设一个默认的初始化参数,构建一个以该参数长度为大小的空数组,

    当设的参数不能作为数组的初始化参数时会抛出IllegalArgumentException。

    复制代码
     1 public ArrayList(Collection<? extends E> c) {
     2         elementData = c.toArray();
     3         if ((size = elementData.length) != 0) {
     4             // c.toArray might (incorrectly) not return Object[] (see 6260652)
     5             if (elementData.getClass() != Object[].class)
     6                 elementData = Arrays.copyOf(elementData, size, Object[].class);
     7         } else {
     8             // replace with empty array.
     9             this.elementData = EMPTY_ELEMENTDATA;
    10         }
    11     }
    复制代码

    带一个Collection型参数的构造方法,将Collection内的元素用c.toArray()转换为数组直接传给elementData。

    当发现传入长度不为0的时候还要检测其类型是否正确转为Object[],因为JDK编号6260652的BUG,若没有正确转换,还需要调用Arrays.copyOf(elementData, size, Object[].class);来进行转换。

    当传入的是一个空的集合的时候将elementData设为默认空数组。

    ArrayList的关键方法

    复制代码
     1 public boolean add(E e) {
     2         ensureCapacityInternal(size + 1);  // Increments modCount!!
     3         elementData[size++] = e;
     4         return true;
     5     }
     6 
     7 public void add(int index, E element) {
     8         rangeCheckForAdd(index);
     9 
    10         ensureCapacityInternal(size + 1);  // Increments modCount!!
    11         System.arraycopy(elementData, index, elementData, index + 1,
    12                          size - index);
    13         elementData[index] = element;
    14         size++;
    15     }
    16 
    17 private void ensureCapacityInternal(int minCapacity) {
    18         if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    19             minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    20         }
    21 
    22         ensureExplicitCapacity(minCapacity);
    23     }
    24 
    25 private void ensureExplicitCapacity(int minCapacity) {
    26         modCount++;
    27 
    28         // overflow-conscious code
    29         if (minCapacity - elementData.length > 0)
    30             grow(minCapacity);
    31     }
    32 
    33 private void grow(int minCapacity) {
    34         // overflow-conscious code
    35         int oldCapacity = elementData.length;
    36         int newCapacity = oldCapacity + (oldCapacity >> 1);
    37         if (newCapacity - minCapacity < 0)
    38             newCapacity = minCapacity;
    39         if (newCapacity - MAX_ARRAY_SIZE > 0)
    40             newCapacity = hugeCapacity(minCapacity);
    41         // minCapacity is usually close to size, so this is a win:
    42         elementData = Arrays.copyOf(elementData, newCapacity);
    43     }
    44 
    45 private void rangeCheckForAdd(int index) {
    46         if (index > size || index < 0)
    47             throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    48     }
    复制代码

    添加元素的方法感觉十分容易,但是ArrayList的本质是数组,

    添加一个元素有可能意味着数组越界,越界的解决方法就是将原来的数组扩容,这里层层调用了3个private方法。

    首先调用ensureCapacityInternal(int minCapacity),这个方法判断了该数组是否是用了无参的构造方法,如果是,就取默认的容量和现在要求的容量的较大值最为下个函数的参数。

    ensureExplicitCapacity(minCapacity)判断要求的容量和当前容量的大小,即判断是否需要扩容,不需要扩容则直接添加,否则就调用扩容的核心方法。

    grow(minCapacity)就是扩容的核心方法,

    int newCapacity = oldCapacity + (oldCapacity >> 1);将新的容量(数组大小)设定成为旧的容量的1.5倍。

    然后比较新的容量和请求的容量的大小,如果仍小于请求容量的大小,就把新容量改为请求容量的大小。

    如果新的容量已经超过了ArrayList设置的最大容量大小private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    就把新的容量设置为最大的容量,最后调用Arrays.copyOf(elementData, newCapacity)把原来的数据复制到新扩容的数组中,数组长度为新的容量。

    最后才是把指定的元素添加到所想要添加的位置上。

    简单的public boolean add(E e)就仅仅是在最后一个元素后面添加,没什么好说的。

    public void add(int index, E element)的添加若是在已有数据的中间添加,则需要将后面的元素逐个后移。

    JDK采取的方法是调用System.arraycopy(elementData, index, elementData, index + 1,size - index);

    这是个本地(native)方法,方法的用处是将index后size-index个数据复制到index+1后。

    复制代码
     1 public boolean addAll(Collection<? extends E> c) {
     2         Object[] a = c.toArray();
     3         int numNew = a.length;
     4         ensureCapacityInternal(size + numNew);  // Increments modCount
     5         System.arraycopy(a, 0, elementData, size, numNew);
     6         size += numNew;
     7         return numNew != 0;
     8     }
     9 
    10 public boolean addAll(int index, Collection<? extends E> c) {
    11         rangeCheckForAdd(index);
    12 
    13         Object[] a = c.toArray();
    14         int numNew = a.length;
    15         ensureCapacityInternal(size + numNew);  // Increments modCount
    16 
    17         int numMoved = size - index;
    18         if (numMoved > 0)
    19             System.arraycopy(elementData, index, elementData, index + numNew,
    20                              numMoved);
    21 
    22         System.arraycopy(a, 0, elementData, index, numNew);
    23         size += numNew;
    24         return numNew != 0;
    25     }
    复制代码

    增加多个元素其实和增加单个元素的思路是相似的,都是先确保容量大小,

    然后都是调用了System.arraycopy的方法进行添加和后移。

    复制代码
     1 public E remove(int index) {
     2         rangeCheck(index);
     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     }
    15 
    16 public boolean remove(Object o) {
    17         if (o == null) {
    18             for (int index = 0; index < size; index++)
    19                 if (elementData[index] == null) {
    20                     fastRemove(index);
    21                     return true;
    22                 }
    23         } else {
    24             for (int index = 0; index < size; index++)
    25                 if (o.equals(elementData[index])) {
    26                     fastRemove(index);
    27                     return true;
    28                 }
    29         }
    30         return false;
    31     }
    32 
    33 private void rangeCheck(int index) {
    34         if (index >= size)
    35             throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    36     }
    37 
    38 private void fastRemove(int index) {
    39         modCount++;
    40         int numMoved = size - index - 1;
    41         if (numMoved > 0)
    42             System.arraycopy(elementData, index+1, elementData, index,
    43                              numMoved);
    44         elementData[--size] = null; // clear to let GC do its work
    45     }
    复制代码

    删除单个元素ArrayList提供了两个方法,一个是删除指定索引的方法,另一个则是删除ArrayList中第一次出现的特定对象(如果存在)。

    删除指定索引的方法记录了一个numMoved,即需要移动的元素,和add一样是调用了System.arraycopy(elementData, index+1, elementData, index,numMoved);来将元素逐个向前移动。

    最后把size减一的同时置为null留给GC来处理。

    删除指定对象的方法都先是遍历整个ArrayList,找到对象的索引,然后调用一个fastRemove(int index)方法,

    该方法和删除指定索引的方法基本相同(除了记录返回值和参数检测之外),就不再赘述了。

    复制代码
    1 public void clear() {
    2         modCount++;
    3 
    4         // clear to let GC do its work
    5         for (int i = 0; i < size; i++)
    6             elementData[i] = null;
    7 
    8         size = 0;
    9     }
    复制代码

    删除所有元素和删除单个元素的思路是一样的,就是置为null,让GC去处理。

    注意到删除元素并没有对数组的容量进行改变。

    复制代码
     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     }
    复制代码

    另外发现一个有趣的代码,删除从fromIndex到toIndex的所有元素,看上去是一个挺有用的功能,

    但是令人感到奇怪的是他的修饰符居然是protected,意味着程序员无法在程序中直接调用,

    其实是因为removeRange(int fromIndex, int toIndex)和sublist(int fromIndex,int toIndex).clear()方法的效果是相同的,

    所以并不需要额外增加一个可以被调用的方法,那为什么还要设计这个方法呢?

    首先要知道,这个方法是从AbstractList中继承过来的,

    而在AbstractList中对此方法的说明是

    此方法由此列表及其 subList 上的 clear 操作调用。重写此方法以利用内部列表实现可以极大地 改进此列表及其 subList 上 clear 操作的性能。 

    1 public void clear() {
    2         removeRange(0, size());
    3     }

    但是在ArrayList中的实现并没有像AbstractList中这样实现,反而是自己另外实现了,那这个方法是不是就没有用了呢?

    并不是如此,ArrayList还在自己的内部添加了一个内部类SubList。

    private class SubList extends AbstractList<E> implements RandomAccess

    这个类只有在其外部ArrayList调用subList方法后才会生成一个特定的实例

    1 public List<E> subList(int fromIndex, int toIndex) {
    2         subListRangeCheck(fromIndex, toIndex, size);
    3         return new SubList(this, 0, fromIndex, toIndex);
    4     }

    对应的构造方法

    复制代码
    1 SubList(AbstractList<E> parent,
    2                 int offset, int fromIndex, int toIndex) {
    3             this.parent = parent;
    4             this.parentOffset = fromIndex;
    5             this.offset = offset + fromIndex;
    6             this.size = toIndex - fromIndex;
    7             this.modCount = ArrayList.this.modCount;
    8         }
    复制代码

    而他的removeRange方法

    复制代码
    1 protected void removeRange(int fromIndex, int toIndex) {
    2             checkForComodification();
    3             parent.removeRange(parentOffset + fromIndex,
    4                                parentOffset + toIndex);
    5             this.modCount = parent.modCount;
    6             this.size -= toIndex - fromIndex;
    7         }
    复制代码

    现在一切揭晓了,修改ArrayList的removeRange方法同时也是在修改其内部类SubList的removeRange方法,

    而内部类SubList并没有覆盖父类的clear方法,即和AbstractList的clear方法相同,也就和JDK的说明完全符合。

    复制代码
    1 public void trimToSize() {
    2         modCount++;
    3         if (size < elementData.length) {
    4             elementData = (size == 0)
    5               ? EMPTY_ELEMENTDATA
    6               : Arrays.copyOf(elementData, size);
    7         }
    8     }
    复制代码

    这个方法用作将数组的容量变为数组的元素数量,可以使ArrayList所占内存空间达到最小。

    具体实现是调用了Arrays.copy方法将数据复制到一个长度为size的数组中。

     ArrayList的迭代器

    介绍迭代器前首先要介绍一个从AbstractList中继承的实例域modCount,这个modCount在前面也经常出现。

    JDK的解释是

    从结构上修改 此列表的次数。从结构上修改是指更改列表的大小,或者打乱列表,从而使正在进行的迭代产生错误的结果。 

    这是一个并发操作的问题,例如我在遍历的时候删除一个元素,是否返回这个元素是未知的,即返回的结果可能出错也可能没错。

    在之前的很多算法中我们也发现了这个modCount的出现,在后面的方法中,modCount经常会进行检测,发现错误会抛出ConcurrentModificationException。

    但这个异常的抛出是尽力而为的,因为是并发操作,这个就被称为“快速失败”。

    下面对出现modCount的代码就不再做解释了。

    Iterator的声明

    private class Itr implements Iterator<E>

    继承自Iterator,不必多说。

    Iterator的域

    1 int cursor;       // index of next element to return
    2 int lastRet = -1; // index of last element returned; -1 if no such
    3 int expectedModCount = modCount;

    cursor表示光标,用来指示下一个元素的索引。

    lastRet表示上一个返回元素的索引,初始为-1表示没有。

    expectedModCount用作和modCount进行对比,初始和modCount相等。

    Iterator的关键方法

    复制代码
     1 public boolean hasNext() {
     2             return cursor != size;
     3         }
     4 
     5 public E next() {
     6             checkForComodification();
     7             int i = cursor;
     8             if (i >= size)
     9                 throw new NoSuchElementException();
    10             Object[] elementData = ArrayList.this.elementData;
    11             if (i >= elementData.length)
    12                 throw new ConcurrentModificationException();
    13             cursor = i + 1;
    14             return (E) elementData[lastRet = i];
    15         }
    16 
    17 public void remove() {
    18             if (lastRet < 0)
    19                 throw new IllegalStateException();
    20             checkForComodification();
    21 
    22             try {
    23                 ArrayList.this.remove(lastRet);
    24                 cursor = lastRet;
    25                 lastRet = -1;
    26                 expectedModCount = modCount;
    27             } catch (IndexOutOfBoundsException ex) {
    28                 throw new ConcurrentModificationException();
    29             }
    30         }
    复制代码

    hasNext直接范围其和size的比较值,若和size相等即表示到达最后,没有下一个元素。

    next先判断是否有下个元素,如果没有就抛出NoSuchElementException,

    用i保存cursor,增加cursor然后直接返回ArrayList.this.elementData[i](当前对象的elementData),并将i的值赋给lastRet。

    remove方法先判断是否有上个返回的元素,没有则抛出IllegalStateException,

    调用当前对象的remove方法删除上个元素,并把光标前移,将lastRet设为-1。

    复制代码
     1 public void forEachRemaining(Consumer<? super E> consumer) {
     2             Objects.requireNonNull(consumer);
     3             final int size = ArrayList.this.size;
     4             int i = cursor;
     5             if (i >= size) {
     6                 return;
     7             }
     8             final Object[] elementData = ArrayList.this.elementData;
     9             if (i >= elementData.length) {
    10                 throw new ConcurrentModificationException();
    11             }
    12             while (i != size && modCount == expectedModCount) {
    13                 consumer.accept((E) elementData[i++]);
    14             }
    15             // update once at end of iteration to reduce heap write traffic
    16             cursor = i;
    17             lastRet = i - 1;
    18             checkForComodification();
    19 }
    复制代码

    这是JAVA8添加了的一个缺省方法,只是遍历所有元素然后用consumer.accept接受该元素。

    另外还有相应的listiteror的实现,也仅是增加了hasPrevious和previous以及set和add方法,

    前两个和hasNext及next实现大同小异,后两种也仅是调用了ArrayList对应的方法,在此就不作赘述。

  • 相关阅读:
    蛙蛙推荐:五分钟搞定网站前端性能优化
    蛙蛙推荐:AngularJS学习笔记
    蛙蛙推荐:如何实时监控MySql状态
    这6种思维,学会了你就打败了95%文案!zz
    10分钟,解决卖点没创意的难题zz
    总感觉自己工作沟通想问题时没有逻辑,这可怎么办?| 极简逻辑指南
    「零秒思考」是个神话,不过这款笔记术你值得拥有zz
    关于提高沟通能力的书单 | 章鱼书单zz
    日常沟通的 3 种模式zz
    关于提高沟通能力的书单zz
  • 原文地址:https://www.cnblogs.com/XuYiHe/p/6860452.html
Copyright © 2011-2022 走看看