文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java集合框架概览之ArrayList源码分析

2023-07-05 16:50

关注

今天小编给大家分享一下Java集合框架概览之ArrayList源码分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

一、从一段简单的代码入手

下面是一段简单的集合操作代码,实例化一个 ArrayList 集合并插入和获取元素的代码:

    public static void main(String[] args) {        // 实例化一个初始容量为5的 ArrayList 集合        List list = new ArrayList<String>(6);        // 向指定索引位置插入数据        list.add(1, "hello");// 代码行号:17        // 获取指定索引位置的数据        System.out.println(list.get(1));    }

小伙伴可以先思考一下执行的结果是什么?

好啦,揭晓谜底:

Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 1, Size: 0
    at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:665)
    at java.util.ArrayList.add(ArrayList.java:477)
    at com.example.qgdemo.studydemo.Test.Test2.main(Test2.java:17)

细心的小伙伴已经注意到了上面的那段代码有一行专门标注了行号,而执行的结果的异常行号刚好是我标注的那一行,不难得出 就是在:list.add(1, "hello");这一行就抛出了异常。那么问题到底出现在哪里了呢?

下面我们从这短短几行代码逐行深入源码去刨析,挖出隐藏宝藏。

二、初始化

ArrayList的初始化

先从集合的初始化入手:

List list = new ArrayList<String>(5);

上源码(硬菜):

    transient Object[] elementData; // non-private to simplify nested class access        private static final Object[] EMPTY_ELEMENTDATA = {};        public ArrayList(int initialCapacity) {        if (initialCapacity > 0) {            this.elementData = new Object[initialCapacity];        } else if (initialCapacity == 0) {            this.elementData = EMPTY_ELEMENTDATA;        } else {            throw new IllegalArgumentException("Illegal Capacity: "+                                               initialCapacity);        }    }

简单分析一下这段源码:

小贴士: 细心的小伙伴会注意到我们的 elementData成员变量使用了 transient 关键字修饰,这里简单科普一下:

被 transient 修饰的变量不能被序列化。

transient 只能作用于实现了 Serializable 接口的类当中。

transient 只能用来修饰普通成员变量字段。

分析到这里目前没有发现关于我们的问题的信息,我们继续往下看。

三、添加元素

ArrayList添加元素

现在到了我们的重头戏,从执行结果反馈来看,抛出异常的位置就在这:list.add(1, "hello");,让我们磨刀霍霍向源码一探究竟。

        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++; // 集合的实际大小累加    }         private int size;            private void rangeCheckForAdd(int index) {        if (index > size || index < 0)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));    }

源码刨析:

好了,虽然问题的根源找到了,但是源码我们还是要继续往下看的。

// 判断是否需要扩容    private void ensureCapacityInternal(int minCapacity) {        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));    }    private void ensureExplicitCapacity(int minCapacity) {        modCount++; // 修改次数累加        // overflow-conscious code        if (minCapacity - elementData.length > 0)            grow(minCapacity);    }        private void grow(int minCapacity) {        // overflow-conscious code        int oldCapacity = elementData.length;        int newCapacity = oldCapacity + (oldCapacity >> 1);// 将原容量扩容至原来的1.5倍,以本例来说就是扩容至:6+3=9        if (newCapacity - minCapacity < 0)            newCapacity = minCapacity; //取 newCapacity 和 minCapacity 的最大值赋值给 newCapacity,考虑了溢出的情况        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 final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;        private static int hugeCapacity(int minCapacity) {        if (minCapacity < 0) // overflow 溢出检查            throw new OutOfMemoryError();        return (minCapacity > MAX_ARRAY_SIZE) ? // 如果申请的最小容量比数组的容量上限还大则容量设置为:            Integer.MAX_VALUE : // Integer.MAX_VALUE,否则设置为:数组容量上限(MAX_ARRAY_SIZE)            MAX_ARRAY_SIZE;    }

源码刨析:

小贴士: 上面的:grow(int minCapacity)方法用到了移位运算符。 java中有三种移位运算符: << :左移运算符,num << 1,相当于num乘以2。 >> :右移运算符,num >> 1,相当于num除以2。 >>>:无符号右移,忽略符号位,空位都以0补齐。

确定了集合的新容量,接下来就需要将集合的旧数据拷贝到新数组当中:

        System.arraycopy(elementData, index, elementData, index + 1,                         size - index);//[#System] 调用了系统级的数组拷贝方法    public static native void arraycopy(Object src,  int  srcPos,                                        Object dest, int destPos,                                        int length);

源码刨析:

四、ArrayList获取元素

ArrayList 由于是基于数组来存储数据的,所以支持按指定下标来获取数据:

        public E get(int index) {        rangeCheck(index); // 索引越界检查        return elementData(index); // 按下标获取元素    }        private void rangeCheck(int index) {        if (index >= size)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));    }

源码刨析:

五、删除元素

ArrayList 主要提供了:指定下标删除,按元素删除,批量删除,特定条件删除,下标区间删除等方法。

5.1 指定下标删除

        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 将数组末尾空缺出来的位置引用置为null,便于GC        return oldValue;    }

源码刨析:

