Java集合源码分析之List(二):ArrayList

做了这么多准备,终于到了ArrayList了,ArrayList是我们使用最为频繁的集合类了,我们先看看文档是如何介绍它的:

Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)

可见,ArrayListVector的翻版,只是去除了线程安全。Vector因为种种原因不推荐使用了,这里我们就不对其进行分析了。ArrayList是一个可以动态调整大小的List实现,其数据的顺序与插入顺序始终一致,其余特性与List中定义的一致。

ArrayList继承结构

ArrayList结构图

可以看到,ArrayListAbstractList的子类,同时实现了List接口。除此之外,它还实现了三个标识型接口,这几个接口都没有任何方法,仅作为标识表示实现类具备某项功能。RandomAccess表示实现类支持快速随机访问,Cloneable表示实现类支持克隆,具体表现为重写了clone方法,java.io.Serializable则表示支持序列化,如果需要对此过程自定义,可以重写writeObjectreadObject方法。

一般面试问到与ArrayList相关的问题时,可能会问ArrayList的初始大小是多少?很多人在初始化ArrayList时,可能都是直接调用无参构造函数,从未关注过此问题。例如,这样获取一个对象:

ArrayList<String> strings = new ArrayList<>();

我们都知道,ArrayList是基于数组的,而数组是定长的。那ArrayList为何不需要指定长度,就能使我们既可以插入一条数据,也可以插入一万条数据?回想刚刚文档的第一句话:

Resizable-array implementation of the List interface.

ArrayList可以动态调整大小,所以我们才可以无感知的插入多条数据,这也说明其必然有一个默认的大小。而要想扩充数组的大小,只能通过复制。这样一来,默认大小以及如何动态调整大小会对使用性能产生非常大的影响。我们举个例子来说明此情形:

比如默认大小为5,我们向ArrayList中插入5条数据,并不会涉及到扩容。如果想插入100条数据,就需要将ArrayList大小调整到100再进行插入,这就涉及一次数组的复制。如果此时,还想再插入50条数据呢?那就得把大小再调整到150,把原有的100条数据复制过来,再插入新的50条数据。自此之后,我们每向其中插入一条数据,都要涉及一次数据拷贝,且数据量越大,需要拷贝的数据越多,性能也会迅速下降。

其实,ArrayList仅仅是对数组操作的封装,里面采取了一定的措施来避免以上的问题,如果我们不利用这些措施,就和直接使用数组没有太大的区别。那我们就看看ArrayList用了哪些措施,并且如何使用它们吧。我们先从初始化说起。

构造方法与初始化

ArrayList一共有三个构造方法,用到了两个成员变量。

//这是一个用来标记存储容量的数组,也是存放实际数据的数组。
//当ArrayList扩容时,其capacity就是这个数组应有的长度。
//默认时为空,添加进第一个元素后,就会直接扩展到DEFAULT_CAPACITY,也就是10
//这里和size区别在于,ArrayList扩容并不是需要多少就扩展多少的
transient Object[] elementData;

//这里就是实际存储的数据个数了
private int size;

除了以上两个成员变量,我们还需要掌握一个变量,它是

protected transient int modCount = 0;

这个变量主要作用是防止在进行一些操作时,改变了ArrayList的大小,那将使得结果不可预测。

下面我们看看构造函数:

//默认构造方法。文档说明其默认大小为10,但正如elementData定义所言,
//只有插入一条数据后才会扩展为10,而实际上默认是空的
 public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//带初始大小的构造方法,一旦指定了大小,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);
    }
}

//从一个其他的Collection中构造一个具有初始化数据的ArrayList。
//这里可以看到size是表示存储数据的数量
//这也展示了Collection这种抽象的魅力,可以在不同的结构间转换
public ArrayList(Collection<? extends E> c) {
    //转换最主要的是toArray(),这在Collection中就定义了
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

重要方法

ArrayList已经是一个具体的实现类了,所以在List接口中定义的所有方法在此都做了实现。其中有些在AbstractList中实现过的方法,在这里再次被重写,我们稍后就可以看到它们的区别。

先看一些简单的方法:

//还记得在AbstractList中的实现吗?那是基于Iterator完成的。
//在这里完全没必要先转成Iterator再进行操作
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

//和indexOf是相同的道理
 public int lastIndexOf(Object o) {
    //...
}

//一样的道理,已经有了所有元素,不需要再利用Iterator来获取元素了
//注意这里返回时把elementData截断为size大小
public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

//带类型的转换,看到这里a[size] = null;这个用处真不大,除非你确定所有元素都不为空,
//才可以通过null来判断获取了多少有用数据。
public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // 给定的数据长度不够,复制出一个新的并返回
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

数据操作最重要的就是增删改查,改查都不涉及长度的变化,而增删就涉及到动态调整大小的问题,我们先看看改和查是如何实现的:

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

//只要获取的数据位置在0-size之间即可
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

//改变下对应位置的值
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

增和删是ArrayList最重要的部分,这部分代码需要我们细细研究,我们看看它是如何处理我们例子中的问题的:

//在最后添加一个元素
public boolean add(E e) {
    //先确保elementData数组的长度足够
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    rangeCheckForAdd(index);

    //先确保elementData数组的长度足够
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //将数据向后移动一位,空出位置之后再插入
    System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
    elementData[index] = element;
    size++;
}

以上两种添加数据的方式都调用到了ensureCapacityInternal这个方法,我们看看它是如何完成工作的:

//在定义elementData时就提过,插入第一个数据就直接将其扩充至10
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    
    //这里把工作又交了出去
    ensureExplicitCapacity(minCapacity);
}

