ArrayList源码分析

ArrayList是Java中常用的一个集合类,是List接口的一个实现类,而List接口继承自Collection接口,所以ArrayList是Collection的一个实现类。

本篇主要讨论一下ArrayList底层代码的实现。

核心的成员变量

先来看看ArrayList中几个核心的成员变量。

    //初始化数组的大小,ArrayList底层是基于数组实现的
    private static final int DEFAULT_CAPACITY = 10;
    //一个空数组对象,用于无参数的情况下初始化数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //底层存放数据的数组
    transient Object[] elementData; 
    //已存放的元素数量
    private int size;
    //定义数组的最大容量,实际数组的大小可以扩大到Integer.MAX_VALUE的大小,该字段应该是预留字段
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

默认无参的构造函数

    public ArrayList() {
        //这里首先初始化用于存放数据的数组,默认数组的大小为空
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

add方法实现

    //这里添加一个元素,添加成功后将size+1,并返回true
    public boolean add(E e) {
        //传入size+1,表示数组的容量应该满足最小的容量
        ensureCapacityInternal(size + 1);
        //将当前添加的元素存放到数组中
        elementData[size++] = e;
        return true;
    }
    
    //该方法用来确保当前数组的大小是否满足最小要求,并对数组扩容
    private void ensureCapacityInternal(int minCapacity) {
        //calculateCapacity方法传入存放数据的数组与该数组要求满足的最小容量,并返回设置的容量大小
        //ensureExplicitCapacity会判断当前数组的是否需要扩容
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //当数组的第一次使用时,返回初始化容量DEFAULT_CAPACITY=10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

   //判断当前数组的是否需要扩容
   private void ensureExplicitCapacity(int minCapacity) {
        //该变量为AbstractList中的成员变量,在用iterator迭代器获取数据的数据使用
        modCount++;
        //此处判断当前数组的容量是否满足要求
        if (minCapacity - elementData.length > 0)
            //调用该方法对数组进行扩容
            grow(minCapacity);
    }

    //实际进行数组扩容拷贝操作
    private void grow(int minCapacity) {
        //当前数组的容量大小
        int oldCapacity = elementData.length;
        //新数组的容量大小,oldCapacity >> 1实际为oldCapacity/2
        //这里也就是将数组的容量扩大1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //由于第一次调用该方法时,数组的容量大小为0,所以这里会将默认数组大小赋予newCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //当新数组的容量大小大于MAX_ARRAY_SIZE时,将数组的大小设置为Integer.MAX_VALUE
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

        //最后,将数组进行拷贝扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    //该方法返回数组的最大容量大小
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

具体步骤如下:

  • 当第一次调用add方法添加元素时,会对elementData数组进行扩容,默认大小为DEFAULT_CAPACITY=10;
  • 之后每一次调用add方法时,会对elementData数组的大小进行判断,判断当前容量是否满足最小要求;
  • 如果不满足,则调用grow方法,对数组的大小扩容为原来大小的1.5倍;
  • 当elementData数组的容量大小大于MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)时,将数组的容量大小设置为Integer.MAX_VALUE;
  • 添加成功后,对size加1,并对数组进行赋值数据,然后返回true;

带参数的构造函数

  //传入初始化数组大小的参数
  public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //初始化elementData数组大小
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

在add方法中,我们了解到,在添加元素的过程中,涉及到对元素进行扩容的操作,如果需要在数组中频繁添加大量元素,将会对elementData数组进行多次拷贝扩容操作,很消耗性能,所以,我们可以先预估数组的大小,并调用带参数的构造函数,传入初始化数组的大小的参数,可以避免在add元素的拷贝扩容操作,提升性能。

get方法实现

   //传入要获取数据的索引下标,返回对应位置的数据
   public E get(int index) {
        //检查该索引下标是否越界
        rangeCheck(index);

        return elementData(index);
    }

    //判断当前索引下标是否大于当前数组存放数据的大小,如果索引下标越界,则报IndexOutOfBoundsException异常
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
   
    //返回数组中对应索引下标位置的数据
    E elementData(int index) {
        return (E) elementData[index];
    }

get方法实现比较简单,步骤如下:

  • 首先对传入索引下标进行检查,如果下标越界,则抛出IndexOutOfBoundsException异常;
  • 如果该索引下标合法,则返回对应索引下标位置的数据;

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

        //因为数组向前移动了,所以将数组中最后一个元素的值设置为null
        elementData[--size] = null; 

        //返回删除了的元素
        return oldValue;
    }

具体步骤如下:

  • 检查指定索引下标是否合法,如不合法,抛出IndexOutOfBoundsException异常;
  • 获取指定索引下标的元素数据,用于删除后返回;
  • 删除数据是基于数组前移操作的,numMoved 为要前移元素的数量,假如当前数组为[1,2,3,4],那么数组的size为4,当要删除的index=1时,这里计算出来的numMoved=2,所以实际调用的是System.arraycopy([1,2,3,4], 2, [1,2,3,4], 1, 2),也就是从索引下标为2的位置开始后两个元素向前移动到下标为1的位置,移动后的结果为[1,3,4,4];
  • 将数组元素的最后一个值设置为null;
  • 返回删除了的元素;

remove(Object o)方法实现

  //删除某个元素,删除成功,返回true,否则返回false
  public boolean remove(Object o) {
       //如果要删除的对象为null,则删除数组第一个为null的元素
        if (o == null) {
            //遍历数组,获取出要删除的元素的下标
            for (int index = 0; index < size; index++)
                //查找出第一个元素为null的下标
                if (elementData[index] == null) {
                    //执行按照索引删除元素,实现方法与remove(int index)类似
                    fastRemove(index);
                    return true;
                }
        } else {

            //遍历数组,获取出要删除的元素的下标
            for (int index = 0; index < size; index++)
                //查找出要删除的元素的下标
                if (o.equals(elementData[index])) {
                     //执行按照索引删除元素,实现方法与remove(int index)类似
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
 
   //删除指定索引的元素,与remove(int index)方法逻辑类似
   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; // clear to let GC do its work
    }

具体步骤:

  • 判断要删除的元素是否为null,如果为null,则先遍历获取数组中第一个为null的索引下标,然后根据该索引下标删除元素,也就是删除数组中第一个为null的元素;
  • 如果要删除的元素不为空,则遍历获取第一个出现在数组中的索引下标,然后根据索引下标删除该元素,也就是删除第一个与元素匹配的数据;
  • 删除成功,返回true,否则,返回false;

适用场景

通过源码的分析,我们知道,arrayList底层是基于数组实现的,每个存储的元素,都拥有一个元素的下标,所以,当我们需要通过下标获取数据的时候,适合使用ArrayList来存取数据;而当在添加与删除元素的时候,可能会涉及到数组的扩容与拷贝,所以arrayList不适添加和删除比较多的场景。

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

推荐阅读更多精彩内容