2023-01-14  阅读(10)
原文作者: HelloWorld_EE 原文地址:https://blog.csdn.net/u010412719/category_6159934_2.html

《JAVA源码分析》:ArrayList

ArrayList继承体系结构如下:

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

ArrayList类主要是继承AbstractList类并实现了List接口,实现Cloneable和Serializable接口使得ArrayList具有克隆和序列化的功能。

下面将从构造函数开始介绍,之后介绍一些我们平常使用的关于ArrayList类的一些方法。

属性

ArrayList类中主要有两个属性,如下:

    transient Object[] elementData;

elementData数组用来存储ArrayList中的元素,从这个可以看出,ArrayList是底层是借组于数组来实现的。

    private int size;

此属性用来记录ArrayList中存储的元素的个数。

ArrayList中还有一个默认容量大小属性以及两个空数组属性。它们的声明如下:

1、默认初始容量

    private static final int DEFAULT_CAPACITY = 10;

2、下面两个是共享空常量数组,用于空的实例对象,第二个与第一个的区别文档上面是这么说的: We distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when first element is added. , 翻译一下就是与EMPTY_ELEMENTDATA数组的区别在于当第一个元素被加入进来的时候它知道如何扩张;在源码中函数add(E e)中的第一行代码中的所在的函数就是这句话的实现。

    private static final Object[] EMPTY_ELEMENTDATA = {};
        //下面这个是共享空常量数组
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

构造函数

ArrayList类共有3个构造函数,下面一一进行介绍。

1、无参构造函数

    public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }

源码上介绍的功能为:构造一个初始容量为 10 的空列表。

即当我们不提供参数而new一个对象时,底层的数组就是直接用长度为10的空常量数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA进行实例化。可能有的人会问,此时DEFAULTCAPACITY_EMPTY_ELEMENTDATA的长度我们还不知道呀,从哪里可以看到是构造了一个初始容量为10的空列表呢?这里我们先不解释,在下面的add(E e)函数源码的介绍中会给出答案。

2、指定容量作为参数的构造函数

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

从源码可以看到,就是根据参数的大小作为容量来实例化底层的数组对象,其中对参数的3中情况进行了处理。当参数小于0时,抛异常。当参数等于0时,用空的常量数组对象EMPTY_ELEMENTDATA来初始化底层数组elementData。

3、Collection作为参数的构造函数

    public ArrayList(Collection<? extends E> c) {
            elementData = c.toArray();
            if ((size = elementData.length) != 0) {
                // c.toArray might (incorrectly) not return Object[] (see 6260652)
                if (elementData.getClass() != Object[].class)
                    elementData = Arrays.copyOf(elementData, size, Object[].class);
            } else {
                // replace with empty array.
                this.elementData = EMPTY_ELEMENTDATA;
            }
        }

从源码可以看到,将容器Collection转化为数组赋给数组elementData,还对Collection转化是否转化为了Object[]进行了检查判断。如果Collection为空,则就将空的常量数组对象EMPTY_ELEMENTDATA赋给了elementData;

ArrayList类的3个构造函数就介绍到这里了。下面我们就介绍一些平时我们常用的方法是如何实现的。

add

我们一般就是用ArrayList对象来存储数据,因此,首先要介绍的当仁不让就是add函数了。add有两种形式,下面将分别进行介绍。

1、add(E e)

函数功能:将制定的元素加入到List的末尾。

源码如下:

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }

既然此函数中涉及到ensureCapacityInternal函数,那我们就看一下这个函数,源码如下:

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

从源码可以看出,如果elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则就用默认容量10来进行开辟空间。这里的源码就解释了DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组和EMPTY_ELEMENTDATA数组的区别之所在。也给出了当我们用无参构造函数来实例化一个对象时,确实是构造的一个长度为10的数组对象。

上面代码

    private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

剩余的部分下次再来分析,今天练了一天车,好累,睡了

今天教研室春游,玩了一天,还不累,所以就来完成这篇博客了,2016年4月10日22:41:04

接上文

从源码可以看出,如果elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则就用默认容量10来进行开辟空间。这里的源码就解释了DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组和EMPTY_ELEMENTDATA数组的区别之所在。也给出了当我们用无参构造函数来实例化一个对象时,确实是构造的一个长度为10的数组对象。

上面代码

    private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

看源码我一般喜欢跟着路线一直跟下去,既然上面的函数中提到了grow这个函数,下面就看下这个函数的源码,如下:

    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            /*
            下面两个if的作用为处理两种情况:
            1)第一种情况为:如果newCapacity扩展的过小。则应该至少扩张到所需的空间大小minCapacity
            2)第二种情况为:newCapacity扩张的过大,如果过大,则用Integer.MAX_VALUE来代替。
    
             */
            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);
        }

从源码中可以看到,此函数的功能就是一个数组的扩张。一般情况下是扩展到原来数组长度的1.5倍。
但是,由于扩张1.5倍可能和我们的需要不一致,即可能太小,也可能太大。因此,就有了源码中的两个if条件的处理。即如果扩张1.5倍太小,则就用我们需要的空间大小minCapacity来进行扩张。如果扩张1.5倍太大或者是我们所需的空间大小minCapacity太大,则进行Integer.MAX_VALUE来进行扩张。

