jdk源码阅读一:ArrayList

本文详细介绍了Java中ArrayList的工作原理,包括其内部实现、容量扩展策略、常用方法解析等内容,并深入探讨了迭代器实现及其特性。

ArrayList介绍

这里写图片描述
ArrayList是我们使用的最多的集合类了。特点主要如下:

  • 基于数组实现底层存储,容量可以自动扩展。
  • 实现List全部接口。
  • 允许null对象
  • 提供操作数组容量大小的方法。
  • 粗略来看和Vector类相同,但是 Vector在 方法上加了同步,所以Vector是线程安全的。
  • size,isEmpty,get,set,iterator和listIterator运行时间为常量。
  • add方法 如果添加n个元素 时间复杂度O(n)
  • ArrayList非 thradsafe,所以自己wrap或者用Collections#synchronizedList
  • 返回的iterator是fail-fast,如果iterator创建后,修改了list结构则会抛出异常ConcurrentModificationException,
    注意这里异常表明是一个bug,需要避免,而不能处理业务逻辑。

size 变量

size 表示当前list中存储了元素的个数,初始化为0。add一个元素加一,删除一个元素减一。

capacity(容量)增长策略

每个ArrayList示例都有一个capacity。表示数组(list中用来储存元素)的大小。

capacity至少和size相等,不会小于它。capacity大小是自动增长的。你也可以调用ArrayList#ensureCapacity方法改变容量。
不过如果你将容量改小 是不起作用的,ensureCapacity方法不允许。

增长容量的方法有三个。

这里写图片描述

ensureCapacity

` /**
 * Increases the capacity of this <tt>ArrayList</tt> instance, if
 * necessary, to ensure that it can hold at least the number of elements
 * specified by the minimum capacity argument.
 *
 * @param   minCapacity   the desired minimum capacity
 */
public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;

    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}`

说明:

  • public的,说明是提供给用户操作的,因为其他两个方法是private的。这个方法Arraylist内部是没有调用的。内部扩容调用的是ensureCapacityInternal方法。
  • 增加容量如果有必要:原则就是判断minCapacity 和当前的capacty大小,如果大则增加。
  • minExpand(最小扩展容量)可能是0 ,也可能是DEFAULT_CAPACITY=10。
  • 一般ensureCapacity类型的三个方法只是确保最小容量满足,但是真正的增长策略可能不同,比如add一个元素,minCapacity=size+1。
    这时候如果需要扩容(minCapacity>size)那么数组只需要增大1,即可,但是这样后面每次都要复制,所以后面真正讲到的grow方法策略不是+1,而是然后大小增加150%。

源码分析:

  • minExpand表示最小拓展容量

首先我们看下ArrayList创建的两种创建方法和他的两个常量

