ArrayList源码分析

整体介绍

ArrayList实现了List接口,是一个常见的集合类,它有一下特点:

  • 是顺序容器,即元素存放的数据与放进去的顺序相同,
  • 允许放入null元素,
  • 底层通过数组实现。
  • 添加元素而容量不足时,可自动扩充底层数组的容量。
  • 除该类未实现同步外,其余跟Vector大致相同。
  • 操作时间复杂度分别为:
    • size,isEmpty,get,set,iterator,listIterator方法运行时间为常量时间
    • add方法运行为摊还常量时间,也即增加n个元素的时间为O(n)
    • 其他的操作运行时间大致为线性时间,常数因子相较于LinkedList更小。

摊还时间是一个操作的平均代价,可能某次操作代价很高,但总体的来看也并非那么糟糕;add方法是摊还常量时间与底层数组容量自动扩充有关

image.png

源码分析

成员变量

    /**
     * 初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 当ArrayList的空实例,用于无参数初始化
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * ArrayList的底层数组
     * 泛型只是编译器提供的语法糖所以这里的数组是一个Object数组
     * ArrayList的容量就是elementData.length
     * 当ArrayList为空时,elementData == EMPTY_ELEMENTDATA
     * 当第一个元素加入时elementData.length == DEFAULT_CAPACITY
     * transient 说明这个数组无法序列化。
     */
    private transient Object[] elementData;

    /**
     * ArrayList包含的元素数量,与容量不同.
     */
    private int size;

    /**
     * 数组最大容量
     * 一些虚拟器需要在数组前加个头标签,所以减去8 。 
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 这个变量并不是ArrayList里面定义的,而是继承自父类AbstractList
     * ArrayList在使用Iterator时发生不同步会抛出错误就是依靠了这个变量
    (虽然不可靠,想要可靠的同步保证用
    List list = Collections.synchronizedList(new ArrayList(...))
    或
    concurrent包下的CopyOnWriteArrayList
    或
    用synchronized进行一层代理)
     * add和remove方法都是有modCount++.
     * 下面再具体说说modCount的作用
     */
     protected transient int modCount = 0;

get()

get()方法本身很简单,不涉及数组的容量扩充,除了检查下标范围以及类型转换,与一般的数组get()操作没区别

   public E get(int index) {
        //检查是否越界,
        //由于java数组本身就会检查是否越界,所以这里只是检查是否超过了size,
        //要知道数组的大小大于size,index大于size并不会触发数组越界.
        rangeCheck(index);
        return elementData(index);
    }
    
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        //对元素进行类型转换,底层储存是Object,但返回是E.
        return (E) elementData[index];
    }

set()