上面grow函数中最后一条语句elementData = Arrays.copyOf(elementData, newCapacity);的功能就是将原来的数组中的元素复制扩展到大小为newCapacity的新数组中,并返回这个新数组。Arrays.copyOf(elementData, newCapacity)的源码如下:

    public static int[] copyOf(int[] original, int newLength) {
            int[] copy = new int[newLength];
            System.arraycopy(original, 0, copy, 0,
                             Math.min(original.length, newLength));
            return copy;
        }

此函数源码实现比较简单,功能上面也进行了描述。

而此时,我们又看到了另外一个类System中的arraycopy这个函数,下面我们也看下源码具体是怎么实现拷贝的。文档上此函数的说明如下:

    static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
          从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。

原想看下这个函数的源码的,发现这个函数时一个原生态(即native)的方法,在System中的声明如下:

    public static native void arraycopy(Object src,  int  srcPos,
                                            Object dest, int destPos,
                                            int length);

到这里我们就把add(E e)函数及与此函数相关的函数的源码都看了下,这些我们就应该比较清晰这个函数的具体实现过程以及其中涉及到的一些细节。

2、add(int index,E element)

    public void add(int index, E element) {
        //位置有效性检查
        rangeCheckForAdd(index);
        //添加修改次数以及判断是否需要扩张数组长度,
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);//完成数组自身从index开始的所有元素拷贝到index+1开始且长度为size-index的位置上。
        elementData[index] = element;
        size++;
    }

看了add(E e)这个函数的实现过程之后,这个函数的实现过程也就相当容易理解了。上面讲解了增加元素,这个就介绍如何从ArrayList中获取元素。

3、addAll(Collection

    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);//将a中的所有元素拷贝到数组elementData最后一个元素的后面.
            size += numNew;
            return numNew != 0;
        }

4、addAll(int index, Collection

    public boolean addAll(int index, Collection<? extends E> c) {
            rangeCheckForAdd(index);
    
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacityInternal(size + numNew);  // Increments modCount
    
            int numMoved = size - index;
            if (numMoved > 0)
                System.arraycopy(elementData, index, elementData, index + numNew,
                                 numMoved);
    
            System.arraycopy(a, 0, elementData, index, numNew);
            size += numNew;
            return numNew != 0;
        }

get(int index)

源码如下:

    public E get(int index) {
            rangeCheck(index);//有效性检查
    
            return elementData(index);
        }

由于ArrayList底层是借助于数组来实现,因此,从ArrayList中获取元素就相当简单了。直接利用了数组随机访问能力强的特点。

set(int index,E element)

这个是改变ArrayList对象中某个位置元素的值的大小。源码如下:相信看到源码我们都能够看到他们它是如何实现的,相当简单哈。

    public E set(int index, E element) {
            rangeCheck(index);
    
            E oldValue = elementData(index);
            elementData[index] = element;
            return oldValue;
        }

remove:移除元素

移除ArrayList中的移除元素,包括两个函数的重载,一个是移除指定位置的元素,另一个是移除指定值的元素。下面分别进行介绍。

remove(int index):移除指定位置的元素

此函数的功能是:移除ArrayList对象中index位置的元素。

有了上面add(int index,E e)的实现思路,我们不难猜测其实现思路是:自身拷贝(即将数组从index+1位置开始到末尾的元素拷贝到从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);
            elementData[--size] = null; // clear to let GC do its work
    
            return oldValue;
        }

remove(Object o):移除指定值的元素

源码如下:

    public boolean remove(Object o) {
            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++)
                    if (o.equals(elementData[index])) {
                        fastRemove(index);
                        return true;
                    }
            }
            return false;
        }

从源码中可以看到,无论是指定对象o是否为null,都是在ArrayList中找到与此第一个相等的元素的位置,然后调用fastRemove(index)来进行移除;如果没有找打指定对象o的位置,则返回false,表示没有移除成功。

既然借助了fastRemove(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
        }

原以为fastRemove(int index)这个函数与remove(int index)会有很大的不同,看到源码之后,发现其实区别真心不大,唯一的区别是fastRemove函数没有对index进行有效性检查,以及没有返回移除的旧值,为什么不返回呢?这是因为remove(Object o)给出的就是要删除的值,因此不返回旧值是非常正常的。

clear

最后要介绍的函数就是这个了,清除ArrayList中所有的元素。

直接将数组中的所有元素设置为null即可,这样便于垃圾回收。

    public void clear() {
            modCount++;
    
            // clear to let GC do its work
            for (int i = 0; i < size; i++)
                elementData[i] = null;
    
            size = 0;
        }

到这里就将ArrayList源码进行了一个简单的分析和介绍。

小结

以前看过几次JAVA关于集合的源码,但是都没有写成博客,现在又在重新看一篇集合的源码,因此,还是想写成博客的形式,加深自己对这些源码的理解。


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文