从源码看集合ArrayList

     可能大家都知道,java中的ArrayList类,是一个泛型集合类,可以存储指定类型的数据集合,也知道可以使用get(index)方法通过索引来获取数据,或者使用for each 遍历输出集合中的内容,但是大家可能对其中的具体的方法是怎么实现的不大了解,本篇就将从jdk源码的角度看看什么是动态扩容数组(毕竟我们不应该停留在会用的层面上)。本篇主要从以下几个角度看看ArrayList:

  • add及其重载方法是如何实现的
  • remove及其重载方法是如何实现的
  • 迭代器的本质及实现的基本原理

一、add方法添加元素到集合中
     实际上ArrayList内部是用的 transient Object[] elementData;这么一条语句定义的一个Object类型的数组,因为我们知道数组一旦被初始化长度就不能再发生改变,那我们的ArrayList是怎么做到可以不断的添加元素到集合中的呢?其实就是通过创建新的数组,将原来的数组中的内容转移到新的数组中来,实现动态扩容。具体的我们看源码:

public static void main(String[] args){
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);
    }
/*这是最简单的add方法*/
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

     当调用此add方法时,将指定了类型的数据传入(变量e接受),首先执行第一条语句:ensureCapacityInternal(size + 1);,这条语句实际上就是用来判断size+1之后是否会导致原数组长度溢出,如果会就扩充数组容量,如果没有就什么也不做。我们看看ensureCapacityInternal方法内部源码:

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
    /*ensureExplicitCapacity*/
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

     首先判断当前数组是否为空,默认数组长度为DEFAULT_CAPACITY=10,minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);表示:如果数组还未初始化即刚刚声明并未做任何操作,就取10作为数据容量值,然后调用方法ensureExplicitCapacity(minCapacity);设置数组长度。
     接受过传入的数据容量值,执行modCount++;增加修改次数(后文会说为什么有这个计数器),判断数据容量值是否比现数组长度大,如果数据容量值超过现有数组长度(需要扩容),执行:grow(minCapacity);,我们可以看看他是怎么进行扩容的。

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

     这条语句 int newCapacity = oldCapacity + (oldCapacity >> 1);通过位右移将新数组容量扩充为原来的1.5倍。数组的每次扩容都是,扩充为原来的1.5倍。下面是一系列的判断,最终确定新数组的长度,调用Arrays.copyOf方法,新建数组并且转移原数组内容。再往下,就不深究了。。
     最后小结一下整个过程,调用add 方法首先调用ensureCapacityInternal方法,如果原数组是空的就将10作为数据容量值,然后判断数据容量值是否大于当前数组长度(如果当前数组是空数组的话,自然长度为0),然后进行扩充数组容量,创建新数组返回。如果原数组非空,将判断数据容量值是否大于现数组长度,否说明添加此新元素之后数据量长度仍然小于数组长度(数组长度足够),是就会调用grow方法创建新数组赋值elementData数组。
     add的另一个比较麻烦的方法是,addAll方法,其他的重载方法类似,本篇不再赘述。下面我们一起看看addAll方法原理。

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

      addAll()方法的动态扩容和添加数值都和add 类似,我们主要理解一下,他的这个参数是什么意思,也顺便复习一下泛型相关内容。
这里写图片描述

     大家知道Collection<? extends E>作为类型,有哪些类型可以作为形参传入?假如E是Number类型,Collection<Integer>,Collection<Float>,Collection<Double>,都是可以作为形参传入的。而所有继承Collection接口的类也可以作为形参传入,例如:List<Integer>,Set<Integer>,List<Double>,ArrayList<Integer>,等等,但在本方法中是需要调用toArray这个具体的方法的,所以只能使具体类作为形参传入,这样就保证,形参是可以是任意类型的集合(前提是此类型必须继承与我们指定的E)。
二、Remove方法的实现原理
     既然集合是可以添加元素的,自然也是可以删除元素的,接下来我们一起看看ArrayList的Remove方法。

