zoukankan      html  css  js  c++  java
  • 源码阅读(4):Java中主要的List结构——ArrayList集合(下)

    ============
    (接上文《源码阅读(3):Java中主要的List结构——ArrayList集合(上)》)

    4.3、ArrayList中的add(E) 方法和add(int, E) 方法

    ArrayList容器的add(E)方法和Vector容器的add(E)方法类似,其原理都可以概括为:当容器中还有多余容量时,则直接在当前元素集合的尾部添加新元素即可;如果容器没有多余的容量,则首先进行“扩容”后再进行新元素的添加:

    // ArrayList容器的add(E)方法
    /**
     * Appends the specified element to the end of this list.
     * @param e element to be appended to this list
     */
    public boolean add(E e) {
      ensureCapacityInternal(size + 1);  // Increments modCount!!
      elementData[size++] = e;
      return true;
    }
    

    两种容器的add(E)方法只有一些代码细节的不同:

    • 两个add(E)方法都要进行modCount变量的自增操作,不同的是ArrayList容器的add(E)方法中modCount变量的自增在引用的ensureExplicitCapacity(int)方法中能进行;Vector容器的add(E)方法直接在方法体中进行modCount变量的自增;

    • 两个add(E)方法都是在当前集合的尾部进行元素的添加操作,只是两种容器定义标识“尾部”的变量不一样。ArrayList容器使用size变量标识“尾部”;Vector容器使用elementCount变量标识“尾部”。一定注意:这里所谓的“尾部”并不是elementData数组变量的length属性标识的,而是容器中size变量或者elementCount变量所标识的。

    基于以上的讲解,读者基本上可以看出:实际上ArrayLis容器的tadd(E)方法已经没有再进行详细分析的必要了,倒是add(int, E)方法可以做一些详细的操作分析。

    4.4、add(int index, E element)

    /**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
      // 检测指定的index索引位置是否符合要求
      rangeCheckForAdd(index);
      // 确认容量并且在必要时候增加容量,这个方法已经详细介绍过,这里就不再赘述
      // 如果读者需要理解,可参见《源码阅读(3):Java中主要的List结构——ArrayList集合(上)》
      ensureCapacityInternal(size + 1);  // Increments modCount!!
      // 从当前指定的index索引位置开始,将后续位置上的所有元素向后移动一位
      System.arraycopy(elementData, index, elementData, index + 1, size - index);
      // 将当前数组的索引位置重新设置为新值
      elementData[index] = element;
      // ArrayList容器当前已使用的容量+1
      size++;
    }
    

    实际上add(int, E)方法和add(E)方法的差异在“arraycopy”那句代码的位置才显现出来,我们使用下图表示有差异部分的操作过程:

    在这里插入图片描述
    上图所示的过程是调用add(int, E)方法时,容器中容量充足的情况。如果此时容量不充足,则ensureCapacityInternal(int)方法会首先默认按照50%的基数进行容量扩展。接着arraycopy方法将从elementData数组的指定位置开始,将后续的元素依次向后移动一位,最后将新的元素设定在指定的index索引位置。

    4.5、remove(int)方法

    ArrayList容器中的remove(int)方法,代码片段如下:

    /**
     * 该方法删除指定索引位置的元素,并将指定索引位以后的元素向左移动
     * @return 指定索引位上被移除的元素将被返回
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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;
    }
    

    该方法与Vector容器中的removeElementAt(int)方法工作效果类似,其内部的代码都非常相似,只是在细节上有所差异。例如Vector容器的removeElementAt(int)方法中直接将判定索引位是否超界的代码放在方法体内;ArrayList容器的remove(int)方法中将判定索引位是否超界的代码放在rangeCheck(int)方法中;

    两个方法对于移动元素的核心方法都是一样的,既都是使用System类下的arraycopy方法进行,如下所示:

    // ......
    System.arraycopy(elementData, index+1, elementData, index, numMoved);
    // ......
    

    4.6、Vector容器与ArrayList容器比较

    从本篇文章和上篇文章的介绍中,我们基本上都是将Vector容器和ArrayList容器进行比较,这是因为这两个容器的结构、处理逻辑、应用场景都非常相似。本小节我们就来详细比较一下这两个容器在各个方面的差异。

    4.6.1、从结构细节进行比较

    Vector容器和ArrayList容器内部结构都是数组,且数组都是使用名叫elementData的变量表示。那么不同点在于数组的初始化逻辑以及扩容逻辑:

    • Vector容器中数组的初始化大小默认为10,且使用者可以指定Vector容器的初始化大小。ArrayList容器也可指定容器的初始化大小,但多数情况下使用者都不会指定,这种情况下ArrayList容器中的elementData变量将会被初始化为一个大小为0的空数组。

    • Vector容器和ArrayList容器的扩容逻辑不一样,这是因为两个容器对扩容平衡性的理解不一样。Vector容器默认采用当前容量的1倍大小进行扩容。Vector容器也可指定扩容指标,但除非使用者很明确Vector容器即将承载的元素范围,否则不推荐使用,这是因为一个固定的扩容指标要么导致频繁扩容,要么导致比必要扩容带来的性能损失。

    • ArrayList容器在扩容时将当前容量新增50%,且扩容逻辑不能干预。除非扩容前容量小于10——如果发生这样的的情况,则首先扩容到10。ArrayList容器的扩容逻辑相对动态,这保证了在扩容频度和扩容大小之间更好的平衡性。

    4.6.2、从序列化过程上比较

    • Vector容器并没有对容器的序列化过程和反序列化过程进行特殊处理,那么在序列化过程中会出现的情况就是当前elementData中多余未使用的元素位置也跟着被一起序列化,这很明显产生了不必要的性能消耗。

    • 所以ArrayList容器做了针对性的优化,既重写了“writeObject(ObjectOutputStream)”方法和“readObject(ObjectInputStream)”方法。在序列化时只会对elementData数组中已使用的索引位置进行序列化,未使用的位置将不会被序列化;相对的,在反序列化成新的ArrayList容器对象时,新的elementData数组则不会产生任何多余的容量——直到下一次被要求向集合中添加元素时,才会开始新一轮的扩容操作。

    4.6.3、从线程安全性进行比较

    • Vector容器是线程安全的,这种安全性在体现在容器中所有“读”、“写”性质的方法定义上都加上了“synchronized”关键字。如下所示:
    //......
    public synchronized int size() {
      return elementCount;
    }
    
    public synchronized int lastIndexOf(Object o) {
      return lastIndexOf(o, elementCount-1);
    }
    //......
    

    但实际上这种线程安全的保证方式已经不是高并发场景下所推荐的了(这个话题后续文章会详细说明),可以使用诸如“CopyOnWriteArrayList”这样的容器进行替换。

    • ArrayList容器并不是线程安全的,官方也不推荐在多线程场景下使用ArrayList容器。如果使用者强行这么做,那么ArrayList容器很可能会出现“脏数据”问题(实际上ArrayList容器在诸如迭代器中做了一些避免“脏读”问题的限制)。
  • 相关阅读:
    洛谷 P2766 最长不下降子序列问【dp+最大流】
    洛谷 P3254 圆桌问题【最大流】
    洛谷 P2764 最小路径覆盖问题【匈牙利算法】
    洛谷 P2763 试题库问题【最大流】
    洛谷 P2762 太空飞行计划问题 【最大权闭合子图+最小割】
    洛谷 P2761 软件补丁问题 【spfa】
    洛谷 P2754 星际转移问题【最大流】
    洛谷 P1251 餐巾计划问题【最小费用最大流】
    spoj 371 Boxes【最小费用最大流】
    poj 3680 Intervals【最小费用最大流】
  • 原文地址:https://www.cnblogs.com/liulaolaiu/p/11744211.html
Copyright © 2011-2022 走看看