5.2 按元素删除

        public boolean remove(Object o) {        if (o == null) { // 如果待删除的元素为 null,则直接遍历数组元素和 null 进行匹配            for (int index = 0; index < size; index++)                if (elementData[index] == null) {                    fastRemove(index); // 执行删除操作                    return true;                }        } else {            for (int index = 0; index < size; index++)                if (o.equals(elementData[index])) { // 遍历匹配所有元素                    fastRemove(index);// 执行删除操作                    return true;                }        }        return false;    }        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,便于GC    }

源码刨析:

小贴士: 由上述源码可以得出一个结论:==如果你想删除一个 ArrayList 中的 所有 null 元素,调用一次 remove(null); 是无法删除全部的null元素的。==

5.3 批量删除

        public boolean removeAll(Collection<?> c) {        Objects.requireNonNull(c); // 对 c 集合进行空判断        return batchRemove(c, false); // 执行批量删除    }    // 批量删除方法    private boolean batchRemove(Collection<?> c, boolean complement) {        final Object[] elementData = this.elementData;        int r = 0, w = 0;        boolean modified = false; // 操作结果标识        try {        // 通过遍历原集合,将不符合删除条件的 [r] 位置的元素替换掉 [w] 位置的元素,并将 [w] 累加。            for (; r < size; r++) // 每次循环 r++                if (c.contains(elementData[r]) == complement) // 如果待删除的集合不包含有原集合的元素                    elementData[w++] = elementData[r]; // 则用原集合当前下标位置 [r] 的元素覆盖掉下标位置为 [w] 的元素                    // 并将 [w] 累加。        } finally {            // Preserve behavioral compatibility with AbstractCollection,            // even if c.contains() throws.            // 只有在抛出异常时:r != size ,那么此时需要将未比对的元素拼接在已经处理过的元素后面            if (r != size) {                System.arraycopy(elementData, r,                                 elementData, w,                                 size - r);                w += size - r; // 重新设置 [w] 的值,因为在下一步会将 [w] 之后的元素设置为null,此时的 [r] 为抛出异常位置            }            // 在极端情况下,如果待删除集合和原集合的元素完全无交集,则 `w == size`,这种情况下无需对原集合进行任何操作。            if (w != size) {                // clear to let GC do its work                for (int i = w; i < size; i++)                    elementData[i] = null; // 将 [w] 之后的元素全部赋值为 null。                modCount += size - w; // 对 modCount 进行重新设置                size = w;                modified = true;            }        }        return modified;    }    // 集合是否包含指定的元素    public boolean contains(Object o) {        return indexOf(o) >= 0; // 遍历集合查找指定元素    }

源码刨析:

5.4 特定条件删除

// 根据指定的过滤器判断匹配的元素是否在集合内:Predicate 接口主要用来判断一个参数是否符合要求。    public boolean removeIf(Predicate<? super E> filter) {        Objects.requireNonNull(filter); // 指定的过滤器的非null判断        // figure out which elements are to be removed 找出要删除的元素        // any exception thrown from the filter predicate at this stage 在这阶段抛出的任何异常都不会使得集合发生改变        // will leave the collection unmodified        int removeCount = 0; // 删除数目        final BitSet removeSet = new BitSet(size); // BitSet是一种特殊类型的数组来保存位值,其中数组大小会随需要增加        final int expectedModCount = modCount; // 记录修改次数        final int size = this.size;        // 这个循环主要是为了记录需要删除的元素数目        for (int i=0; modCount == expectedModCount && i < size; i++) {            @SuppressWarnings("unchecked")            final E element = (E) elementData[i];            if (filter.test(element)) {                removeSet.set(i); // 通过 removeSet 来记录需要删除的集合下标                removeCount++; // 删除数目进行累加            }        }        if (modCount != expectedModCount) { // 正常情况下这两个值应该是相等的,不相等说明有了并发修改,则抛出异常             throw new ConcurrentModificationException();        }        // shift surviving elements left over the spaces left by removed elements        // 通过遍历使用未删除的元素替换已删除元素,[i] 代表未删除的元素下标,[j] 代表被替换的元素下标        final boolean anyToRemove = removeCount > 0;        if (anyToRemove) {            final int newSize = size - removeCount; // 记录删除后的新的容量            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {                i = removeSet.nextClearBit(i);  // 找出未删除元素的下标 [i]                elementData[j] = elementData[i]; // 使用未删除的元素 [i] 替换对应位置 [j] 的元素            }            for (int k=newSize; k < size; k++) { // 将下标从 [k] 到之后的位置的元素赋值为null                elementData[k] = null;  // Let gc do its work            }            this.size = newSize;            if (modCount != expectedModCount) { // 出现并发修改时抛出异常                throw new ConcurrentModificationException();            }            modCount++; // 修改次数累加        }        return anyToRemove;    }

源码刨析:

小贴士: 对于这个方法使用这里给一个简单的例子: 假设有一个字符串集合 list:["Google","Runoob","Taobao","Facebook"],我们想删除所有带有 ”oo“的元素; 则可以:list.removeIf(e -> e.contains("oo"));,最终的集合就变为:["Taobao"]。

5.5 下标区间删除

        protected void removeRange(int fromIndex, int toIndex) {        modCount++; // 修改次数累加        int numMoved = size - toIndex; // 需要移动的元素数量,也就是待删除区间的末尾之后的元素数量。        System.arraycopy(elementData, toIndex, elementData, fromIndex,                         numMoved); // 用删除区间的末尾之后的元素拷贝至删除区间的元素进行覆盖。        // clear to let GC do its work        int newSize = size - (toIndex-fromIndex); // 计算新的集合的长度,方便后续进行多余数组元素的置空        for (int i = newSize; i < size; i++) {            elementData[i] = null; // 将后半部分的多余位置的元素赋值为 null,方便GC。        }        size = newSize; // 重新设置集合的大小    }

源码刨析:

以上就是“Java集合框架概览之ArrayList源码分析”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注编程网行业资讯频道。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