Java基础之刨根问底第7集——ArrayList

原文转自我自己的个人公众号:Java基础之刨根问底第7集——ArrayList(由于是拷贝过来的,如果排版有问题,请看公众号文章)

  • 本系列不适合初学者,读者应具备一定的Java基础。

  • 本系列依据Java11编写

内容简介:

  • 1.ArrayList的继承关系

  • 2.ArrayList的内部实现机制

  • 3.核心方法实现解析

    • 3.1.构造函数

    • 3.2.add

    • 3.3.remove

    • 3.4.set和get

  • 4.迭代器实现解析

    • 4.1.iterator

    • 4.2.listIterator

  • 5.subList

  • 6.下集预告

1.ArrayList的继承关系

在上一节中,我介绍了Java Collections Framework的接口继承关系

Java基础之刨根问底第6集——集合与List

本集聚焦在ArrayList上,它的继承关系如下图所示:

左侧的三个接口与ArrayList的能力关联不大,并且他们都没有任何方法和属性,仅用于标识:

  • RandomAccess:说明实现它的类支持随机访问。

  • Cloneable:在执行Object的clone方法时,JVM会验证对象是否实现了这个接口,如无实现则抛出异常。

  • Serializable:表示可被序列化和反序列化。

除了左侧的三个接口外,其余都是跟ArrayList的能力有直接关系的接口和抽象类,但这里有一个问题

AbstractList实现了List接口,为什么ArrayList还要实现List接口?

按理来说,AbstractList已经实现了List接口,ArrayList作为它的子类,隐含着就已经实现了List接口,为什么还要多此一举的声明自己实现了List接口呢?

经过我的多方求证,答案似乎是:为了在文档中能清晰的表达实现类与Collections Framework中接口的直接关系。

从继承关系上来看:

  • AbstractCollection:实现Collection接口的抽象类,其能力主要涉及:

    • 新增:add、addAll

    • 删除:remove、removeAll

    • 是否包含:contains、containsAll

    • 清空:clear

    • 获得集合元素数量:size

    • 转数组:toArray

    • 取交集:retainAll

    • 迭代器:iterator

  • AbstractList:实现List接口的抽象类,其在AbstractCollection的基础上,扩展了以下能力:

    • 与下标相关的:get、indexOf、lastIndexOf、add、remove

    • 更新:set

    • 子集合:subList

    • List迭代器:listIterator(相比Iterator的单向访问,ListIterator可双向访问,且提供新增和修改能力)

  • ArrayList:基于内部数组实现了AbstractCollection和AbstractList中的抽象方法,并更具数组的特性提供了更多的重载的方法。

2.ArrayList的内部实现机制

正如名字一样,ArrayList的内部实现主要是基于一个内部数组。在ArrayList内部共有3个数组:

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;

其中,EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA都是静态的空数组,这两个的区别是,前者被所有空ArrayList共享,后者则专用于ArrayList的无参构造函数,表示需要使用默认的容量初始化数组。

elementData则是真正用来存储集合元素的数组,这里有两个点需要注意

  • elementData并没有声明为private,这是因为ArrayList内有几个内部类也需要访问elementData,而内部类在访问private修饰的属性时,JVM实际上是需要创建一个“synthetic”的访问方法才行,为了避免额外的消耗,elementData并没有声明成private。

  • elementData使用了transient关键字,该关键字表示在序列化的时候,忽略该属性。但这并不意味这ArrayList不能序列化,只是在序列化的时候,ArrayList直接将元素进行序列化,在反序列化的时候会用元素重新填充数组。这样做避免了对数组进行序列化时的额外开销。

从上面的源码中可以看到capacity和size两个跟大小相关的属性,它们与elementData的关系如下图所示:

ArrayList的各项能力都是在elementData这个数组上进行的,因为数组的定长和空间连续等特性,因此ArrayList的大部分操作都需要对数组元素进行拷贝。接下来我们就针对几个核心方法来看看具体的实现。

3.核心方法实现解析

3.1.构造函数

ArrayList共有3个构造函数,定义如下:

public ArrayList();
public ArrayList(int initialCapacity);
public ArrayList(Collection<? extends E> c);
  • 在无参构造函数中,仅仅将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData,表示需要使用默认的容量。在上面的介绍中可以看到,默认容量是10。

  • 带有initialCapacity参数的构造函数可以手动设置初始容量,如果initalCapacity设置为0,则会将EMPTY_ELEMENTDATA赋值给elementData,当前的ArrayList的容量也就是0。

  • 传入集合的构造函数,首先会调用传入集合的toArray方法获得元素数组,将获得的数组长度赋值给size属性,如果获得的数组长度是0,则会使用EMPTY_ELEMENTDATA。如果传入的集合同样是ArrayList,则直接将获得的数组赋值给elementData,否则使用Arrays.copyOf将获得的数组拷贝一份赋值给elementData,如下所示:

