「Java 集合框架」之一 ArrayList

原创文章,转载请注明出处

Java程序的核心就是那些在启动时和运行中所创建的对象,如何管理这些对象是一项非常重要的工作。既要方便存储,又要方便读取,有时候还需要对对象进行排序,根据不同的场合,需要将同一类的对象存放在一起,就形成了容器的概念。

Java 集合框架.gif

Java类库为我们提供了一套相当完整的容器来解决程序运行中对象的存储问题,其中最常用的有 List、Set、Queue以及 Map。上面是一张完整的 Java 框架类库图,其中有许多接口和抽象类,他们定义了不同种类基础容器的行为,这个系列的文章将从源码的角度解释图中常用的集合类的实现以及使用。

线性的存储

ArrayList 其实是一个典型的线性表,从名字也不难看出它与数组有莫大的关联,其实 ArrayList 内部本身就是维护了一个数组,所有的插入、删除等操作都是基于这个数组实现的。

首先看一下 ArrayList 中的变量定义:

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

    private static final int DEFAULT_CAPACITY = 10;

    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    transient Object[] elementData;

    private int size;
}   

除了一些预定义的常量,ArrayList 中一共就两个变量:用于存放对象引用的数组elementData和标识个数的size。值得区分一下的概念是 size 和 capacity,size 表示的是当前 ArrayList 中所存放有效对象的个数,而 capacity 表示的是当前 elementData 数组的容量(即数组大小)。众所周知数组的长度一旦被指定就无法更改,既然 ArrayList 底层使用数组来实现,那它又是如何做到在被调用add()方法时看上去没有容量限制的呢?答案就是不断的比较 size 和 capacity,然后创建新的数组,再将原数组的内容复制过去,这在ArrayList内部称为扩容操作。

对象的存放

ArrayList共有三个构造器,他们的作用都是初始化elementData数组:

    // NO.1
    public ArrayList() {
        this(10);
    }

    // NO.2
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    // NO.3
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        size = elementData.length;
        if (elementData.getClass() != Object[].class)
           elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

前两个构造器通过指定的capacity来初始化一个空的数组:this.elementData = new Object[initialCapacity],如果没有initialCapacity参数则默认为10。

tips: 在新建一个ArrayList时可以预估需要的大小,以new ArrayList(20)这种形式调用,可以避免在使用ArrayList时多次扩容。

第三个构造器接收一个Collection参数,如果你有仔细看过文章开头的集合框架图不难发现ArrayList是Collection的一种实现,这个构造器表示可以接收Collection接口的所有实现类型,并将其转换为自身类型(ArrayList)进行存储。

扩容与移位

要想将对象放入 ArrayList 中需要调用add(E)或者add(int, E)方法,前者将对象按顺序放到末尾,后者可以指定放置的位置。

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

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

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

在调用两种 add 方法时都需要先调用ensureCapacityInternal,这个方法的做用是将数组进行扩容。由于在真正扩容之前 JDK 会进行一些逻辑检查和判断,最终调用到grow()函数,出于篇幅考虑将一些函数的调用关系省略掉,有兴趣的朋友可以自行查阅源码。先看看grow()函数中关键的部分:

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

可以看到参数 minCapacity 只是一个“建议”值,ArrayList 默认每次扩容为当前容量的1.5倍:

int newCapacity = oldCapacity + (oldCapacity >> 1);

在扩充到1.5倍之后如果还没有达到 minCapacity 时,便会采用 minCapacity 的值,最后调用 Arrays.copyOf(elementData, newCapacity);将整个数组进行拷贝。
回到add(int, E)方法,数组扩容完后,需要在指定的位置插入元素,接着便调用了System.arraycopy,根据参数可以看出这一步将 elementData 数组第index之后的元素全部向后移了一位,再在 index 位置插入新元素。
至此,对象的存放完成,通过对扩容和移位的分析,可以看出基于数组实现的 ArrayList 在对象的插入操作上效率并不高,但是数组的优点是快速的随机访问,在接下来的对象获取方法中将可以看到 ArrayList 很好的继承了数组的这一特性。

对象的获取

ArrayList 最常用的get(int index)方法一共只有两行:

    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    //...
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

在检查完参数 index 的范围之后,直接就返回 index 位置上的元素,非常快速。

另一个获取对象有关的常用函数是indexOf(Object o),到这里读者应该可以想到基于数组的 ArrayList 是怎样查找元素在数组中的下标了,遍历:

    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的参数允许为 null,并且如果数组中有值为 null的元素(在 size 范围以内)时将返回下标。

调用remove(int index)函数同样会使数组中多个元素位置变动:

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

同样是调用 arraycopy方法,只不过拷贝的位置与 add 时相反。同时注意到elementData[--size] = null;是非常关键的,正如注释所说,进行remove操作以后,数组最后一个位置上引用的对象逻辑上已经被“移除”,但是实际数组还保持着对象的引用,这会导致 GC 无法将其回收,造成内存泄露。

功能性函数

介绍完了主要操作函数,ArrayList 封装的一些功能函数也非常有意思。

  • trimToSize:
    这个函数的作用是将数组容量(capacity)修整到 size 的大小。譬如ArrayList在大量读入对象之后又进行了许多移除操作,并且预计在今后很长一段时间容量都将保持在较小范围内,这时可以手动都用trimToSize()来节约内存空间。
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
  • addAll:
    addAll 方法接收一个实现了 Collection 接口的类型,并将其添加到当前 ArrayList 末尾:
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

其实质最终还是调用了 arraycopy 方法,ArrayList.addAll()方法很容易让人联想到定义在 Collections 中的 addAll 方法,在没有特殊要求的时候两者可以互换,个别情况下由于其实现逻辑不同两者的性能会有一些差异,这里不做过多讨论,开发者可以根据编程风格和规范自行选择,Collections.addAll()的定义如下:

    public static <T> boolean addAll(Collection<? super T> c, T... elements) {
        boolean result = false;
        for (T element : elements)
            result |= c.add(element);
        return result;
    }

小结

ArrayList 正如其名字,是实现了 List接口规范,底层采用数组的一个集合类。ArrayList 继承了数组的所有优缺点,并且在使用上针对 List 接口进行了封装,例如在指定位置插入、删除元素等。开发人员在使用时可以完全按照 List 接口定义的规范进行调用,但是知道其底层实现细节对程序调优、不同集合类之间的选择有很大的帮助。

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

推荐阅读更多精彩内容