读懂ArrayList这一篇就够了

微信公众号Duffy说码 若是你以为这篇文章对你有帮助,欢迎关注java

注:本文全部代码来自JDK1.8面试

ArrayList简介

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable 复制代码

ArrayList继承了AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable接口api

java.lang.Object
   java.util.AbstractCollection<E>
      java.util.AbstractList<E>
         java.util.ArrayList<E>
复制代码

List是动态数组,并容许操做全部元素,包括null。 size,isEmpty,get,set,iterator和listIterator方法都是以恒定时间运行。数组

每一个ArrayList实例都有一个容量大小。此容量是列表数组中可以存储元素的个数。它始终是大于等于元素个数的。当ArrayList中添加元素时,其容量会自动增长。在经过ensureCapacity方法添加大量元素以前,能够手动设置ArrayList实例的容量,这样会减小从新分配的次数。安全

请注意,ArrayList实现是不一样步。若是多个线程同时访问ArrayList实例,而且至少有一个线程在结构上修改了列表,则必须进行同步。 (结构修改是指添加或删除一个或多个元素的操做,或显式调整后备数组的大小;仅设置元素的值不是结构修改。)这一般经过同步一些天然封装的对象来实现。名单。若是不存在此类对象,则应使用Collections.synchronizedList方法“包装”该列表。bash

经常使用函数

面试常问问题

一、如何扩容的?微信

二、线程安全吗?如何确保线程安全数据结构

三、如何初始化?多线程

四、查找、添加、删除元素时间复杂度?并发

五、ArrayList与LinkedList的比较

源码分析

/** * 默认初始化容量 */
    private static final int DEFAULT_CAPACITY = 10;

    /** * 用于没有实例的共享空数组实例 */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /** * 用于默认大小空实例的共享空数组实例 * 区别于EMPTY_ELEMENTDATA, 当第一个元素添加进来可知该如何膨胀 */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /** * ArrayList的底层数据结构 * 任何空ArrayList元素类型为DEFAULTCAPACITY_EMPTY_ELEMENTDATA * 当第一个元素添加进来将扩展为DEFAULT_CAPACITY * */
    transient Object[] elementData; //非私有,以简化嵌套类访问
复制代码

注意:transient关键字是指在采用Java默认的序列化机制的时候,被该关键字修饰的属性不会被序列化,说明此属性不采用默认序列化方法,由于本身实现了序列化和反序列化(writeObjectreadObject)。

/**
     * 包含的元素个数,与容量作区别
     */
    private int size;
    /**
     * 最大数组大小为Integer.MAX_VALUE - 8,为何要 -8 ?
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
复制代码

初始化函数

/** * 构建一个指定容量的空列表 */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {//建立输入大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//初始容量为0,初始化一个空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//当输入的初始化容量不符合要求,抛出IllegalArgumentException
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /** * 构建一个容量为10的空列表 */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /** * 构建一个包含指定集合的全部元素的列表,元素顺序按照集合迭代器返回的顺序 */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // 将原来集合中元素向上转换为Object类型
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 当集合元素为0,初始化一个空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

    /** * 把ArrayList的容量减少到元素数目大小,可最小化存储 */
    public void trimToSize() {
        modCount++;//算结构性改变
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
复制代码

查找元素

public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }

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

先判断要查找的下标是否大于等于数组大小(从0开始存数据),若没有返回该位置元素,不然抛出下标越界异常

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

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
复制代码

一、首先判断输入元素是否为null,避免空指针异常(ArrayList能够存null元素)

二、indexOf方法从头至尾遍历,lastIndexOf方法从尾到头遍历

三、找到则返回相应位置下标,为大于等于0的整数,没有找到相应元素则返回-1

添加元素

假设在一群人在排队中,忽然来了一我的,正常的话是排到队尾的,至关于add(E e);可是这我的可能有急事就会选择插队,那么他找个位置插进去,那么他插进去位置后面的人都须要日后走一步才会产生空间,这就至关于add(int index, E element)

//在列表末尾添加元素
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // modCount加1
        elementData[size++] = e;
        return true;
    }

    //在列表中任意位置添加元素
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  

        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
复制代码

一、不只检查输入的index是否大于数组大小,还判断是否小于0

二、size+1,其中modCount加1

三、向后移动一部分元素,为要添加的元素腾出空间

四、在所在下标添加元素

五、list中元素个数加1

//指定集合全部元素添加到list集合中,添加了返回true,不然返回false
    public boolean addAll(Collection<? extends E> c) {// E表明父类 ,?表明 子类 
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
复制代码

其中各个参数意义以下:

System.arraycopy(src, srcPos, dest, destpoS,length);
复制代码

Object src : 从哪复制 -- 源数组.

int srcPos : 源数组从哪一个下标开始复制.

Object dest : 复制到哪 -- 目的数组.

int destPos :粘贴到开始下标的位置.

int length :将要被复制的元素个数.

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

        ensureExplicitCapacity(minCapacity);
    }

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
复制代码

若是minCapacity - elementData.length > 0,说明数组已经放满,须要扩容,调用grow()

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
复制代码

