ArrayList 随记

ArrayList 的 elementData 为什么要用 transient 修饰?

/**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */
transient Object[] elementData; // non-private to simplify nested class access

Java中的ArrayList底层实现机制是动态扩容数组,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。

例如:

List list = new ArrayList(10);

这段代码的实际构建过程是:【 new Object[initialCapacity] 】

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    }
    // ...
}

因此当我们通过【 new ArrayList(10) 】构建一个新对象时,它的实际空间占用情况是:

Object[] objs = new Object[10];
System.out.println(objs.length); // 10

注:动态扩容内容后面再详解

根据上面的内容,如果不使用 transient 修饰 elementData ,会导致序列化elementData数组时,将未使用的空间也序列化了,无疑会浪费一部分空间。因此设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。

ArrayList覆写的writeObject()方法代码:(只保留关键代码)

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    // ...
    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]);
    }
    // ...
}

注意在代码的 for 循环中,使用的是【 i < size】,而不是 i < elementData.length,也可以看出来这里只序列化了存储的实际元素。通过这样的方式,可以节约一部分的空间和时间。

ArrayList 中 modCount 的作用

在ArrayList中有个成员变量modCount,继承于AbstractList。

在ArrayList的所有涉及结构变化的方法中都增加modCount的值,包括:add()、remove()、addAll()、removeRange()及clear()方法。这些方法每调用一次,modCount的值就加1。
【注:add()及addAll()方法的modCount的值是在其中调用的ensureCapacity()方法中增加的。】

如果modCount的值意外改变,那么迭代器或者列表迭代器就会抛出ConcurrentModificationException异常,通过 checkForComodification 判断:

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

ArrayList实现了自己的迭代器:

public Iterator<E> iterator() { return new Itr(); }
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; // expectedModCount 在遍历时被固定
    Itr() {}
    // ...
}

例如:

List<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");
strList.add("c");
Iterator<String> iterator = strList.iterator();
while (iterator.hasNext()) {
    String str = iterator.next();
    if ("a".equals(str)) {
        strList.remove(str);
    }
}

在执行了remove操作之后就会抛出ConcurrentModificationException,原因是利用了迭代器进行遍历,遍历时发生了异常并抛出。具体的流程是:
1.strList 添加 a、b、c 后共有三个元素,此时 modCount 为 3
2.遍历时,当碰到 b 时,调用了 strList 上的 remove 方法,这里会让 modCount 值加1,也就是4,但是 expectedModCount 在遍历时被固定,这就会导致 modCount 不再等于 expectedModCount,由此在下一次执行 iterator.next() 时就会抛出ConcurrentModificationException

因此,在遍历中删除元素的正确做法应该是使用Iterator:

// ...
Iterator<String> iterator = strList.iterator();
while (iterator.hasNext()) {
    String str = iterator.next();
    if ("a".equals(str)) {
        iterator.remove();
    }
}

补充:测试以下代码

List<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");
strList.add("c");
Iterator<String> iterator = strList.iterator();
while (iterator.hasNext()) {
    String str = iterator.next();
    if ("b".equals(str)) {
        strList.remove(str);
    }
}

运行后可以发现,这里并没有抛出异常,与之前的代码唯一的区别仅在于删除的是 b 元素。这是因为,执行 strList.remove 是,modCount 会加一,但同时需要注意的是,remove 后 size 也减一,这导致 hasNext 在判断时,因为 cursor != size 而返回 false,也就不会执行循环体内容,自然也就不会抛出异常了。

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

经检验,只要是当remove删除倒数第二个元素时,恰好逃脱了对最后一个元素的迭代,没有next,便没有报错。

通常来说,在迭代时不应该擅自去改动 arrayList,但是删除元素可以通过迭代器本身修改,如 iterator.remove();

ArrayList 的扩容机制

在 ArrayList 中,一共有三个构造函数:

public ArrayList() { //... }
public ArrayList(int initialCapacity)  { //... }
public ArrayList(Collection<? extends E> c) { //... }
  • 无参构造器扩容机制
public ArrayList() {
	// private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; 
}

当使用无参构造器创建ArrayList对象时,初始容量为0。但是当第一次添加元素时,则直接扩容为10。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 1、添加元素时,首要要确定能不能添加,调用 ensureCapacityInternal,传入需要的最小容量
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); // 2、首先通过 calculateCapacity 计算
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 3、当我们使用无参构造器时,这里条件正好满足
        return Math.max(DEFAULT_CAPACITY, minCapacity); // 4、那么这里返回 DEFAULT_CAPACITY ,DEFAULT_CAPACITY 为 10
    }
    return minCapacity;
}
  • 参数为容量大小(initialCapacity)的构造器
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 如果指定大于 0 的initialCapacity,则直接构建指定大小的 Object[] 即可
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        /**
        * 如果传入容量为 0,则赋值为 EMPTY_ELEMENTDATA,其中:private static final Object[] EMPTY_ELEMENTDATA = {};
        * 需要注意的是,这里和无参构造器不同,在添加时,calculateCapacity 方法判断 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为false,
        * 那么 calculateCapacity 方法返回的就是 minCapacity,在当前情况下 minCapacity 为 1,也就是第一次添加容量也为 1,而不是 10
        */
        this.elementData = EMPTY_ELEMENTDATA; // 
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}
  • 参数为 Collection<? extends E> 的构造器
public ArrayList(Collection<? extends E> c) {
    Object[] a = c.toArray(); // 首先将 Collection 转为 Object[]
    if ((size = a.length) != 0) { // a 不为空
        if (c.getClass() == ArrayList.class) {
            elementData = a; // 如果传入的也是 ArrayList,直接将 a 赋值即可
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class); // 否则,通过 Arrays.copyOf 复制数据
        }
    } else {
        // a 为空,则直接相当于 new ArrayList(0)
        elementData = EMPTY_ELEMENTDATA;
    }
}
  • ensureExplicitCapacity 及 grow 方法

在上面提到,calculateCapacity 方法计算容量后,将返回值传入 ensureExplicitCapacity 中:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    /**
    * 如果所需容量 minCapacity 大于 elementData 的长度,则需要扩容,这里的 minCapacity = size + 1
    */
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

再进入 grow 中看一下:

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容1.5倍
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) // 不能超过最大安全值减8 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity); // 扩容,并复制数据
}
  • 其他
/**
 * 将此ArrayList实例的容量修剪为列表的当前大小。应用程序可以使用此操作来最小化ArrayList实例的存储。
 * 简单来说,就是把未使用的空间给抹去
 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

/**
 * 如有必要,增加此ArrayList实例的容量,以确保它至少可以容纳最小容量参数指定的元素数。
 * 简单来说,就是确定当前空间是够存放 minCapacity 多的元素的,如果不能保证,则执行 ensureExplicitCapacity 扩容
 */
 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);
    }
}

其他常规方法看源码即可。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值