Java ArrayList(源码刨析)

ArrayList可以称得上是集合框架方面最常用的类了,可以和HashMap一教高下。

从名字就可以看出来,ArrayList实现了List接口,并且是基于数组实现的。

数组的大小是固定的,一旦创建的时候指定了大小,就不能再调整了。也就是说,如果数组满了,就不能再添加任何元素了。ArrayList在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活。

Java这门编程语言和C语言的不同之处就在这里,C语言的话,你必须得手动实现自己的ArrayList,原生的库函数里面是没有的。

01、创建ArrayList

ArrayList<String> arrayList = new ArrayList<String>();

可以通过上面的语句来创建一个字符串类型的ArrayList(通过尖括号来限定ArrayList中元素的类型,如果尝试添加其他类型的元素,将会产生编译错误),更简单的写法如下。

List<String> arrayList = new ArrayList<>();

由于ArrayList实现了List接口,所以arrayList变量的类型可以是List类型;new关键字声明后的尖括号中可以不再指定元素的类型,编译器可以通过前面尖括号中的类型进行只能判断。

此时会调用无参构造函数创建一个空的数组,常量 DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值为{}

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

如果非常确定ArrayList中元素的个数,在创建时候还可以指定初始大小。

List<String> arrayList = new ArrayList<>(20);

这样做的好处是,可以有效地避免在添加新的元素时候进行不必要的扩容。

02、向ArrayList中添加元素

可以通过add()方法向ArrayList中添加一个元素。

arrayList.add("董李阳");

我们先来看看源码,看看add方法到底执行了哪些操作。在这个过程中,可以学习如何优雅的写代码。

先给结论:

堆栈过程图示:
add(element)
└── if (size == elementData.length) // 判断是否需要扩容
    ├── grow(minCapacity) // 扩容
    │   └── newCapacity = oldCapacity + (oldCapacity >> 1) // 计算新的数组容量
    │   └── Arrays.copyOf(elementData, newCapacity) // 创建新的数组
    ├── elementData[size++] = element; // 添加新元素
    └── return true; // 添加成功

来具体看一下,add()方法的源码

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
    /**
     * This helper method split out from add(E) to keep method
     * bytecode size under 35 (the -XX:MaxInlineSize default value),
     * which helps when add(E) is called in a C1-compiled loop.
     */
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

    private Object[] grow() {
        return grow(size + 1);
    }

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     * @throws OutOfMemoryError if minCapacity is less than zero
     */
    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //如果elementData不是默认的空数组并且,大于零。
                    int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
            //这里的首选增长和最小增长。里面的逻辑是谁大,就选谁扩容后大小=原有的+两个中最大的。
        } else {
            //这里是elementData是默认的空数组,那就new一个出来(默认容量和传进的容量进行比对)
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

翻译下:

elementData:存储ArrayList元素的数组缓冲区, ArrayList的容量是此数组缓冲区的长度。当添加第一个元素时,任何具有elementData == DEFAULTCAPCITY_empty_elementData的空ArrayList都将扩展为DEFAULT_CAPACITY.

非私有简化嵌套类访问

size:ArrayList的大小(它包含的元素数量)。

将指定的元素附加到此列表的末尾。

参数:是要附加到此列表的元素e。

返回值:操作成功与否的boolean。

此辅助方法从add(E)中分离出来,将方法字节码大小保持在35以下(-XX:MaxInlineSize 默认值),这有助于在C1编译循环中调用add(E)

增加容量,以确保它至少可以容纳由最小容量参数指定的*个元素。

参数e为要添加的元素,此时的值为“董李阳”,size为ArrayList的长度,此时为0.

当第一次执行的时候,会从DEFAULT_CAPACITY和1作比较。然后长度是10;

然后将元素放入elementData[0] ;然后size+1,变为1;

当进行第二次添加的时候,因为elementData.length长度为10,然后现在size为1,代表还有9个空,那么就不扩容,然后直接将元素放到elementData[1];然后size+1,变为2;

可以判断,当下一次进行扩容的时候,是添加第11个元素时,大家可以尝试分析以下这个过程。

03、右移操作符

在ArrayList操作中,扩容操作里面 oldCapacity>>1。

这里扩容时候,会 oldCapacity+max(minCapacity-oldCapacity,oldCapacity>>1)

在add操作中,等添加11元素时,10 + max(11-10,10/2)--->也就是15变为1.5倍。

并且是固定的,来进行分析,因为这里的minCapacity = size + 1,当需要扩容时候,size = oldCapacity。也就是说add这个方法,

左侧这里的总是1,右侧是原本值除2.

简单说 公式会变为 oldCapacity + oldCapacity/2 =1.5倍oldCapacity(因为可以验证右边这个值一定是比1大的,add这个方法,左侧插值只能是1)。

可以在ArrayList中添加第11个元素debug验证下。

04、向ArrayList的指定位置添加元素

除了 add(E e) 方法,还可以通过 add(int index, E element) 方法把元素添加到 ArrayList 的指定位置:

arrayList.add(0, "董李阳");

add(int index, E element) 方法的源码如下:

    public void add(int index, E element) {
        rangeCheckForAdd(index); //检查索引是否越界
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            //这里是当前的数组容量 == 已经填充的数组容量,也就是满了。
            //并且当第一次(数组容量是0,并且填充也是0,也满足情况)
            elementData = grow();
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);
        elementData[index] = element; //这里是将元素插入到指定位置。
        size = s + 1;
    }

