简明数据结构源码阅读(一)-- ArrayList

推荐阅读时间 13min+

目录

  1. ArrayList 关键词
  2. 源码阅读
  3. 问题解答和总结

前言

本文基于Java8的源代码进行了源代码的代码阅读分析,关于Java8中的新增加的Stream API特性在数据结构中的时候会在之后之后专门使用一篇文章的内容进行介绍,这里只介绍源代码和代码逻辑分析。

ArrayList关键词

阅读ArrayList的相关文档,很容易从中提取出如下的关键词:

  1. backing array
  2. capacity / incremental reallocation
  3. structural modification / fail-fast

综合这三个关键词,我们不难了解到ArrayList的特性和问题。backing array 指出实现了ArrayList的幕后机制的是一个幕后数组(backing array,Object数组),其中它的容量(capacity)是可以增量调整的(incremental reallocation),并且ArrayList并不像它的前辈Vector是一种线程安全的容器,如果出现了结构性变化(structural modification,比如 add remove,set不是 ,会通过modCount标记这个结构性变化)会使用一种机制,这种机制不会让存在这缺陷的过程继续下去,而是立刻停止系统工作,这种机制也被称为fail-fastfail-fast是一种尽最大努力(best-effort)的机制,不可以基于它抛出的异常做错误控制。

从三个关键词中衍生出三个问题

  1. 如果是一个Object数组,ArrayList如何实现类型检查和相关的问题?
  2. fail-fast机制如何实现的,具体如何体现?
  3. ArrayList如何实现扩容的?

关于这三个问题,希望读者能在我的分析中思考,最后我也会给出答案。

源码分析

ArrayList的继承关系图


其中ArrayListSpliterator和Strem API有关,这里后续会详细分析。

ArrayList中的关键常量

这些常量都是见名知意的,{}表示空数组字面量

名称 类型 初始值 意义
DEFAULT_CAPACITY Object[] 10 表示下面的DEFAULT空数组的大小
DEFAULTCAPACITY_EMPTY_ELEMENTDATA Object[] {}
elementData Object[] {}
EMPTY_ELEMENTDATA Object[] {} 空数组大小为0,和DEFAULT数组相区别
size int 0 List大小
modCount int 0 fail-fast标记
MAX_ARRAY_SIZE int Integer.MAX_VALUE-8 最大大小

构造函数

ArrayList的中的三个构造函数

  1. 空构造函数
 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

直接初始化了elementDataDEFAULT数组,大小默认为10

  1. 带有初始大小参数的构造函数
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
  1. 将现有的Collection的内容初始化ArrayList
 public ArrayList(Collection c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652) ****
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

其中Collection中的元素的在ArrayList中的顺序和Collection citerator的遍历顺序相同,具体的实现在toArray函数中,这里采取ArrayListLinkedListtoArray作为参考:

  1. ArrayList
 public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
  1. LinkedList
 public Object[] toArray() {
        Object[] result = new Object[size];
        int i = 0;
        for (Node x = first; x != null; x = x.next)
            result[i++] = x.item;
        return result;
    }

ArrayList中迭代器迭代的顺序是按照数组的顺序,LinkedList中的顺序是按照Node连接的顺序,没毛病。
这里看一下 打****的问题,这是一个官方bug,为什么toArray()不一定能返回Object[]? 事实上这是一个类型转换的问题,这篇文章说的非常清晰,轻容许我简要说明一下他的观点:

 public class MyList extends ArrayList {

        // toArray() 的同名方法
        public String[] toArray() {
            return new String[]{"1", "2", "3"};
        }

    }

因为方法的重载不看返回值的,如果子类定义了这个方法,当调用toArray()的时候,就回返回String[],这样就会出现错误。

add相关操作

add有很多相关方法,不一一列举代码了,具体实现大同小异。add操作的代码调用情况图如下:


这些方法都依赖ensureCapacity相关方法,这里要着重分析一下。

ensure相关方法