ArrayList arr = new ArrayList<>();
ArrayList arr = new ArrayList<>(0);

 /**
 * Shared empty array instance used for empty instances.
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

如果使用第一种创建方法那么this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

如果使用第二种创建方法 则this.elementData = EMPTY_ELEMENTDATA;

什么意思?就是说如果你没穿参数,那么Arraylist 设置 默认空数组。如果你传了个0,则认为是你故意设置容量为0的。它用上面两个常量来区分这种行为,区分的目的在于对待这两种不同的空数组, 第一次的容量增长策略不同。

下面我们接着说ensureCapacity方法,就好解释多了,如果elementData!=DEFAULTCAPACITY_EMPTY_ELEMENTDATA 那么minExpand最小扩展容量=0,如果否则minExpand = DEFAULT_CAPACITY=10。

  • 委托给ensureExplicitCapacity方法,扩容

然后比较传入参数和minExpand的大小。取其中大者。然后委托给ensureExplicitCapacity方法。

ensureExplicitCapacity

`private void ensureExplicitCapacity(int minCapacity) {
    //修改次数 计数器 在iterator迭代出现并发修改快速失败就是利用到该字段
    modCount++;

    // overflow-conscious code
    // 如果minCapacity大于数组长度则扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}`

grow方法是真正的增长策略方法:

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    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);
}

方法说明:
1. newCapacity先扩展到为原来的1.5倍。
2. 比较newCapacity和minCapacity选择大的。
3. newCapacity如果超过MAX_ARRAY_SIZE(= Integer.MAX_VALUE - 8),则调用hugeCapacity,增大到Integer.MAX_VALUE。
4. 调用Arrays.copyOf调整elementData到新容量。

ensureCapacityInternal

  private void ensureCapacityInternal(int minCapacity) {
    //如果满足elementData是默认空数组,则比较 minCapacity和DEFAULT_CAPACITY 取大者
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

到此容量增长策略已经分析完成。

扩容方法总结:

  1. ensureCapacity是public给开发人员调用的api,内部是没有调用的。
  2. ensureCapacityInternal才是内部判断是否需要扩容的方法。
  3. 上面两个方法都是调用ensureExplicitCapacity实现。
  4. ensureCapacityInternal和ensureExplicitCapacity是私有的。
  5. 这三个方法是判断是否需要扩容,但是不一定真正执行扩容动作,真正的方法是grow。
  6. 如果我们能够提前预知list容量 直接赋值容量,可以减少后面自动扩容导致的数组复制的开销,调用ensureCapacity直接到一个比较大的容量也是可行的。

List常用方法分析

这里写图片描述

插入元素

顺序插入add(E e)

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

确保容量加1,然后直接数组赋值。时间复杂度O(1)。

随机插入

public void add(int index, E element) {
    //检查index是否在 0-size范围内
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

因为是随机插入,所以要把插入位置index以及以后的元素向后移动一位。所以速度会慢些。

插入集合addAll

这里不再细讲,大致思路是将Collection转成数组然后插入数组。不过如果是随机插入容量足够 还是复制了两次(第一次向后移动n位,第二次赋值Collection 到指定位置)。

clear清空

很简单 循环将数组设置null

检索操作

主要包括contains,containsAll,indexOf,lastIndexOf。

containsAll循环调用的contaions,contaios调用的indexOf。
indexOf和lastIndexOf差不多,一个从数组顺序查找,一个从数组逆序查找。我们主要看下lastIndexOf。

public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

方法也很简单,但是时间复杂度还是非常高的,平均O(n*n)。

get 随机获取元素

速度很快O(1),会先检查index是否合法[0,size)。

remove方法

有四个,根据元素删除,需要遍历数组,找到位置,然后移动数组。根据index删除,直接移动数组。效率都不高。

removeIf是1.8新加的,根据传入lambda 表达式判断是否删除元素,会应用于每个元素,内部通过迭代器遍历元素,判断是否需要该元素。

迭代器

有iterator,listiterator 和Spliterator。

  1. iterator主要通过内部类实现,存储了一个cursor,默认无参的=0。
  2. listiterator继承了iterator,实现了可以向前遍历。
  3. Spliterator(splitable iterator可分割迭代器)接口是Java为了并行遍历数据源中的元素而设计的迭代器,这个可以类比最早Java提供的顺序遍历迭代器Iterator,但一个是顺序遍历,一个是并行遍历。

可以参考:https://segmentfault.com/q/1010000007087438

iterator分析

public Iterator<E> iterator() {
    return new Itr();
}

是ArrayLIst一个内部类,截取部分代码:

 private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        //每次都会检查modCount是否改变,改变则抛出异常
        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];
    }

listiterator分析

 private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        super();
        cursor = index;
    }

    public boolean hasPrevious() {
        return cursor != 0;
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor - 1;
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        checkForComodification();
        int i = cursor - 1;
        if (i < 0)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i;
        return (E) elementData[lastRet = i];
    }

部分代码如上,继承了Itr,添加了逆序遍历功能。

子列表

通过一个内部类实现,部分代码如下:

 private class SubList extends AbstractList<E> implements RandomAccess {
    private final AbstractList<E> parent;
    private final int parentOffset;
    private final int offset;
    int size;

看出来 主要通过一个offset变量来记录开始位置在数组中的起始位置,还是共享父类的数组。

序列化、反序列化

ArrayList实现了Serializable接口,所以它支持序列化,但是我们看到底层存储:

 transient Object[] elementData; // non-private to simplify nested class access

又是transient,表示忽略该字段的序列化,其实他是通过writeObject(java.io.ObjectOutputStream s),readObject(java.io.ObjectInputStream s)两个方法重写实现的。注意这两个方法是固定写法。以write为例:

 private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
 //记录当前修改次数 
    int expectedModCount = modCount;
    //写入类元信息和非静态成员和非transient成员
    s.defaultWriteObject();
    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    //如果序列化过程中发现有修改list结构 则抛出异常

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

上面代码中写了两次size,网上说是兼容老板jdk原因。还提到反序列化老版本会恢复容量 新版本只恢复size大小的容量。
具体参考:
http://mushanshitiancai.github.io/2016/06/17/java/language/JDK%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-ArrayList/#序列化-反序列化

克隆

克隆只是浅拷贝。元素对象还是共享的。

其他总结

用的最多的移动数组方法是Arrays.copyOf和 System.arraycopy。

pdf下载

这里写链接内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值