set()方法也很简单,不涉及数组的容量扩充,除了检查下标范围,与一般的数组set()操作没区别

    public E set(int index, E element) {
        //在get()中提到的,检查越界问题.
        rangeCheck(index);
        //直接用element替换elementData[index]
        //赋值到指定位置,复制的仅仅是引用
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

add()

  • 如果不算入容量扩充的耗费,add()方法运行时间为常量时间.但是可能某次add()触发了容量扩充,那么那次操作的运行时间为线性时间.所以add()的运行时间为摊还常量时间,即n次操作的时间为O(n).

  • add()会触发modCount++

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    /**
     *从这个方法可知第一次add元素,初始容量为DEFAULT_CAPACITY,即10
     */
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
    /**
     *add()的modCount++来源于这里
     *当新容量大于当前容量时,触发容量扩充,即调用grow方法.
     */
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    /**
     *ArrayList实现容量自动扩充是依靠了这个方法.
     *新容量为原容量的1.5倍
     *耗费时间为O(n),因为需要复制原来的元素到扩充后的数组.
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //每次扩充,新容量为原容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // //扩展空间并复制
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

remove

  • remove运行时间为O(n),因为ArrayList底层实现为数组,所以删除元素后,需要移动之后的元素.
  • 除了越界检查,与一般数组的remove没有区别
  • remove后并不会影响数组容量
  • remove会触发modCount++
    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]的引用,
       //因为假如不清除引用,那么ArrayList就会一致保留那个对象
       //GC不能清除仍被持有引用的对象
        elementData[--size] = null;

        return oldValue;
    }

序列化

  • 在上面成员变量那里提到了Object[] elementData用了transient关键字,无法序列化,这里ArrayList复写了readObject和writeObject方法,实现底层数组的序列化.
  • 复写的writeObject用modCount保证了当发生不同步问题时抛出错误(虽然不可靠)
  • 关于序列化的知识可以看我转载的<<深入理解Java对象序列化>>
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // 记下方法开始时的modCount
        int expectedModCount = modCount;
        //将没有transient关键字的成员变量写入
        s.defaultWriteObject();

        //这里写入数组的容量,但是把数组元素的数量size视为容量,
        //而不用原来的容量elementData.length
        s.writeInt(size);

        // 将底层数组的元素依次写入
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        //与之前记下的modCount对比,看是否有其他线程操作了ArrayList,有则抛出错误.
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // 将没有transient关键字的成员变量读入
        s.defaultReadObject();

        // 将数组容量读入,
        //但是如上面writeObject所写把size视为了数组容量,所以这里没有实质的作用
        s.readInt(); // ignored

        if (size > 0) {
            // 扩充数组大小
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // 把数组元素一次写入
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

迭代器

ArrayList用iterator()可以返回迭代器.

  • 实现了Iterator<E>接口;private class Itr implements Iterator<E>
  • 当有多个线程同时操作ArrayList,使用迭代器可以抛出错误,避免发生不同步(这里就是用了modCount,用法与writeObject()的差不多)
  • 迭代器使得对容器的遍历操作完全与其底层相隔离,可以到达极好的解耦效果。
iterator()
    public Iterator<E> iterator() {
        //使用构造函数,返回一个新的迭代器
        return new Itr();
    }
迭代器成员变量
        int cursor;       // 下一个返回的元素的下标
        int lastRet = -1; // 上一次返回的元素的下标,初始为-1
        //记下创建迭代器时的modCount,用于之后的同步检查
        int expectedModCount = modCount;
hasNext()

hasNext()用于检测是否有下一个元素返回,实现很简单.

        public boolean hasNext() {
            return cursor != size;
        }
checkForComodification()
  • 这个方法是迭代器可以在多个线程操作时抛出错误(fail-fast机制)的关键方法
  • 下面的removenext都调用了这个方法
  • 实现很简单,对比modCount,如果不一致,则说明有其他线程也在用ArrayList,抛出错误.
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
next()
  • next()方法的实现很简单,代码写的很清楚
  • next()在内部做了很多安全检查,保证了使用迭代器的安全性
  • next() 调用了checkForComodification(),使用了fail-fast机制
        @SuppressWarnings("unchecked")
        public E next() {
            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];
        }
remove()
  • 迭代器的remove()方法内部是调用了ArrayList的remove()方法
  • remove() 调用了checkForComodification(),使用了fail-fast机制
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

后记

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

推荐阅读更多精彩内容

  • ArrayList是在Java中最常用的集合之一,其本质上可以当做是一个可扩容的数组,可以添加重复的数据,也支持随...
    ShawnIsACoder阅读 542评论 4 7
  • ArrayList 原文见:Java 容器源码分析之 ArrayList 概述 ArrayList是使用频率最高的...
    Leocat阅读 211评论 0 0
  • 定义 除了实现了List接口,还实现了RandomAccess,Cloneable, java.io.Serial...
    zhanglbjames阅读 414评论 0 0
  • List List是一个维持内部元素有序的采集器,其中的每个元素都会拥有一个索引,每个元素都可以通过他的索引获取到...
    dooze阅读 385评论 0 4
  • ArrayList 概述 ArrayList 是实现List接口的动态数组,每个ArrayList实例都有一个默认...
    ZcEDiaos阅读 216评论 0 0