ensure相关方法都和扩容操作有关,minCapacity就是如果elementData容量不够大,就会最小扩容到这个大小,并且留意modCount的变化

 private void ensureCapacityInternal(int minCapacity) {
        //封装方法 详细调用,直接看后面
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

  private static int calculateCapacity(Object[] elementData, int minCapacity) {
      //如果backing array是Default数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
          //体现了Default数组和其Default大小的对应关系
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

 private void ensureExplicitCapacity(int minCapacity) {
       //这是一个结构性变化,
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

 private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
    //首先尝试扩容到原来的大小的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //下面的的两个if语句指向了扩容情况的两个极端:
        //不够minCapacity
        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);
    }

    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(E e)方法干了什么,看一个方法,其余的同名方法的做法大同小异:

public boolean add(E e) {
        //minCapacity = size + 1 然后在进行是否扩容的试探
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

综上所述,add操作都是结构性变化的操作,通过ensure相关函数进行扩容和modCount的自增(fail-fast防止多线程操作)。再扩容期间,每次都会扩容1.5倍,所以在感觉可能数据很多的情况下,使用默认的无参数构造函数的所产生的10个空间是不够的。

remove相关操作


remove操作也是一个结构性变化的操作,我们主要看看几个修改modCount的操作:
fastRemove

 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;
    }
//fastRemove 之所以fast是因为不需要在这个方法中做边界判断,边界判断在上面的for循环已经完成了
 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
    }

batchRemove()

 private boolean batchRemove(Collection c, boolean complement) {
        final Object[] elementData = this.elementData;
        //使用了一种类似于双指针的操作
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
            //如果需要保留c中的元素,complement取true
                if (c.contains(elementData[r]) == complement)
                  //利用双指针进行复制覆盖原来的位置
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            //因为ArrayList是可以容纳null元素的,所以contains不会抛出异常,但是有些容器不能容纳null的时候,会从上面的if语句进入finally块
          //直接默认r后面的是需要保留的
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
            //set不是结构性变化操作,删除才是,所以是size - w
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

介绍完了modCount的两个主要修改的地方,下面请看modCount对于fail-fast的作用的体现。

checkForComodification()

ArrayList有两个迭代器ItrListItrListItr在上面的图中可以看出是继承了Itr的。这篇优秀的文章
中给我们展示了CME(ConcurrentModificationException)的两种出现的的情况,不难看出,CME常常出现在使用迭代器迭代的情况下,或者Java的语法糖foreach中,对List进行结构性修改。通过阅读Itr的源码可以对CME的出现原因了解的非常清楚。


Itr中的重要常量:

名称 类型 初始值 意义
cursor int 0 游标
lastRet int -1
expectedModCount int modCount 在初始化Itr时会复制modCount,用于确保不会有其他线程对其进行修改

Itrnextremove方法中都需要调用checkForComodification方法:

     public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

在上面的文章中列举的单线程情况中,因为ArrayListremove方法虽然修改modCount但是,和Itr中的expectedModCount不符,而导致异常。说穿了,这实际上是因为外部类的modCount和内部类的expectedModCount不相同导致的问题,这只能说是一个缺陷。在多线程的环境下,使用modCount才是比较好的控制策略。

SubList

SubListArrayList的一个内部类,其中的方法大体上和ArrayList一致,可以看做是对ArrayList的一个“视图”,但是这个视图是可以影响其原本映射的List的。

  public E set(int index, E e) {
            rangeCheck(index);
            checkForComodification();
            E oldValue = ArrayList.this.elementData(offset + index);
            ArrayList.this.elementData[offset + index] = e;
            return oldValue;
        }

        public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }

问题解答和总结

虽然这篇文章并没有对于整体的所有代码进行逐行解释,但是大体上能够让读者对于ArrayList源码有一个直观地认识。
下面来回答文章开头提的几个问题:

  1. fail-fast是通过modCountItr还有ListItrSubList等中的checkForComodification操作实现的。但是在foreach和while中使用迭代器模式进行遍历时,禁止使用ArrayListremoveadd操作。这样的机制在最大程度上避免了多线程修改。
  2. 扩容机制是通过ensure函数实现的,如果大小不够会通过扩充1.5倍看看。
  3. 类型检查。首先使用了静态类型检查,泛型做了一定的限定。其次,针对toArray()这里天然的语法缺陷,也做了个逻辑层面的判断,规避类型不一产生的问题。

你可能感兴趣的:(简明数据结构源码阅读(一)-- ArrayList)