两个流程是很相似的,这个是需要越界检查,然后扩容的机制是一样的,其余的都一样。

这里用到了一个非常重要的本地方法:System.arraycopy();  他会对数组进行复制(要插入位置上的元素往后复制--头插法)。

来品味下:

这是arraycopy()的语法:


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

在ArrayList.add(int index, E element)方法中,具体用法如下:

System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);

elementData:表示要复制的源数组,即ArrayList中的元素数组。

index:表示源数组中要复制的起始位置,即需要将index及后面的元素向后移动以为

elementData:表示要复制到的目标数组,即ArrayList中的元素数组。

index + 1:表示目标数组中复制的起始位置,即将 index 及其后面的元素向后移动一位后,应该插入到的位置。

size - index:表示要复制的元素个数,即需要将 index 及其后面的元素向后移动一位,需要移动的元素个数为 size - index。

05、更新ArrayList中的元素

可以使用set()方法来更改ArrayList中的元素,需要提供下标和新元素。

arrayList.set(0,"董李阳");

假设原本0位置上的元素为"董董李阳“,现在可以将其更新为”董李阳“。

来看一下set()方法的源码:

    public E set(int index, E element) {
        Objects.checkIndex(index, size);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

该方法先检查下标,看是否越界,然后替换新值,并返回旧值(有时候可以借助这个来完成一些操作)。

06、删除ArrayList中的元素

remove(int index)方法用于删除指定下标位置上的元素,remove(Object o)方法用于删除指定值的元素。

alist.remove(1);
alist.remove("董李阳");

来看看remove(int index)方法的源码:

    public E remove(int index) {
        Objects.checkIndex(index, size);//检查索引是否越界
        final Object[] es = elementData;

        @SuppressWarnings("unchecked") E oldValue = (E) es[index];//获取要删除的元素
        fastRemove(es, index);

        return oldValue;
    }
    private void fastRemove(Object[] es, int i) {
        modCount++;
        final int newSize;
        if ((newSize = size - 1) > i)
            //这里是判断,需要删除的元素,是不是在当前的末尾。
            //这里会优化需要删除的元素在末尾的情况.
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }

需要注意的是,在ArrayList中,删除元素时,需要将删除位置后面的元素向前移动一位,以填充删除位置留下的空隙。如果需要移动元素,则需要使用System.arraycopy方法将删除位置后面的元素向前移动一位。最后,将数组末尾的元素置为null,以便让垃圾回收机制回收该元素占用的空间。

再来看remove(Object o)方法的源码:

    public boolean remove(Object o) {
        final Object[] es = elementData;
        final int size = this.size;
        int i = 0;
        found: {
            if (o == null) {
                for (; i < size; i++)
                    if (es[i] == null)
                        break found;
            } else {
                for (; i < size; i++)
                    if (o.equals(es[i]))
                        break found;
            }
            return false;
        }
        fastRemove(es, i);
        return true;
    }

该方法通过遍历的方式找到要删除的元素,null的使用使用==操作符判断,非null的时候使用equals()方法,然后调用fastRemove()方法。

注意:

有相同元素时,只会删除第一个(因为break)。

判断两个元素是否相等。

这里是数组的复制和移动。

07、查找ArrayList中的元素

如果要正序查找一个元素,可以使用indexOf()方法;如果要倒序查找一个元素,可以使用lastIndexOf()方法。

arrayList.indexOf("董李阳");
arrayList.lastIndexOf("董李阳");

来看看indexOf()方法的源码:

    public int indexOf(Object o) {
        return indexOfRange(o, 0, size);
    }

    int indexOfRange(Object o, int start, int end) {
        Object[] es = elementData;
        if (o == null) { //如果要查找的元素是null
            for (int i = start; i < end; i++) { //遍历列表
                if (es[i] == null) {//如果找到了null元素
                    return i;//返回索引
                }
            }
        } else {//如果要查找到元素不是null
            for (int i = start; i < end; i++) {//遍历列表
                if (o.equals(es[i])) {//如果找到了要查找的元素
                    return i;//返回索引
                }
            }
        }
        return -1; //如果找不到要查找的元素,则返回 -1;
    }

如果元素为null的时候使用"=="操作符,否则使用equal()方法。

lastIndexOf()方法和indexOf()方法类似,不过遍历的时候,从最后开始。

    public int lastIndexOf(Object o) {
        return lastIndexOfRange(o, 0, size);
    }

    int lastIndexOfRange(Object o, int start, int end) {
        Object[] es = elementData;
        if (o == null) {
            for (int i = end - 1; i >= start; i--) {
                if (es[i] == null) {
                    return i;
                }
            }
        } else {
            for (int i = end - 1; i >= start; i--) {
                if (o.equals(es[i])) {
                    return i;
                }
            }
        }
        return -1;
    }

contains()方法可以判断ArrayList中是否包含某个元素,其内部就是通过indexOf()方法实现的。

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

这里前面的-1,和找到了的返回值就形成对比,借助这个差异,来判断有没有包含某个元素。

08、二分查找法

如果ArrayList中的元素是经过排序的,就可以使用二分查找法,效率更快。

Collections 类的sort()方法可以对ArrayList进行排序,该方法会按照字母顺序对String类型的列表进行排序。如果是自定义类型的列表,还可以指定Comparator进行排序。

List<String> copy = new ArrayList<>(alist);
copy.add("a");
copy.add("c");
copy.add("b");
copy.add("d");

Collections.sort(copy);
System.out.println(copy);

输出结果如下所示:

[a, b, c, d]

排序后就可以使用二分查找法了:

int index = Collections.binarySearch(copy, "b");

09、ArrayList增删改查的时间复杂度

最后,来简单总结一下ArrayList的时间复杂度吧,方便后面学习LinkedList时候对比。

1)查询