//如果elementData的长度不能满足需求,就需要扩充了
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);
    
    //扩充完之后,还是没满足,这时候就直接扩充到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);
}

至此,我们彻底明白了ArrayList的扩容机制了。首先创建一个空数组elementData,第一次插入数据时直接扩充至10,然后如果elementData的长度不足,就扩充1.5倍,如果扩充完还不够,就使用需要的长度作为elementData的长度。

这样的方式显然比我们例子中好一些,但是在遇到大量数据时还是会频繁的拷贝数据。那么如何缓解这种问题呢,ArrayList为我们提供了两种可行的方案:

  • 使用ArrayList(int initialCapacity)这个有参构造,在创建时就声明一个较大的大小,这样解决了频繁拷贝问题,但是需要我们提前预知数据的数量级,也会一直占有较大的内存。

  • 除了添加数据时可以自动扩容外,我们还可以在插入前先进行一次扩容。只要提前预知数据的数量级,就可以在需要时直接一次扩充到位,与ArrayList(int initialCapacity)相比的好处在于不必一直占有较大内存,同时数据拷贝的次数也大大减少了。这个方法就是ensureCapacity(int minCapacity),其内部就是调用了ensureCapacityInternal(int minCapacity)

其他还有一些比较重要的函数,其实现的原理也大同小异,这里我们不一一分析了,但还是把它们列举出来,以便使用。

//将elementData的大小设置为和size一样大,释放所有无用内存
public void trimToSize() {
    //...
}

//删除指定位置的元素
public E remove(int index) {
    //...
}

//根据元素本身删除
public boolean remove(Object o) {
    //...
}

//在末尾添加一些元素
public boolean addAll(Collection<? extends E> c) {
    //...
}

//从指定位置起,添加一些元素
public boolean addAll(int index, Collection<? extends E> c){
    //...
}

//删除指定范围内的元素
protected void removeRange(int fromIndex, int toIndex){
    //...
}

//删除所有包含在c中的元素
public boolean removeAll(Collection<?> c) {
    //...
}

//仅保留所有包含在c中的元素
public boolean retainAll(Collection<?> c) {
    //...
}

ArrayList还对父级实现的ListIterator以及SubList进行了优化,主要是使用位置访问元素,我们就不再研究了。

其他实现方法

ArrayList不仅实现了List中定义的所有功能,还实现了equalshashCodeclonewriteObjectreadObject等方法。这些方法都需要与存储的数据配合,否则结果将是错误的或者克隆得到的数据只是浅拷贝,或者数据本身不支持序列化等,这些我们定义数据时注意到即可。我们主要看下其在序列化时自定义了哪些东西。

//这里就能解开我们的迷惑了,elementData被transient修饰,也就是不会参与序列化
//这里我们看到数据是一个个写入的,并且将size也写入了进去
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    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]);
    }

    //modCount的作用在此体现,如果序列化时进行了修改操作,就会抛出异常
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

readObject是一个相反的过程,就是把数据正确的恢复回来,并将elementData设置好即可,感兴趣可以自行阅读源码。

总结

总体而言,ArrayList还是和数组一样,更适合于数据随机访问,而不太适合于大量的插入与删除,如果一定要进行插入操作,要使用以下三种方式:

  • 使用ArrayList(int initialCapacity)这个有参构造,在创建时就声明一个较大的大小。

  • 使用ensureCapacity(int minCapacity),在插入前先扩容。

  • 使用LinkedList,这个无可厚非哈,我们很快就会介绍这个适合于增删的集合类。

上一篇:Java集合源码分析之List(一):超级接口List

下一篇:Java集合源码分析之Queue(一):超级接口Queue


我是飞机酱,如果您喜欢我的文章,可以关注我~

编程之路,道阻且长。唯,路漫漫其修远兮,吾将上下而求索。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容