elementData = Arrays.copyOf(a, size, Object[].class);

使用Arrays.copyOf是为了避免其他类型的集合的toArray返回的并不是Object数组,例如:

@Override
public Object[] toArray() {
    String[] test = {};
    return test;
}

从构造函数中可以看到,我们日常使用最多的无参构造函数并没有实质性地初始化数组,仅仅是用DEFAULTCAPACITY_EMPTY_ELEMENTDATA占了一个位置。默认容量数组的初始化延迟到了第一次添加元素的时候。

3.2.add

源码如下:

public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}
​
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

这里又有两个点要注意:

  • modCount是一个变更计数器,所有会更改元素数量的方法都会增加这个计数器,在开始遍历元素的时候记录下这个值,每遍历一个元素对比下当前值,如果不一样,则说明遍历过程中元素有增减,就会抛出ConcurrentModificationException异常。

  • add方法调用了一个私有的add方法,而这个私有的add方法仅仅在这里被调用了一次,是否多此一举呢?答案肯定不是。这是针对HotSpot VM使用的C1-compiler的一个优化,在C1编译器中,如果方法足够的小,调用方法的时候会直接将方法体inline化,调用就不需要再通过类似“jump”指令去别的地方找方法体了。add方法的调用频率很高,因此做了这样的一个优化。

继续来看私有的add方法,首先对比size(元素数量)和element.length(容量),如果二者相等,说明容量不够了,就需要扩容,默认构造函数初始化后的第一次add就是这样的情况。扩容后将新元素添加到数组元素的最后一个。

再来看以下扩容方法,源码如下:

private Object[] grow() {
    return grow(size + 1);
}
​
private Object[] grow(int minCapacity) {
    return elementData = Arrays.copyOf(elementData,
                                       newCapacity(minCapacity));
}
​
private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity <= 0) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return minCapacity;
    }
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);
}

可以看到,重点在newCapacity方法,它接收一个参数,表示最小的库容容量,add方法扩容的时候,这个参数被设置为size+1,表示至少需要扩容到size+1的容量大小。实际扩容的时候,代码中使用了右移操作符">>",右移1位相当于除以2,因此初步计算的newCapacity值为elementData.length的1.5倍。

接着对比newCapacity和minCapacity,如果前者比后者还小,说明扩容后依然不满足List对容量的最小需求,此时可能有三种情况:一种是无参构造函数后的第一次add,此时因为elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,length是0,按照上面的公式计算后,newCapacity=0,而minCapacity=1,此时就返回默认容量和需求容量中的大值,在构造函数这个场景中,默认容量是10,肯定大于第一次的需求容量1;另一种情况是minCapacity本身小于0 ,这可能是由于int达到最大值后越界导致的符号位改变,此时就抛出OutOfMemoryError异常;如果上述的两种情况都不满足,那么原因就是minCapacity确实设置的比自动扩容的比例大,此时就已需求值minCapacity为准。

另一头,如果newCapacity比minCapacity大,按道理直接返回newCapacity就行了,但源码中还是进行了一些处理。这是因为,在有些JVM中,数组需要预留一些容量,这部分是不能被使用的(通常这部分容量是8),因此这里又对比了排除预留部分后是否还有足够的空间,如果有空间,就返回newCapacity,如果没有空间了,就返回hugeCapacity方法的值,该方法源码如下:

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE)
        ? Integer.MAX_VALUE
        : MAX_ARRAY_SIZE;
}

如果需求的容量还没有大过MAX_ARRAY_SIZE,就返回这个最大的MAX_ARRAY_SIZE,否则说明此时不但打过了MAX_ARRAY_SIZE,而且minCapacity还没有越界变为复数,也就是说当前的JVM并没有对数组预留容量,因此就返回Integer的最大值。

得出新的容量后,就使用Arrays.copyOf方法创建一个新容量的数组,并且把原来数组的元素拷贝到新数组中。这样就完成了add操作。

3.3.remove

remove有两个重载的方法,一个是根据下标删除,另一个是根据对象删除,这两个最终都会调用fastRemove这个私有方法:

public E remove(int index) {
    Objects.checkIndex(index, size);
    final Object[] es = elementData;
​
    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    fastRemove(es, index);
​
    return oldValue;
}
​
public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i);
    return true;
}

