ArrayList(一)

1. 概述

ArrayList是平常工作中非常常用的集合类之一,说起这个类,我就想起了我大学时期找实习的时候,经常被问起ArrayList与其他集合类的区别,我总是会像背书一样的说,ArrayList是基于数组,可添加重复元素,有序的,随机查找快但删除慢,那今天我们就从源码的角度分析一下,ArrayList为什么会具有这些特点.


2.源码分析(JDK1.8)

注:下面出现在代码全都出自JDK1.8的ArrayList源码,为了方便理解,所以拆分开来分析

首先,ArrayList中有两个非常重要的成员变量:

    // 实际存放数据的结构,从这里我们可以看出,ArrayList确实是基于数组的
    transient Object[] elementData;

    // ArrayList的大小,大家经常调用的size()方法就直接返回该值
    private int size;

    public int size() {
        return size;
    }

看到这里我想大家一定有一个疑惑,平时我们实例化一个数组时,必须在实例化时指定数组的长度,并且长度不能改变,那么ArrayList基于数组,为什么可以做到一直添加呢?难道ArrayList中实例化了一个超大的数组吗?当然是不可能的了,看完add方法的代码后,我们就会理解,在此之前先让我们来看看它的构造方法:

    // 无参构造方法,直接将一个静态的空数组赋值给elementData
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 传入一个数字为初始化数组的大小,在可以估算出该List大小的情况下,推荐使用这种方法,理由下面会讲
    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);
        }
    }
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 该构造方法主要作用是将其他的集合类型(如List家族、Set家族等)转换成ArrayList
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

看完了构造方法,我们现在来看看add方法来解除我们刚开始的疑惑:

    public boolean add(E e) {
        // 该方法判断数组长度是否足够,参数含义为需要的最小数组长度
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 将元素添加进数组,并将size加1
        elementData[size++] = e;
        return true;
    }

    private static final int DEFAULT_CAPACITY = 10;
    private void ensureCapacityInternal(int minCapacity) {
        // 若该List是无参构造方法实例化,并且第一次添加元素,则取需要的最小数组长度和默认长度10中较大的值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        // 操作次数加1,作用于fast-fail机制,这里暂时可不关注,不是本文的重点
        modCount++;

        // 终于到了重点的地方,如果需要的最小数组长度比当前数组长度大,则需要对当前数组进行扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // 将数组长度扩容为之前的1.5倍,若扩容后还是达不到需要的最小数组长度,则直接扩容到最小数组长度
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 若扩容后的长度大于MAX_ARRAY_SIZE,则需要判断是否溢出了
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 真正的扩容操作,其实就是实例化一个更大的数组,然后将原数组中的所有值复制到新数组中(还记得之前说过如果能估算出ArrayList的大小,推荐使用给定长度的构造方法吗?原因就在这里,如果不指定大小,而一直添加元素的话,会不断的扩容,不断的复制数组到新数组,影响性能)
        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 :
            MAX_ARRAY_SIZE;
    }

看到这里,我想大家都明白了概述中一开始提到的一些ArrayList的特点:
1.基于数组 : elementData成员变量
2.可添加重复元素 : 我们在添加时,并没有看到检测重复元素的代码,所以可以添加重复元素
3.有序的 : 既然是基于数组,那肯定是有序的,数组天然就是有顺序的

还有一点随机查找快但删除慢,好像还不能从以上代码中提现出来,我们继续看get()方法:

    public E get(int index) {
        // 检测index是否越界
        rangeCheck(index);

        // 返回该索引数组元素
        return elementData(index);
    }

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    E elementData(int index) {
        return (E) elementData[index];
    }

get()方法的代码中我们可以看出,由于是基于数组,所以查找值直接是返回索引处的元素,时间复杂度为O(1),效率非常高.

至于删除慢我们来看一下remove()方法的代码,remove方法有两种,一种是根据索引删除,一种是根据元素删除,我们先来看第一种,根据索引删除:

    public E remove(int index) {
        // 检测索引越界
        rangeCheck(index);

        modCount++;
        // 被移除的元素,需要作为方法的返回值
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 将index+1以及之后的元素复制到index以及之后,效果相当于将index之后的元素都向前移动了一位,这里就可以解释移除慢的特点了,因为当ArrayList中元素非常多的时候,这样的复制是很慢的
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);

        // 帮助GC回收垃圾
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

再来看另一种,根据元素删除:

    // 该方法若移除成功返回true,否则返回false
    public boolean remove(Object o) {
        if (o == null) {
            // 从初始位置遍历每个元素,找到和传入元素相同的,调用fastRemove方法进行移除
            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
    }

从以上代码可以看出,根据元素移除元素,需要先找到该元素索引,再根据索引去复制覆盖进行移除.


3.总结

经过分析,我们从原理上理解了ArrayList为什么会有基于数组,可添加重复元素,有序的,随机查找快但删除慢这些特性,其实ArrayList中还有很多其他的方法,例如:
1. public int indexOf(Object o) : 返回元素o在数组中的索引,从0开始遍历
2. public int lastIndexOf(Object o) : 返回元素o在数组中的索引,从size处开始遍历
3. public void trimToSize() : 将elementData长度缩短到size大小.当我们将一个ArrayList扩容到很大,但是之后又将其中的元素remove了,这时,实际的数组长度很大,而存储的元素很少,就很浪费空间,这时可以调用一下这个方法,将其实际数组长度缩小.
4. public void add(int index, E element) : 在指定的位置添加一个元素,这个方法其实效率也并不是很高,因为从之前的源码分析中我们可以看出,在数组中间添加一个元素,肯定要将其之后的元素向后移动一位,也就需要进行复制操作.

还有一些方法这里就不一一列举了,感兴趣的同学可以自己去看看源码.

(完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值