• 浅析Java源码之ArrayList


      面试题经常会问到LinkedList与ArrayList的区别,与其背网上的废话,不如直接撸源码!

      文章源码来源于JRE1.8,java.util.ArrayList

      既然是浅析,就主要针对该数据结构的内部实现原理和部分主要方法做解释,至于I/O以及高级特性就暂时略过。

    变量/常量

      首先来看定义的(静态)变量:

    class ArrayList2<E>
        //extends AbstractList<E>
        //implements RandomAccess, Cloneable, java.io.Serializable 
    {
        private static final long serialVersionUID = 8683452581122892189L;
        private static final int DEFAULT_CAPACITY = 10;
        private static final Object[] EMPTY_ELEMENTDATA = {};
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        transient Object[] elementData; 
        private int size;
    }

      这里在一开始定义了6个变量,其中第一个跟序列化相关不用管,其余5个依次解释一下:

    DEFAULT_CAPACITY:代表容器ArrayList的初始化默认大小

    Object[] EMPTY_ELEMENTDATA:一个空数组,在某些方法调用后(例如removeAll)会用到

    Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默认空数组,未传参初始化时默认为这个

    Object[] elementData:保存着当前ArrayList的内容,该变量被标记为序列化忽略对象

    int size:很明显,当前ArrayList大小

      需要注意的是,其中几个被标记为static final变量。

     

    构造函数

      看完变量,接下来看构造函数部分,构造函数有3个重载版本,分别阐述如下。

    1、无参版本

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

      如果不传任何参数直接初始化一个ArrayList,会得到上面默认的空数组。 

    2、int版本

        public ArrayList(int initialCapacity) {
            // 正常情况会初始化一个指定大小的数组
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            }
            // 传0在实现上与不传是一样的
            else if (initialCapacity == 0) {
                this.elementData = EMPTY_ELEMENTDATA;
            } 
            // 乱传就抛异常
            else {
                throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
            }
        }

      这基本上最普遍的情况,可以看出内部实现就是普通的数组。

    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 {
                // 传空集合相当于空数组
                this.elementData = EMPTY_ELEMENTDATA;
            }
        }

      如果初始化传入一个集合,会将此集合作为ArrayList的初值。

      这里存在一个bug,即toArray方法返回的不一定是Object,虽然默认情况下是,但是如果被重写就不一定了。

      详细问题可见另一位的博客:http://blog.csdn.net/gulu_gulu_jp/article/details/51457492

      如果返回不是Object类型,会做向上转型。

    方法

      接下来看看常用的方法。

    首先是get/set方法:

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

      可以看出十分简单暴力,首先会进行范围检查,然后返回/设置对应index的元素。

      简单看一下rangeCheck:

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

      也十分简单,主要是判断所给的索引index是否大于数组的大小size,否则抛出异常。

      获取对应的值时,没有直接用elementData[index],而是用了一个方法elementData(),看着有点混,看一下方法定义:

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

      方法其实只是对取出来的值进行了类型转换,保证了返回类型的准确。

    接下来是add/remove方法

      这两个方法都有重载版本,但是并不复杂,而且都用的比较多。

      首先看add的一个参数版本,会在尾部插入给定元素。

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

      稍微讲下源码注释的modCount,这个变量来源于java.util.AbstractList,专门用来计算容器被改动的次数,对于我这种菜鸟使用者来说没啥用。

      这里会首先检测下容器的容量,然后在尾部加入元素,并将size加1。

      看看ensureCapacityInternal方法:

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

      原来这是一个皮包函数,当数组元素为空时,会进行参数修正,由于容器的默认大小为10,所以不会对10以下的容量进行检测。

      修正后,将10或者比10大的形参传入ensureExplicitCapacity进行检测:

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

      这看起来也像个皮包方法,不过并不是,如果当前容器大小已经达到上限,会调用grow进行扩容:

        private void grow(int minCapacity) {
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            elementData = Arrays.copyOf(elementData, newCapacity);
        }

      其实这里的形参名字我觉得不是特别好,应该叫最小所需容量,即minNeededCapacity。

      这里首先会获取当前容器大小,并进行扩容,这里的扩容是这样算的:

      oldCapacity + (oldCapacity >> 1)

      也就是如果之前为10,那么新容量为10 + Math.floor(10/2) = 15。

      得到新容量后,会与传进来的 所需容量进行对比,如果还不够,那就干脆取所需容量为新容量。

      第二个if是判断扩容后的容量是否大于最大(数组可达)整数,看下MAX_ARRAY_SIZE变量定义就明白了:

        /**
         * The maximum size of array to allocate.
         * Some VMs reserve some header words in an array.
         * Attempts to allocate larger arrays may result in
         * OutOfMemoryError: Requested array size exceeds VM limit
         */
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

      这里的注释有必要看一眼,简单讲就是有些JVM会在数组中加入一些东西,所以实际上数组大小是比理论上小一点的。这个很容易理解的,比如电脑硬盘,容量100G,可用容量其实会打个折扣,一个道理的。

      为了完整,所以也看一下hugeCapacity函数的内部实现:

        private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
        }

      参数检测挺好玩,内部使用的函数还怕传入负数。

      这里会将所需容量与最大可用安全容量作比较,如果实在没办法,就将容量设置为最大可用容量,至于这里会不会出问题我也不知道。

      回到grow方法,得到新的容量后,会调用Arrays.copyOf方法,这个方法是包内另一个类的方法,内部实现是调用System.arraycopy直接进行内存复制,效率很高,最后返回一个新数组,size为加大后的容量。

      

      接下来看第二个重载的add方法:

        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 void rangeCheckForAdd(int index) {
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }

      由于这个重载方法是插入,所以需要进行数值检测,如果插入索引大于数组大小或者小于0,抛个异常。

      接下来是常规的容量检测。

      下一步的方法就是之前提到的System.arraycopy,该方法会将索引+1后面的元素全部复制到源数组,举个简单的例子:

      如果原数组为[1,2,3,4],假设索引为1,经过这一步,数组会变为[1,2,2,3,4]。

      最后是将对应索引的值赋为给定值,size++。

      可以看出,在数组中间插入一个元素是非常耗时的,会变动索引后面的每一个数组元素。

      接下来是remove,这个方法也有2个重载,一个是删除给定索引,一个是删除给定元素:

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

      没啥好讲的,忽略检测,一句话概括就是将对应索引-1的所有元素复制到原数组,然后size-1,并将末尾元素置null让GC进行回收,最后返回删除元素。

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

      这个是第二个重载,对null进行了特殊处理,这个奇怪的东西只能用==来进行比较。

      总体来讲就是遍历数组,如果找到了匹配元素,进行fastRemove,删除成功返回true,否则返回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
        }

      跟第一个重载的remove很相似,只是移除了范围检测与返回值的处理,更快一些。

      其余的方法大多数是上面的变种,没什么研究的必要了,有兴趣的可以自行阅读源码。

  • 相关阅读:
    Object C学习笔记2-NSLog 格式化输出数据
    NSPoint 位置
    NSNull空值
    工商银行卡 安全码是什么
    查看苹果开发者账号类型
    IOS中录音后再播放声音太小问题解决
    解决RegexKitLite导入报错问题
    iOS开发--OC常见报错
    UIImagePickerController
    UIPageViewController
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/7750097.html
Copyright © 2020-2023  润新知