Java集合之ArrayList

Java集合之ArrayList

ArrayList是Java中使用非常频繁的类,它是一个可动态调整大小的数组List实现。

1. 源码分析

1.1 定义

首先来看ArrayList的定义:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

AbstractList 是List接口的抽象实现,减少子类实现List接口的复杂度。
RandomAccess 声明有随机访问的能力。
Cloneable 覆盖实现了clone(), 能被克隆。
Serializable 支持序列化,表示ArrayList可以通过序列化进行传输。

1.2 属性

ArrayList只有5个属性,分别是:

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // 内部的实际buffer
private int size; // 数组已使用大小

这里要特别注意EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA两个属性,它们都表示一个空的数组,它们的区别会在后面的分析中提到。

1.3 构造方法

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

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

ArrayList的无参构造方法会直接将elementData初始化,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个静态的空数组,达到内存共享的目的,节省了内存空间。
第二个ArrayList构造方法设置了初始化的容量大小,当设置的大小为0时,使用静态属性EMPTY_ELEMENTDATA来节省内存空间,当有值时直接初始化一个该大小的数组。

1.4 重要方法

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

ensureCapacityInternal()这个方法相当重要,直接来看看这个方法:

    // 如果使用无参构造方法,在首次调用add方法后计算出的大小为应该为DEFAULT_CAPACITY也就是10.
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        // 计算大小,minCapacity表示当前需要最小的内存,如果比当前的数组还要大,则需要重新申请数组大小。
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); // 1 + 0.5
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

在最后调用的Arrays.copyOf()时会重新申请一个大小为newCapacity的新的数组,并将老数组的数据拷贝至新的数组中。
上面的一连串代码中,根据判断elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA可以区分出是否使用了无参构造方法,如果是则会在首次调用add()时申请大小为10的新数组。ensureExplicitCapacity是ArrayList中非常重要的方法,在当前数组大小不能满足时会重新计算并申请大小,新申请的大小比以前的大小增加了50%。ArrayList可以动态调整大小就是因为这个方法。

trimToSize()
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

这个方法在平时并不常用,并且size < elementData.length这里的判断比较奇怪,size的大小和elementData.length难道还能不一样吗?

其实在ArrayList中Size并不是表示elementData的大小。还记得在上面的ensureExplicitCapacity方法么,elementData容量大小不能满足后会新申请一个比原来数组大50%的新数组,而Size没有增加。这里要理解在ArrayList中其实有两种概念:CapacitySizeCapacity是表示数组elementData的容量,而Size是表示的当前数组已使用的大小,这个大小不会囊括新申请的未使用的部分。

这下好理解了,调用trimToSize会计算已使用的部分大小,如果size为0,直接使用空数组代替,否则会生产一个size大小的数组。这样做的目的就减少数组的大小,将后面申请的未使用的空数组释放。

但该方法不宜频繁调用,可以想象如果在操作ArrayList后马上调用该方法再有数组增加时又会导致重新计算数组大小并重新申请新的数组,这样会导致很大的开销。所以应当在ArrayList使用稳定下来之后调用该方法,来最好的节省内存空间。

writeObject() 和 readObject()

在介绍ArrayList属性时,elelmentData是这样声明的:

transient Object[] elementData;

transient关键字表示忽略序列化,而ArrayList实际上是实现了序列化的接口。奇怪了,ArrayList中最主要的数据竟然没有序列化,这是为什么呢?

其实ArrayList重写了writeObject() 和 readObject()方法:

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        s.defaultWriteObject();

        s.writeInt(size);

        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        s.defaultReadObject();

        s.readInt();

        if (size > 0) {
            ensureCapacityInternal(size);

            Object[] a = elementData;
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

上面的代码做了一些省略,可以看到在writeObject()时的大小是仅将size大小的数据写入了流中,在readObject()时也是仅读取了size大小的数据。为什么要重写了呢?其实在上面的分析中得到size是真实使用的大小,取size当然就不会去序列化后面那些未使用的数据了,这些数据其实是没有作用的,使得减小了序列化大小。

2. 常见的错误

调用ArrayList的toArray(),抛出java.lang.ClassCastException异常

原因在与elelmentData的声明是Object[],在转换为其他类型,如Integer时会就会发生类转换异常,解决办法是调用toArray(T[] a)方法转为T类型

for循环、迭代或排序等,抛出ConcurrentModificationException异常

在操作ArrayList时,内部有一个属性modCount用来计算操作的次数,如在for正在迭代时对ArrayList进行了操作导致modCount变更,从而导致抛出该异常。该异常常发生在多线程中操作ArrayList导致。

3. 小结

1. 从ArrayList的构造方法看到,最好是在构造时使用设置初始化大小的函数。可以免使去重新申请数组,如果在创建ArrayList时无法指定大小,应该在使用ArrayList并达到较为稳定后调用trimToSize来减少空间的使用。
2. ArrayList内部实现是数组,且实现了RandomAccess,使得随机访问是ArrayList的优势,所以在有大量随机操作时ArrayList应该较优先考虑的数组。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值