/*根据集合索引删除任意位置的元素*/
 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;
    }

     第一行代码很简单,rangeCheck(index);,检查指定索引是否越界,如果越界抛出异常。然后计算出,移除后的数据容量,因为经过判断index是<size的,也就是说numMoved >=0。判断是否大于0,如果等于0表示原来就一个数据,直接将其赋值null交给GC回收即可。如果大于0,执行System.arraycopy方法,因为此方法为native方法,我们不得而知它是如何实现的,但是我们可以大致猜出他是这样实现的:以索引位置开始,索引位置后面的数组元素向前覆盖。例如:index=3;elementData[3]=elementData[4],elementData[4]=elementData[5]等等。最后将最后位置的元素赋值为null。
     以上便是remove方法的简单原理,至于其他重载与上述类似。接下来,我们看看重要的迭代器。
三、迭代器

public interface Iterator<E> {
 boolean hasNext();
 E next();
 default void remove() {
        throw new UnsupportedOperationException("remove");
    }
 default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

     我们把接口 Iterator<E>叫做迭代器接口。通过反复调用next方法可以访问到所有的元素,当访问到最后一个元素的下一的位置时,就会抛出异常,所以我们常常在调用next方法之前调用hasNext方法判断是否还有下一个元素,remove方法表示删除元素(一个要求,调用remove方法之前一定要先调用next方法,这一点下文说)
     了解完 Iterator<E>,我们看看另一个和它相关的接口,Iterable<E>:

public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
 default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

     这个接口 Iterable<E>表示可迭代,强调了可以迭代的这种能力。声明一个方法 iterator();返回 Iterable<E> 迭代器接口,所有实现了 Iterable<E>接口的类都是可以使用for each 循环遍历集合中元素的。当我们的类实现 Iterable<E>接口时,可以使用for each 循环集合,其实内部还是,通过调用方法 iterator()实现当前集合和迭代器的一种类似于绑定的过程,最终返回迭代器接口,实际上for each 语法还是调用的是 迭代器接口中声明的方法 类似:

ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
Iterator<Integer> i = list.interator();
while(i.hasNext()){
    System.out.println(i.next);
}
/*for each 语句的本质其实就是这样*/

     下面要说的关于迭代器的一个重要的特性,迭代器的结构不可破坏性。就是说,在进行迭代的过程中,是不允许改变原集合的结构性的,集合的结构性就是指:对集合进行添加(add),删除(remove)。对集合的修改操作不属于破坏集合的结构性。例如:

for(Integer a : list){
    if(a == 3){
        list.remove(a);  //throw exception
    }
}
//破坏了集合的结构性,不允许的。

     要想解决这个问题就要看看ArrayList中是怎么实现迭代器的。实际上是通过内部类来实现迭代器接口的。

public Iterator<E> iterator() {
        return new Itr();
    }
//内部类,我们只看其中remove方法
private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
   //remove方法
    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();
            }
        }

     我们之所以在for each循环中不能破坏结构性,是因为for each每次调用next方法时,都会检查是否破坏了结构性,而这种检查就是依靠modCount 这个变量,通过对比前后的修改次数得出是否破坏了结构性,在我们的remove方法中,调用了外部类remove方法删除元素,并且 expectedModCount = modCount; 更新了修改次数变量,使得下次检查时,不会出现结构性破坏。

Iterator<Integer> it = list.interator();
while(it.hasNext()){
    if(it.next().equals(1)){
        it.remove();
    }
}

     最后想要强调一点的是,迭代器中调用remove方法之前一定要,调用next方法,例如:

Iterator<Integer> it = list.interator();
while(it.hasNext()){
    it.remove();
}//报错

     现在大家能够想明白为什么在调用remove方法之前一定要调用next方法了吧,因为next方法为lastRet和cursor重置数值,如果没有next方法,lastRet为 -1 自然是不能用作删除的。
     本篇就此结束,如果文中有博主说的不清楚的地方,望大家指出!

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

推荐阅读更多精彩内容