时间复杂度O(1),因为ArrayList内部使用数组来存储元素,所以可以直接根据索引来访问。

    public E get(int index) {
        Objects.checkIndex(index, size);
        return elementData(index);
    }
    E elementData(int index) {
        return (E) elementData[index];
    }

2)插入

添加一个元素(调用add()方法时)的时间复杂度最好情况为O(1),最坏情况为O(n)。

如果在列表末尾添加元素,时间复杂度为O(1)。

如果要在列表的中间或开头插入元素,则需要将插入位置之后的元素全部向后移动一位,时间复杂度为O(n).

3)删除

删除一个元素(调用remove(Object)方法时)的时间复杂度最好情况O(1),最坏情况O(n)。

如果要删除列表末尾的元素,时间复杂度为O(1)。

如果要删除列表中间或开头的元素,则需要将删除位置之后的元素全部向前移动一位,时间复杂度为O(n)。

4)修改

修改一个元素(调用set()方法时)于查询操作类似,可以直接根据索引来访问,时间按复杂度为O(1)。

    public E set(int index, E element) {
        Objects.checkIndex(index, size);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

10、总结

ArrayList,如果有个中文名的话,可以称作动态数组(可自动调整大小)。动态数组克服了静态数组的限制,更符合实际的开发需求。

需要掌握扩容机制,必须得读源码,这是你精进的机会。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值