第二个根据对象删除的remove方法中有一个不常用的语法found:和之后的break found,这是java中的根据标签break的语法,冒号前的是标签,break可以根据标签选择跳出的位置。从这个方法中也可以看出,根据对象删除的时候是需要遍历数组来确定下标的,而且对null值进行了等值判断,否则使用equals方法。这里也可以得出一个结论:根据对象删除的效率要低于下标删除

下面看下fastRemove方法:

private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}

这个方法代码比较少,但是稍微有些不好理解,我用下面这个图来展示一下执行过程,其中“D”是要删除的对象:

3.4.set和get

set和get都特别简单,所以就放在一起来说了,源码如下:

public E set(int index, E element) {
    Objects.checkIndex(index, size);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
​
public E get(int index) {
    Objects.checkIndex(index, size);
    return elementData(index);
}
​
E elementData(int index) {
    return (E) elementData[index];
}

这部分代码简单到基本上不用解释了,我就不解释了^_^!

4.迭代器实现解析

4.1.iterator

iterator方法返回了内部类Itr的实例,Itr实现了Iterator接口,可以从前向后单向访问元素,并且可以在遍历过程中删除元素。遍历是通过next方法实现的,利用了Itr内部的一个int变量记录上一次访问的下标,比较简单就不说了,这里重点说一下Itr是如何在遍历中删除元素并且不抛出ConcurrentModificationException异常的,下面是源码:

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();
    }
}

checkForConmodification方法用来检查执行过程中元素有没有增减,如果此时有增减则会抛出ConcurrentModificationException异常。然后就调用ArrayList自身的remove方法删除上一个遍历的下标元素,根据上面对remove的讲解,可以想到,被删除原始后的所有元素都会向前移动一格,因此将上一个位置设置给cursor,此时的上一个位置实际上是被删除元素的下一个元素。最后一行很关键,因为ArrayList自己的remove方法会将modeCount增加1,因此将此时的modCount赋值给expectedModCount,这样后续的操作就不会因为Itr中记录的modCount与实际的modCount不一致而抛出ConcurrentModificationException了。

4.2.listIterator

与iterator方法类似,listIterator也是通过返回ListItr内部类实现的,这个内部类实现了ListIterator接口,我们上面也提到过,相比Iterator接口,ListIterator接口提供了双向访问和增加、修改的能力,因此为了复用其他能力,ListIterator继承了Itr,在Itr的基础上提供了previous方法用来向前遍历,原理和Itr的next类似,不细说了。

新增的set方法比较简单,就是调用了ArrayList自己的set方法对上一个遍历的下标元素进行重新赋值。

新增的add方法实现上和remove方法比较类似,也是通过重置expectedModCount的方式来避免出现ConcurrentModificationException异常的。

5.subList

subList是对一个完整ArrayList的切分,可以截取其中的一段,但这里需要注意的是,subList是逻辑上的分割,其中的元素都是原ArrayList中的,并不是新创建的独立元素,因此,对subList中元素的操作等同于对原始ArrayList中元素的操作。

我们从构造函数和add方法中,就可以看到其对原ArrayList的影响,以及实现原理,源码如下:

public SubList(ArrayList<E> root, int fromIndex, int toIndex) {
    this.root = root;
    this.parent = null;
    this.offset = fromIndex;
    this.size = toIndex - fromIndex;
    this.modCount = root.modCount;
}
​
private SubList(SubList<E> parent, int fromIndex, int toIndex) {
    this.root = parent.root;
    this.parent = parent;
    this.offset = parent.offset + fromIndex;
    this.size = toIndex - fromIndex;
    this.modCount = parent.modCount;
}
​
public void add(int index, E element) {
    rangeCheckForAdd(index);
    checkForComodification();
    root.add(offset + index, element);
    updateSizeAndModCount(1);
}

从第一个构造函数中可以看到,fromIndex被赋值给了内部的offset,然后在add方法中,会给index加上这个offset,然后调用原ArrayList的引用root来执行add方法。而第二个构造函数则说明了subList可以嵌套继续subList。

6.下集预告

下一集我们来聊聊LinkedList,这家伙还是能不用就不用,原因下集我们细聊。

插播个小广告:

本人新书发布!《企业架构与绕不开的微服务》。

  • 在理论方面,介绍了企业架构标准、云原生思想和相关技术、微服务的前世今生,以及领域驱动设计等;

  • 在实践方面,介绍了用于拆分微服务的“五步法”、包含4个维度的“企业云原生成熟度模型”,以及衡量企业变革成果的“效果收益评估方法”等。

本书可以帮助企业明确痛点、制定原则、规划路径、建设能力和评估成效,最终实现微服务架构在企业中的持续运营和持续演化,从而应对日益增多的业务挑战。

 点击这里进入购买页面

 更多内容请关注我的个人公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值