数据程序员Java 杂谈

JAVA容器-自问自答学ArrayList

2017-08-15  本文已影响685人  liangzzz

前言

在之前的几篇文章里面,我主要都是推荐了一些工具类,为的就是让大家可以提高开发效率,但是我们在提高开发效率,也应该提高代码的执行效率,注重代码的质量。如何提高,其中的一个好办法就是阅读源码,知其然知其所以然。

下面我就以面试问答的形式学习我们的最常用的装载容器——ArrayList(源码分析基于JDK8)

问答内容

1.

问:ArrayList有用过吗?它是一个什么东西?可以用来干嘛?

答:有用过,ArrayList就是数组列表,主要用来装载数据,当我们装载的是基本类型的数据int,long,boolean,short,byte...的时候我们只能存储他们对应的包装类,它的主要底层实现是数组Object[] elementData。与它类似的是LinkedList,和LinkedList相比,它的查找和访问元素的速度较快,但新增,删除的速度较慢。

示例代码:

        // 创建一个ArrayList,如果没有指定初始大小,默认容器大小为10
        ArrayList<String> arrayList = new ArrayList<String>();
        // 往容器里面添加元素
        arrayList.add("张三");
        arrayList.add("李四");
        arrayList.add("王五");
        // 获取index下标为0的元素      张三
        String element = arrayList.get(0);
        // 删除index下标为1的元素      李四
        String removeElement = arrayList.remove(1);
ArrayList底层实现示意图
2.

问:您说它的底层实现是数组,但是数组的大小是定长的,如果我们不断的往里面添加数据的话,不会有问题吗?

答:ArrayList可以通过构造方法在初始化的时候指定底层数组的大小。

    // 定义ArrayList默认容量为10
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组,当调用无参构造方法时默认复制这个空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 真正保存数据的底层数组
    transient Object[] elementData; 

    // ArrayList的实际元素数量
    private int size;

    public ArrayList() {
        // 无参构造方法默认为空数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 通过构造方法出入指定的容量来设置默认底层数组大小 
    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);
        }
    }
    public boolean add(E e) {
        //确保底层数组容量,如果容量不足,则扩容
        ensureCapacityInternal(size + 1); 
        elementData[size++] = e;
        return true;
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 容量不足,则调用grow方法进行扩容
        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;
        // 判断是否超过最大长度限制
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 将原数组的数据复制至新数组, ArrayList的底层数组引用指向新数组
        // 如果数据量很大,重复扩容,则会影响效率
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    /**
     * 将底层数组一次性指定到指定容量的大小
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table 
             ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

    /**
     * 将容器最小化到存储元素容量
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
3.

问:那它是怎么样删除元素的?您上面说到ArrayList访问元素速度较快,但是新增和删除的速度较慢,为什么呢?

答:

示例代码:

    /**
     * 将元素添加至末尾
     */
    public boolean add(E e) {
        // 确保底层数组容量,如果容量不足,则扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    /**
     * 将元素添加至指定下标位置
     */
    public void add(int index, E element) {
         // 检查下标是否在合法范围内
        rangeCheckForAdd(index);
        // 确保底层数组容量,如果容量不足,则扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 将要添加的元素下标后的元素通过复制的方式逐一往后移动,腾出对应index下标的存储位置
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 将新增元素存储至指定下标索引index
        elementData[index] = element;
        // ArrayList的大小 + 1
        size++;
    }

    /**
     * 通过下标索引的方式删除元素
     */
    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);
        //将底层数组长度减1,并清空最后一个存储元素。
        elementData[--size] = null; // clear to let GC do its work
        // 返回移除元素
        return oldValue;
    }
4.

问:ArrayList是线程安全的吗?

答:ArrayList不是线程安全的,如果多个线程同时对同一个ArrayList更改数据的话,会导致数据不一致或者数据污染。如果出现线程不安全的操作时,ArrayList会尽可能的抛出ConcurrentModificationException防止数据异常,当我们在对一个ArrayList进行遍历时,在遍历期间,我们是不能对ArrayList进行添加,修改,删除等更改数据的操作的,否则也会抛出ConcurrentModificationException异常,此为fail-fast(快速失败)机制。从源码上分析,我们在add,remove,clear等更改ArrayList数据时,都会导致modCount的改变,当expectedModCount != modCount时,则抛出ConcurrentModificationException。如果想要线程安全,可以考虑使用Vector、CopyOnWriteArrayList。

示例代码:

    /**
     * AbstractList.Itr 的迭代器实现
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        //期望的modCount
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

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

总结

  1. 如果在初始化的时候知道ArrayList的初始容量,请一开始就指定容量ArrayList<String> list = new ArrayList<String>(20);,如果一开始不知道容量,中途才得知,请调用list.ensureCapacity(20);来扩充容量,如果数据已经添加完毕,但仍需要保存在内存中一段时间,请调用list.trimToSize()将容器最小化到存储元素容量,进而消除这些存储空间浪费。

  2. ArrayList是以1.5倍的容量去扩容的,如初始容量是10,则容量依次递增扩充为:15,22,33,49。扩容后把原始数据从旧数组复制至新数组中。

  3. ArrayList访问元素速度较快,下标方式访问元素,时间复杂度为O(1),添加与删除速度较慢,时间复杂度均为O(n)。

  4. ArrayList不是线程安全的,但是在发生并发行为时,它会尽可能的抛出ConcurrentModificationException,此为fail-fast机制。

上一篇下一篇

猜你喜欢

热点阅读