ArrayList源码解析

ArrayList的本质是动态数组。既然是数组,肯定是可以随机访问的。ArrayList是非线程安全的,当当个线程并发访问同一个ArrayList时,会抛出ConcurrentModificationException,这就是fail-fast机制。

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

由上面代码知

  1. 实现List<E>接口表明实现了List接口中的size(),add(),set()等等方法
  2. 实现RandomAccess接口表明支持随机访问
  3. 实现Cloneable接口表明支持clone()
  4. 实现Serializable接口表明支持序列化。

成员变量


//数组,由此表明ArrayList是数组
private transient Object[] elementData;

 //记录数组中所含元素的个数
 private int size;
 //数组的最大容量。如果请求更大的容量,可能会导致OutOfMemoryError
 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

构造方法

//构造一个容量为initialCapacity的List
    public ArrayList(int initialCapacity) {
        super();
        //initialCapacity<0时,抛出参数异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        
        //其本质是创建一个长度为initialCapacity的数组
        this.elementData = new Object[initialCapacity];
    }

   //默认容量为10
    public ArrayList() {
        this(10);
    }

    //构造一个内容为集合c的List
    public ArrayList(Collection<? extends E> c) {
        //将c转化为数组,赋值给elementData数组
        elementData = c.toArray();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        //如果当前c.toArray不是Object类型的,则将其拷贝为Object类型的
        if (elementData.getClass() != Object[].class)
        //数组拷贝
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

成员方法

我们最常用的就是add()和set()方法,我们先来看这两个方法

添加 时间复杂度为O(1)

add(E e)


//在数组末尾添加一个元素
    public boolean add(E e) {
        //确保数组增加元素后,数组有足够的容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //在size位置处添加e
        elementData[size++] = e;
        return true;
    }


添加元素时,会首先调用ensureCapacityInternal(minCapacity)

ensureCapacityInternal(int minCapacity)


private void ensureCapacityInternal(int minCapacity) {
        modCount++;
        // overflow-conscious code
        //如果所需的容量大于当前数组的容量,那么,需要增加数组的容量,调用grow(int minCapacity)
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

     //扩大数组容量为原来的1.5倍
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果扩充后的容量大于MAX_ARRAY_SIZE,调用hugeCapacity()
        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);
    }

    private static int hugeCapacity(int minCapacity) {
    //当minCapacity<0时,一个内存数组的分配不能被创建,会抛出OutOfMemoryError
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }


在上述代码中,我们可以看到,当数组容量不够时,数组有一个扩容的过程,在扩容的过程中,会将原来数组的元素拷贝到新的数组中,这是一个很耗时的操作。由此可见,动态数组(ArrayList)在使用方便的同时,也会承担降低性能的风险。当然,如果我们在用ArrayList时,知道所需的容量,可以构造所需容量的ArrayList。

注意:在ArrayList中有两个情况可以导致OutOfMemoryError

  1. 当minCapacity<0时,系统无法创建长度小于0 的数组;
  2. 当数组容量超过VM中堆的剩余空间大小时,VM无法为其分配足够的内存。

add(int index, E element)


 //在数组的特定位置插入一个元素
    public void add(int index, E element) {
        //检查index是否越界
        rangeCheckForAdd(index);
        
        //确保数组有足够的容量,去存储新的数据
        ensureCapacityInternal(size + 1);  // Increments modCount!!

        //将elementData数组中index...size-1处的元素拷贝到index+1...size-index位置处。即从index处的元素向后移动一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }


在这里,ArrayList的add()操作时间复杂度为O(1)。确切的说应该是amortised constant time。可能你会认为,在数组扩充容量的时,需要把旧的数组拷贝到新的数组中,时间复杂度应该是O(N)。但是,你要知道,执行add很多次时,不能只关心它的最坏和最好的情况,你需要关心的是重复上百万次的总的时间。amortised constant time的意思就是说,你做了很多次操作后,每次操作的平均时间。
下面引用StackOverFlow上面的解释。

Amortised constant time

If you do an operation say a million times, you don't really care about the worst-case or the best-case of that operation - what you care about is how much time is taken in total when you repeat the operation a million times.

So it doesn't matter if the operation is very slow once in a while, as long as "once in a while" is rare enough for the slowness to be diluted away. Essentially amortised time means "average time taken per operation, if you do many operations". Amortised time doesn't have to be constant; you can have linear and logarithmic amortised time or whatever else.

Let's take mats' example of a dynamic array, to which you repeatedly add new items. Normally adding an item takes constant time (that is, O(1)). But each time the array is full, you allocate twice as much space, copy your data into the new region, and free the old space. Assuming allocates and frees run in constant time, this enlargement process takes O(n) time where n is the current size of the array.

So each time you enlarge, you take about twice as much time as the last enlarge. But you've also waited twice as long before doing it! The cost of each enlargement can thus be "spread out" among the insertions. This means that in the long term, the total time taken for adding m items to the array is O(m), and so the amortised time (i.e. time per insertion) is O(1).

修改 时间复杂度为O(N)

//将index处的元素更改为element
    public E set(int index, E element) {
        //判断index是否越界
        rangeCheck(index);

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

删除 时间复杂度为O(N)

remove(int index)

//删除特定位置的元素
    public E remove(int index) {
        //判断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; // Let gc do its work

        return oldValue;
    }

remove(Object o)

//删除某个元素
    public boolean remove(Object o) {
        //由此可知,ArrayList允许元素为null
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                //如果数组中存在o,则删除
                if (o.equals(elementData[index])) {
                    //删除index位置的元素
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // Let gc do its work
    }


//删除所有元素
public void clear() {
        modCount++;

        // Let gc do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }
查找
//如果元素o在数组中,则返回其下标,否则,返回-1.
    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;
    }

    //返回元素o在数组中的最后出现的下标,若数组中不存在元素o,则返回-1
    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

    //如果数组中包含元素o,则返回true,否则,返回false
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }


Fail-Fast机制

Java容器中有一种保护机制,能够防止多个进程同时修改同一个容器的内容。如果在你迭代遍历某个容器的过程中,另一个进程介入其中,并且插入、修改或者删除此容器内的某个对象,那么就会出现各种奇怪的问题。Java容器类使用fail-fast机制。它会探查容器上除了当前进程所进行的操作以外的所有的变化,一旦发生变化,就会立刻抛出ConcurrentModificationException异常。在程序中,使用modCount变量记录容器更改的次数。

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

推荐阅读更多精彩内容