一、获取当前数组长度oldCapacity

二、新的长度为oldCapacity + (oldCapacity >> 1),即将oldCapacity右移一位,至关于除以2,新容量是旧容量的1.5倍

三、若是newCapacity - minCapacity < 0,则新容量设置为minCapacity

四、若是新容量比容许的最大数组容量都要大,则新容量设置为虚拟机容许的最大值

五、将elementData放入新容量数组中

在调用add方法时,首先调用ensureCapacityInternal(int minCapacity)

删除元素

//删除指定位置上的元素
    public E remove(int index) {
        rangeCheck(index);//1

        modCount++;//2
        E oldValue = elementData(index);//3

        int numMoved = size - index - 1;//4
        if (numMoved > 0)//5
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; //6

        return oldValue;//7
    }
复制代码

一、判断是否小于数组个数,不然抛异常结束

二、modCount须要增长1

三、旧值须要返回,记录旧值

四、计算移动的个数,下图举例5-2-1=2

五、数组进行左移操做,即删除了要删除的元素

六、最后位置设置为null,使垃圾回收

七、返回旧值

//删除指定的元素
    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;
    }

    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; 
    }
复制代码

一、首先判断是不是null,避免空指针异常,而后从头至尾遍历找对应元素,找到删除返回true,不然返回false。

二、fastRemove(int index)与上面的remove(int index)相似,但少了边界检测,由于说明已经在边界内找到了相应元素。

三、关键modCount++;,下面细节分析中会用到

将list对象转换为数组对象

public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }
复制代码

toArray()做为数组对象和集合对象转化的桥梁,将返回包含list中全部元素的一个新分配的数组。

使用建议

与LinkedList相比

时间复杂度分析

一、 ArrayList基于索引结构,在中间插入或删除一个元素会致使列表中一部分元素移动, 适合查找操做,不适合中间插入、删除操做; LinkedList基于链表结构,在中间插入或删除元素的开销是固定的, 所以适合频繁插入、删除操做。

二、在ArrayList的添加元素,当达到阈值会致使数组进行从新分配的容量。

三、ArrayList的空间浪费主要体如今list列表的结尾预留必定的容量空间;而LinkedList的空间花费则体如今它的每个元素都须要消耗至关的空间,包括指向指针

与Array相比

ArrayList内部封装了一个Object类型的数组,本质与数组没有差异,一些方法内部都是调用array方法实现的。

细节分析

fail-fast为快速失败机制,为Java集合的一种错误检测机制

此类的iteratorlistIterator方法返回的迭代器是fail-fast的:若是在建立迭代器以后的任什么时候候对列表进行结构修改,好比经过迭代器本身的remove或add方法,迭代器将抛出ConcurrentModificationException。所以,在并发修改的状况下,迭代器会快速中止操做,而不是在将来的未肯定时间发生非肯定性行为的风险,并且即便不是在多线程环境,若是单线程违反了规则,一样也会抛出异常。 插入开发手册

在foreach循环中进行元素删除,为何会引发fail-fast机制呢?由于foreach循环中集合遍历是经过iterator进行的。

经过iterator直接运行,固然抛出一样的异常结果。

经过堆栈信息能够获得异常发生在调用链第14行, String next = iterator.next();中调用了 iterator().checkForComodification()方法,而异常就是这个方法中抛出的,看下抛出异常的缘由。

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

说明发生了modCount != expectedModCount,才抛出的异常。接下来咱们来分析modCountexpectedModCount这两个变量 插入图expectedMod

  • modCount是AbstractList中一个成员变量,被ArrayList继承,表示该集合中实际被修改的次数。

  • expectedModCount 是ArrayList中内部类Itr中的成员变量,表示这个迭代器指望该集合被修改的次数,只有经过迭代器对集合进行操做,该值才会改变。

  • Itr是一个Iterator的实现,ArrayList.iterator方法得到的迭代器就是Itr的实例。

private class Itr implements Iterator<E> {
        int expectedModCount = modCount;
}
复制代码

之间的关系以下:

class ArrayList{
    private int modCount;
    public void add();
    public void remove();
    private class Itr implements Iterator<E> {
        int expectedModCount = modCount;
    }
    public Iterator<E> iterator() {
        return new Itr();
    }
}
复制代码

是什么致使modCount != expectedModCount,经过以前分析源码可知fastRemove(int index)modCount加1,但此时expectedModCount没有加1,才致使两个数不相等。

简单来讲集合遍历是经过iterator进行的,可是元素的add/remove倒是使用类中本身的方法吗,所以混淆在了一块儿,致使iterator在遍历的时候,当一个元素经过本身方法添加或删除时,就会抛出一个异常,提醒用户,可能发生了并发修改。

知道了之后咱们能够选择正确的方法在遍历的时候删除一些元素

一、使用普通for循环,此时没有使用iterator

二、迭代器iterator的remove()方法

参考:

Java Standard Ed. 8 docs.oracle.com/javase/8/do…

Holis 《为何阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操做》

持续输出高质量干货,欢迎关注公众号Duffy说码

相关文章
相关标签/搜索