Java集合之ArrayList总结

一、ArrayList 概述

       
         摘自JDKAPI-1.6版本部分解释:ArrayList 实现了 List 接口的所有可选实现,是一个“大小可变数组”的容器,允许 null 元素和重复元素,此容器的操作是不同步的(线程不安全的)。在添加大量元素之前,我们可以在程序中调用 ensureCapacity 方法来增加 ArrayList 容器底层数组的大小容量,这样可以减少 ArrayList 扩容的次数,从而提高效率。ArrayList 除了实现了 List 接口之外还实现了 RandomAccess 接口,实现此接口的容器表明其支持快速随机访问,如果列表很大时应该使用 for(int i = 0,n = list.size; i < n; i++)的方式进行遍历容器。RandomAccess 是一个标记接口,所谓的标记接口标识着你可以采用 if (list instanceof ArrayList) 来判断使用循环的方式。同时 ArrayList 也实现了可克隆、可序列化。

二、总结 ArrayList 的关键点


        1、ArrayList 是 大小可变的数组容器,按照放入元素的先后顺序排序,允许 null 元素,允许重复元素,多线程中不同步。

        2、ArrayList 使用 for ( int i = 0, n = list.size(); i < n, i++) 的方式遍历比使用迭代器的方式效率要高。

        3、实现了浅表克隆,可序列化。

三、结论分析

       
         如果你看过 ArrayList 的 JDKAPI 解释和源码的实现,一定会产生一些疑问,下面对 ArrayList 的一些疑问点进行详细分析。

        1、为什么 ArrayList 是大小可变的数组容器?

                ArrayList 有3个构造器,无参构造器初始化一个底层数组容量默认为 10 的 ArrayList,使用者也可以使用指定容量的构造器,还可以构造一个包含指定 collection 的元素的容器。查看 JDK 源码可以看到,ArrayList 有一个私有属性是Object[] elementData,指定容量便是指定这个数组的大小。

    private transient Object[] elementData;

                构造完 ArrayList 之后,使用 add 方法新增元素时首先调用了 ensureCapacity 方法,它的参数为 size + 1。其中 size 表示的是 ArrayList 这个容器的元素数量,并不是 elementData 这个数组的长度,我们看一下 ensureCapacity 做了一些什么事情。

    public void ensureCapacity(int minCapacity) {
        // 修改次数 + 1
        modCount++;
        // 获取 ArrayList 的元素数组的长度
        int oldCapacity = elementData.length;
        // 如果 size + 1 大于元素数组的长度,就进行扩容,否则就忽略
        if (minCapacity > oldCapacity) {
            // 用 oldData[] 指向 elementData 的引用,是为了防止数组在复制的过程中被 GC回收掉
            Object oldData[] = elementData;
            // 将原元素数组的长度扩大到 1.5倍 + 1
            int newCapacity = (oldCapacity * 3) / 2 + 1;
            if (newCapacity < minCapacity)
                newCapacity = minCapacity;
            // 进行扩容
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    }

                从代码上可以看出,每次新增的时候会判断 ArrayList 的 size + 1 的数量是否大于了 elementData 的长度,如果没有那就不进行扩容,否则在原数组长度的基础上扩大 1.5 倍 + 1,具体扩容的代码就是 Arrays.copyOf ( elementData,  newCapacity),那面再看一下 copyOf 这个方法。

    public static  T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }

    public static  T[] copyOf(U[] original, int newLength, Class newType) {
        // 如果源数组的类型是 Object 类型就按照 newLength 创建一个新的 Object 数组,否则创建一个和源数组类型一致的新数组
        T[] copy = ((Object) newType == (Object) Object[].class) ? (T[]) new Object[newLength]
                : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        /**
         * original 源数组
         * 0 源数组中开始复制的起始位置
         * copy 目标数组
         * 0 目标数组中开始被复制的起始位置
         * Math.min(original.length,newLength)要复制的数组元素的数量
         */
        System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
        return copy;
    }

                其中最后一句 System.arraycopy 的调用是本地方法,看不到源码,从其他代码中,我们可以得出一个结论: ArrayList 的扩容实际上是增加它内部的元素数组的长度,增加的方式就是将旧的数组复制到新的数组中去,这个新的数组的长度是旧数组的 1.5倍+1 。

        2、为什么 ArrayList 查询快,增删慢?

                ArrayList 是可变数组,可变的原因是创建了一个新的大于原旧数组的数组,将旧数组的数据复制到新数组中,这种操作是消耗性能的。根据 ArrayList 提供的构造器,我们在具体的业务编程中,如果大概知道 ArrayList 中将存储多少元素,那么可以通过构造器指定 elementData 数组的大小,避免扩容以提高效率。ArrayList 查询快,增删慢也是相对其他储存容器而言的,可以根据具体业务自己实现 List 容器。至于查询快,因为底层是数组的原因,就不多解释了。

        3、为什么 ArrayList 是可序列化的容器,但是 elementData 要用 transient 修饰。

                transient 用来表示一个域不是该对象序列化的一部分,当一个对象被序列化时,transient 修饰的变量的值是不包括在序列化的行为中的。elementData 是 ArrayList 具体存放元素的成员,按道理来讲也是应该被序列化的,否则岂不是将会丢失元素吗?这是因为 ArrayList 实现了两个方法:

    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out array length
        s.writeInt(elementData.length);

        // Write out all elements in the proper order.
        for (int i = 0; i < size; i++)
            s.writeObject(elementData[i]);

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

    }

    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in array length and allocate array
        int arrayLength = s.readInt();
        Object[] a = elementData = new Object[arrayLength];

        // Read in all elements in the proper order.
        for (int i = 0; i < size; i++)
            a[i] = s.readObject();
    }

                ArrayList 在序列化时会调用 writeObject ,直接将 size 和 element 写入 ObjectOutputStream,反序列化时调用 readObject ,从 ObjectInputStream 中获取 size 和 element ,再恢复到 elementData。这样做的好处在于因为 ArrayList 扩容的机制,elementData 的实际大小一般是大于 size 的,它通常会预留一些容量,等容量不足了再扩充容量。那么有些空间可能就没有实际存储元素,采用上诉方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。

        4、为什么 ArrayList 使用 for (int...) 循环比使用迭代器迭代的效率要高?

                ArrayList 是 RandomAccess 接口的子类,它用来表明其支持快速随机访问,在程序中可以使用 instanceof 判断 List 的类型来决定使用哪种迭代方式。ArrayList 本身是一个实现随机访问的容器而 LinkedList 是连续访问的数据存储。 ArrayList 底层是数组结构,使用索引获取元素有天然的优势。

总结

        
        作为一个开发人员,只是学会某个工具的使用方法,这并不完善。例如上面的分析,如果没有学习过源码,没有这些疑问,那么在编码时就不会考虑到效率问题,写出来的东西就是会比知道这些细节的同行差。如果你能知道扩容的原理,在知道存储量时你就能指定容量;如果你能知道 ArrayList 使用 for 循环比较快,那你就不会使用迭代器去迭代;如果你能知道 ArrayList 随机访问快,那么你就不会用其他容器进行随机访问。综上所述,要走的路还有很长,和别人不一样,就差这么一点点。

你可能感兴趣的:(源码解析总结)