ArrayList源码分析(全)

1 ArrayList

Collections体系结构!还是特别庞大的丫!

在这里插入图片描述

1.1 ArrayList简介

img

集合的诞生(数组的优缺点):

一、优点

  • 按照索引查询元素比较快
  • 能存储大量数据
  • 按照索引遍历数组方便
  • 数组定义简单,而且访问方便
  • 可以随机访问其中的元素

二、缺点

  • 根据内容查找元素速度慢
  • 长度确定(初始化固定),类型固定
  • 数组提供方法非常有限,增删改操作不便,效率不高
  • 数组的空间必须是连续的
  • 存储特点:有序可重复,但是无法满足无序不重复的需求

(集合即可解决数组方面的弊端)

三、Arraylist集合特点

  • 底层数据结构是数组**,查询快,在频繁数组中间或头部增删慢,效率低,线程不安全。**

线程不安全举例:

比如有两个线程,线程 A 先将元素存放在位置0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。

线程B也向此ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1)。

所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增 加 Size 的值。

那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而Size却等于 2,这就是“线程不安全”了。

  • 数组默认容量是10**,当长度不够时自动增长0.5倍,也就是原数组的1.5倍,容量因子 1,扩容系数0.5,新长度= 旧长度* 1.5**
// 数组默认容量为10
private static final int DEFAULT_CAPACITY = 10;

1.2 ArrayList底层探究

ArrayList的属性有哪些?

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    // 序列化版本号
    private static final long serialVersionUID = 8683452581122892189L;

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

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

    // 构造一个初始容量为10的空列表,但只在第一次add元素后将容量调整为10
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 存储ArrayList的元素的数组缓冲区, ArrayList的容量是此数组缓冲区的长度。
    // 添加第一个元素时,任何具有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList都将扩展为DEFAULT_CAPACITY(10)。
    transient Object[] elementData; // 非私有以简化嵌套类访问

    // ArrayList的大小(它包含的元素数)
    private int size;

    // 要分配的数组的最大大小,一些虚拟机在数组中保留一些标题字,当请求的阵列大小超出JVM限制会尝试分配更大的阵列可能会导致OutOfMemoryError。
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

知识拓展之serialVersionUID:

serialVersionUID有两种显示的生成方式。

一是默认的1L,比如:private static final long serialVersionUID = 1L。

二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如: private static final long serialVersionUID = xxxxL。

serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。

序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致。

如果相同就认为是一致的,否则就会出现序列化版本不一致的异常,即是InvalidCastException。

那么什么是序列化,为什么要序列化呢?下文有解释o

ArrayList的构造器分析

// 构造具有指定初始容量的空列表
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 当输入的initialCapacity为0,则指向共享空数组实例
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
// 构造一个初始容量为10的空列表。
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
     * 构造一个列表,该列表包含指定集合的元素,其顺序由集合的迭代器返回。
     * @param c 要将其元素放入此列表的集合
     * 如果指定的集合为null,@throws NullPointerException
     */
public ArrayList(Collection<? extends E> c) {
    //  这里说明所有的 Collection 都可以用数组来承载
    //  这个步骤可能会抛空指针 NullPointerException
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        //必须是Object数组
        if (elementData.getClass() != Object[].class)
            // 然后再copy一份到 elementData 并不是引用 所有改变不会影响到原先的Collection
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 有参构造函数 当初始化为空数组时 赋值为 EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

// 这里属于针对初始化时的扩容判断,当为DEFAULTCAPACITY_EMPTY_ELEMENTDATA时说明是无参构造函数创建的,则可以直接扩容为DEFAULT_CAPACITY也就是10为初始容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

小总结:

  • 如果使用ArrayList的无参构造,则使用成员变量中定义的空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA作为存储元素的数组,赋值给elementData。
  • 如果使用带容量大小的构造函数,会判断传入的容量大小,如果容量大于0,则会创建一个与容量大小等长度的数组;如果容量大小小于0,则会抛出异常;如果容量大小等于0,则会使用成员变量EMPTY_ELEMENTDATA定义的空数组作为存储元素的数组elementData。
  • 如果使用带有collection集合作为参数的构造方法,也会使用toArray()方法将集合转化为数组然后复制一份赋给存储元素的数组。如果传入的collection集合为空,则也会使用成员变量EMPTY_ELEMENTDATA定义的空数组作为存储元素的数组elementData。

1.3 ArrayList之十万个为什么?

ArrayList为什么定义两个空数组对象?

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

// 构造一个初始容量为10的空列表
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

在ArrayList的构造方法中,如果使用无参构造创建对象或者使用带参构造但是传入的初始容量为零或者Collection对象为空时,则此时创建出来的ArrayList的实例不需要存储元素,即是一个空集合。

在JDK1.7中,如果ArrayList对象的容量是0的话,会创建一个空数组并将其引用赋给elementData,这就造成了如果我们的项目中空的ArrayList集合的数量比较多的话,则会创建很多个空数组,造成性能与空间的浪费。

// JDK中的ArrayList源码
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    // 这里直接new一个空数组
    this.elementData = new Object[initialCapacity];
}
public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    // 这里实际上也相当于new了一个空数组 当c.toArray() 为空数组时copy了一份空数组
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

为了解决存在大量空数组问题,在JDK1.8中,ArrayList定义了两个由所有对象共享的静态属性指向两个空数组,并且该属性用final修饰所以存储的数组地址不能改变,并且由所有ArrayList实例所共享。

这样,无论有多少个没有存储元素的空ArrayList对象,其elementData属性都将其指向了这两个相同的空数组中的一个,不必创建空数组,很大程度上减少了空数组的创建与存在。

小总结:

  • 两个空对象都是用来减少空数组的创建,所有空ArrayList都共享空数组。
  • EMPTY_ELEMENTDATA用在有参构造函数当初始容量为0或者传入空集合对象时共享赋值用。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA 用在无参构造函数赋值用。

ArrayList是实例化的时候就是默认容量10?

虽然使用无参构造创建时会指向一个初始容量为10的空列表。

但实际上ArrayList创建的是一个容量为0的数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA 标识),只有在第一次新增元素时才会被扩容为10。

只有在add()之后才会调用扩容方法,newCapacity = 10。

// 添加方法,假设当前加入了第一个元素
add("hello")
public boolean add(E e) {
    // ensureCapacityInternal调用ensureExplicitCapacity是否需要扩容
    // ensureCapacityInternal(0 + 1)
    ensureCapacityInternal(size + 1); 
    // 后++,先计算后++
    // elementData[0] = "hello";
    elementData[size++] = e;
    return true;
}

// 此时minCapacity = 1
private void ensureCapacityInternal(int minCapacity) {
    // calculateCapacity返回的是DEFAULT_CAPACITY的默认值10
    // ensureExplicitCapacity(10)
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是空参调用,则返回DEFAULT_CAPACITY(10)与传入的minCapacity两者的最大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 非空参调用,直接返回传入的值
    return minCapacity;
}
// 返回较大的值
public static int max(int a, int b) {
    return (a >= b) ? a : b;
}


// ensureExplicitCapacity(10)
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 10 - 0  > 0,接着调用扩容方法
    if (minCapacity - elementData.length > 0)
        // grow(10)
        grow(minCapacity);
}

 // grow(10)
private void grow(int minCapacity) {
    // oldCapacity = 0;
    int oldCapacity = elementData.length;
    // newCapacity = 0 + (0)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 0 - 10必然成立,所以将newCapacity赋值为10
    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并且扩容为10
    elementData = Arrays.copyOf(elementData,newCapacity);
}

// ArrayList集合默认存储的最大元素数量是int型数据的最大值减去8(2^31-8),实际能存储的最大元素数量是int型数据的最大值,即为2147483647。
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}
// 得到新的容量大小后,首先会创建一个长度为新容量大小的新数组,然后采用数组拷贝的方式,将旧的数组拷贝至一个以新容量为长度的新数组中。数组拷贝使用的是System类的arraycopy方法。

// System类的arraycopy()方法的作用是实现数组复制,其中各个参数含义为:src:原数组;srcPos:源数组要复制的起始位置; dest:目的数组; destPos:目的数组放置的起始位置; length:复制的长度。
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

// ArrayList最终调用的创建新数组并拷贝数据的方法:以新的数量大小为长度创建一个新数组,然后将原数组的元素从0开始拷贝到末尾,到新数组中从0位置开始放置。
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

小总结:

1、调用构造方法创建时,采用懒加载的方式,即AraryList的实例的初始size是0,等进行第一次add时,才会把size变成10,这是第一次扩容,从0到10。

2、第一次扩容之后,从第二次add到第十次,都不会有扩容操作,直到第11次add,即上一次插入元素后,数组已经存满了,那么下一次就会触发扩容机制。新数组的容量是newCapacity = oldCapacity + (oldCapacity >> 1),可能正好是原数组大小的1.5倍,或1.5倍左右,关键看原数组大小是不是偶数,这就是第二次扩容。

3、之后的每次扩容条件以及扩容机制就都和第二次一样了。

elementData和size一样大嘛?

用size标记集合的元素个数,size为集合的元素个数,并非集合中用来存储元素的数组elementData的长度。

实际上用来存储元素的数组elementData的长度一般比size更大一些。

为什么更大呐?下文有解释。

元素数组为什么用transient修饰?

首先需要了解一下什么是序列化?

序列化是一种用来处理对象流的机制,就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。

实现:使用上将需要被序列化的类实现Serializable接口即可。

序列化的时候会调用 writeObject() 方法,把对象转换为字节流,反序列化的时候会调用 readObject() 方法,把字节流转换为对象。

Java 在反序列化的时候会校验字节流中的 serialVersionUID 与对象的 serialVersionUID 时候一致。如果不一致就会抛出 InvalidClassException 异常。官方强烈推荐为序列化的对象指定一个固定的 serialVersionUID。否则虚拟机会根据类的相关信息通过一个摘要算法生成,所以当我们改变类的参数的时候虚拟机生成的 serialVersionUID 是会变化的。

作用:1、序列化是为了解决在对对象流进行读写操作时所引发的问题,方便在网络上传送对象的字节序列;2、把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中

大白话:序列化 就是把信息以某种方式表现出来,本质是信息交换的需要

举个栗子:你把知识写到书中这就是个序列化的过程,而读者将书中的知识为己所用就是反序列化,这样构成了一个单向的信息交互过程,这个序列化过程中的媒介是“书”

使用transient的目的就是为了避免了Java自带的序列化机制,并定义了两个方法,实现了可定制的序列化。

为什么不使用java自带的序列化机制呢?难道不希望elementData被序列化?假如elementData的长度为10,而其中只有5个元素,那么在序列化的时候只需要存储5个元素,而数组中后面5个元素是不需要存储的。因此使用ArrayList 自己实现这两个序列化方法;

/**
     * 将ArrayList实例的状态保存到流(即,对其进行序列化)。 
     * @serialData发出支持ArrayList实例的数组的长度(int),然后以正确的顺序跟随其所有元素(每个Object)。
     */
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // modCount是继承自AbstractList中的字段,表示数组修改的次数,数组每修改一次,就要增加modCount
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // 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();
    }
}

/**
     * 从流中重构ArrayList实例(即,将其反序列化)。
     * deserialize it).
     */
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);

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

在 writeObject() 方法中,for循环按需序列化,用了几个下标序列化几个对象。读取的时候也是一样的,有几个读几个,开发人员真是优化到极致!

ArrayList容量上限为什么是Integer.MAX_VALUE - 8

数组对象的形状和结构(如int值数组)与标准Java对象类似。主要区别在于数组对象有一个额外的元数据,用于表示数组的大小。然后,数组对象的元数据由以下部分组成:Class:指向描述对象类型的类信息的指针。在int数组的情况下,这是一个指向int []类的指针。

标志:描述对象状态的标志集合,包括该对象的散列码(如果有)以及对象的形状(即对象是否为数组)。

锁定:对象的同步信息 - 即对象是否当前同步。

大小:数组的大小。

这就对应了上面实际上用来存储元素的数组的长度一般比size更大一些。

ArrayList的扩容机制并非是将存储元素的数组elementData每次扩容至与实际存储元素数量相同的大小,而是一般采用移位运算将原先数组长度增加50%作为新的数组长度。在这种机制下,可以有效的减少数组的扩容次数,但是也会造成内部封装的用来存储元素的数组的长度一般比ArrayList集合元素数量大。

ArrayList默认的最大容量MAX_ARRAY_SIZE是int型数据的最大值减去8,即(2147483647-8=2147483639)。但是如果你想存储int的最大值即2147483647个元素也可以,只是这样会导致内存溢出的风险。因此ArrayList采取的是尽量不将数组扩容到2147483647,即如果采用原数组长度增加50%的机制扩容则数组长度会超过MAX_ARRAY_SIZE的话,就不再按照原数组长度增加50%的机制来扩容,转而只扩容到数组实际需要的最小长度。

在ArrayList的尾部和指定位置添加元素的区别?

ArrayList检查数组长度的方法是ensureCapacityInternal方法,其中包含了ArrayList的扩容机制。

  • 在尾部增加元素
/**
     *将指定的元素追加到此列表的末尾。 
     */
    public boolean add(E e) {
        // 首先调用ensureCapacityInternal()方法检查数组长度是否不足,不足的话则创建一个新长度的数组,采用数组拷贝的方式将旧数组的元素复制给新数组。
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 将数组的最第N+1个元素赋值。
        elementData[size++] = e; // 数组赋值操作
        return true;
    }
  • 在指定位置添加元素
// 检查传入的位置是否合法,如果位置的值小于0或者大于集合的长度,则抛出异常。
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

public void add(int index, E element) {
    rangeCheckForAdd(index);
	// 检查数组大小是否足够,不够则扩容。
    ensureCapacityInternal(size + 1);  
    // 调用System类的arraycopy()进行数组拷贝,拷贝规则为:将原数组中从传入的位置开始到数组的最后一个元素这一段的所有元素,拷贝到原数组中,但是以传入位置加上1为起始位置进行放置。相当于把从传入的位置开始直到最后的所以元素向后移了一位,然后传入的位置就空了出来。
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    // 将数组中传入位置的元素赋值。
    elementData[index] = element;
    size++;
}

小总结:

  • 在元素尾部追加元素,先调用ensureCapacityInternal()方法检查数组长度是否不足,不足的话则创建一个新长度的数组,采用数组拷贝的方式将旧数组的元素复制给新数组,然后将数组的最第N+1个元素赋值。
  • 在指定元素位置插入的话,将原数组中从传入的位置开始到数组的最后一个元素这一段的所有元素,拷贝到原数组中,但是以传入位置加上1为起始位置进行放置。相当于把从传入的位置开始直到最后的所以元素向后移了一位,然后传入的位置就空了出来,然后将数组中传入位置的元素赋值。

ArrayList查询快体现在哪?

// 检查给定的索引是否在范围内。如果不是,则抛出适当的运行时异常。此方法不检查索引是否为负:始终在数组访问之前立即使用它,如果索引为负,则抛出ArrayIndexOutOfBoundsException。
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}


public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

小总结:

  • 这里其实就可以看出为什么在ArrayList中进行查找指定位置元素的速度很快,因为ArrayList内部封装了一个数组进行元素存储,数组具有下标索引,只需要返回数组中对应下标的元素即可,不用做任何遍历操作,所以速度很快。

ArrayList增删慢的体现?

// 删除此列表中指定位置的元素。将所有后续元素向左移动(从其索引中减去一个)。 @param index要删除的元素的索引 @return返回从列表中删除的元素
public E remove(int index) {
    // 检查传入的位置是否合法,即如果位置的值小于0或者大于集合的长度,则抛出异常。
    rangeCheck(index);

    modCount++;
    // 从数组中取出该位置的元素。
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    // 判断传入的位置是否是数组中最后一个非空元素的位置。
    if (numMoved > 0)
        // 如果传入的位置非数组中最后一个非空元素的位置,则调用System类的arraycopy()进行数组拷贝,拷贝的规则为:将原数组中从传入的位置+1的位置开始到数组的最后一个元素这一段的所有元素,拷贝到以原数组中,但是以传入的位置为起始位置。相当于将删除位置以后的所有元素前进一位,把需要删除的元素覆盖掉。
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 拷贝完成后,该数组最末尾有两个相同的元素,则需要将数组的最后一个非空元素的值设为null,明确让GC开展回收工作
    elementData[--size] = null; 
    // 返回删除的元素。
    return oldValue;
}

小总结:

  • 这里就可以看出为什么在ArrayList中进行插入和删除元素的效率比较低,一方面因为数组本身不能截断或者拼接,在指定位置进行插入和删除都需要在原数组上进行一次范围性的拷贝,以此来让数组中该位置后面的所有元素实现前进或后移。

  • 另一方面数组的长度也不能改变,如果添加元素导致数组长度不够,则需要新建一个更长的数组并且将原数组的所有元素拷贝到新数组中,这是非常消耗性能的。

根据元素对象找到指定元素并删除,是全部删除嘛?

// 如果存在指定元素,则从该列表中删除该元素的第一次出现。如果列表不包含该元素,则它保持不变。
// 更正式地讲,删除索引i 最低的元素,使(o == null?get(i)== null:o.equals(get(i)))(如果存在这样的元素)。
// 如果此列表包含指定的元素(或者等效地,如果此列表由于调用而更改),则返回true。
// @param o要从此列表中删除的元素(如果存在)@return true(如果此列表包含指定的元素)
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;
}

小总结:

  • 从0位置开始遍历数组中的元素,直到找到与该对象相同的元素为止,然后使用fastRemove(index)方法进行删除并返回true。

  • fastRemove(index)和remove(index)方法类似,只是不获取要删除的对象并返回而已。

  • 因为此种删除的逻辑是遍历数组找到第一个与对象相同的元素就进行删除并返回true,所以如果ArrayList集合中包含重复的元素,则该方法只能删除第一个,不能将重复的元素全部删除。

ArrayList的fail-fast(快速失败机制)是什么?

fail-fast简介

fail-fast 机制是java集合(Collection)中的一种错误机制。

当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。

例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;

那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

fail-fast出现场景
  • 单线程情况下
public static void main(String[] args) {
    List list = new ArrayList();
    for (int i = 0; i < 5; i++) {
        list.add(i);
    }
    Iterator it = list.iterator();
    int i = 0;
    while (it.hasNext()) {
        if (i == 3) {
            list.remove(i);
        }
        System.out.println(it.next());
        i++;
    }
}

img

  • 多线程情况下
public static void main(String[] args) {
    new MyThread1().start();
    new MyThread2().start();
}
// 多线程情况下测试fail-fast

private static class MyThread1 extends Thread {
    @Override
    public void run() {
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String s = iterator.next();
            System.out.println(this.getName() + ":" + s);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        super.run();
    }
}

private static class MyThread2 extends Thread {
    int i = 0;

    @Override
    public void run() {
        while (i < 10) {
            System.out.println("thread2:" + i);
            if (i == 2) {
                list.remove(i);
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            i++;
        }
    }
}
fail-fast产生原因

过上面的示例和讲解,我初步知道fail-fast产生的原因就在于程序在对 collection 进行迭代时,某个线程对该 collection 在结构上对其做了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 fail-fast。

了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出改异常

private class Itr implements Iterator<E> {
    int cursor;
    int lastRet = -1;
    int expectedModCount = ArrayList.this.modCount;

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

    public E next() {
        checkForComodification();
        /** 省略此处代码 */
    }

    public void remove() {
        if (this.lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();
        /** 省略此处代码 */
    }

    final void checkForComodification() {
        if (ArrayList.this.modCount == this.expectedModCount)
            return;
        throw new ConcurrentModificationException();
    }
}

从上面的源代码我们可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,

该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

所以要弄清楚为什么会产生fail-fast机制我们就必须要用弄明白为什么modCount != expectedModCount ,他们的值在什么时候发生改变的。

ublic boolean add(E paramE) {
    ensureCapacityInternal(this.size + 1);
    /** 省略此处代码 */
}

private void ensureCapacityInternal(int paramInt) {
    if (this.elementData == EMPTY_ELEMENTDATA)
        paramInt = Math.max(10, paramInt);
    ensureExplicitCapacity(paramInt);
}

private void ensureExplicitCapacity(int paramInt) {
    this.modCount += 1;    //修改modCount
    /** 省略此处代码 */
}

public boolean remove(Object paramObject) {
    int i;
    if (paramObject == null)
        for (i = 0; i < this.size; ++i) {
            if (this.elementData[i] != null)
                continue;
            fastRemove(i);
            return true;
        }
    else
        for (i = 0; i < this.size; ++i) {
            if (!(paramObject.equals(this.elementData[i])))
                continue;
            fastRemove(i);
            return true;
        }
    return false;
}

private void fastRemove(int paramInt) {
    this.modCount += 1;   //修改modCount
    /** 省略此处代码 */
}

public void clear() {
    this.modCount += 1;    //修改modCount
    /** 省略此处代码 */
}

从中,我们发现:无论是add()、remove(),还是clear(),只要涉及到修改集合中的元素个数时,都会改变modCount的值。

梳理:

  • 建立一个ArrayList,名称为arrayList。
  • 向arrayList中添加内容。
  • 新建一个“线程a”,并在“线程a”中通过Iterator反复的读取arrayList的值
  • 新建一个“线程b”,在“线程b”中删除arrayList中的一个“节点A”。
  • 这时,在某一时刻,“线程a”创建了arrayList的Iterator。此时“节点A”仍然存在于arrayList中,创建arrayList时,expectedModCount = modCount(假设它们此时的值为N)。
  • 在“线程a”在遍历arrayList过程中的某一时刻,“线程b”执行了,并且“线程b”删除了arrayList中的“节点A”。“线程b”执行remove()进行删除操作时,在remove()中执行了“modCount++”,此时modCount变成了N+1
  • 线程a”接着遍历,当它执行到next()函数时,调用checkForComodification()比较“expectedModCount”和“modCount”的大小;而“expectedModCount=N”,“modCount=N+1”,这样,便抛出ConcurrentModificationException异常,产生fail-fast事件。
fail-fast解决方法
  • 解决方法1

在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList。

这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

  • 解决方法2

使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。

CopyOnWriteArrayList介绍

CopyOnWriteArrayList是什么?ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。

该类产生的开销比较大,但是在两种情况下,它非常适合使用。

1:在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。

2:当遍历操作的数量大大超过可变操作的数量时。

遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。那么为什么CopyOnWriterArrayList可以替代ArrayList呢?

  • CopyOnWriterArrayList的无论是从数据结构、定义都和ArrayList一样。它和ArrayList一样,同样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。
  • CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制。
static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }
    ......
}

CopyOnWriterArrayList的方法根本就没有像ArrayList中使用checkForComodification方法来判断expectedModCount 与 modCount 是否相等。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // CopyOnWriterArrayList的add方法与ArrayList的add方法有一个最大的不同点就在这里
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

他们所展现的魅力就在于copy原来的array,再在copy数组上进行add操作,这样做就完全不会影响COWIterator中的array了。

所以CopyOnWriterArrayList所代表的核心概念就是:

任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的。

1.4 ArrayList常用方法

官方api也就那么一屏!

在这里插入图片描述

下面列举一些比较常用的方法:

1、add(Object element): 向列表的尾部添加指定的元素。

2、size(): 返回列表中的元素个数。

3、get(int index): 返回列表中指定位置的元素,index从0开始。

4、add(int index, Object element): 在列表的指定位置插入指定元素。

5、set(int i, Object element): 将索引i位置元素替换为元素element并返回被替换的元素。

6、clear(): 从列表中移除所有元素。

7、isEmpty(): 判断列表是否包含元素,不包含元素则返回 true,否则返回false。

8、contains(Object o): 如果列表包含指定的元素,则返回 true。

9、remove(int index): 移除列表中指定位置的元素,并返回被删元素。

10、remove(Object o): 移除集合中第一次出现的指定元素,移除成功返回true,否则返回false。

11、iterator(): 返回按适当顺序在列表的元素上进行迭代的迭代器。

注意点:

  • remove()指定元素为int类型,该怎么删?

remove(int index)是按照索引删除,假设我集合存的都是int类型,那调用remove的哪个方法呢?

毋庸置疑会调用,按照索引删除元素。

那么我就想删除为2的元素,怎么办呢?

img

list.remove(new Integer(2));

  • 如何顺序(倒序)删除节点?

ArrayList有自带的删除所有元素的方法,但是怎么利用remove()移除所有元素呢?

① 错解一

List list = new ArrayList();
// 添加
list.add(1);
list.add(2);
list.add(3);
list.add(4);
for (int i = 0; i < list.size(); i++) {
    list.remove(0);
}
System.out.println(list);

测试结果:
    [3, 4]

② 错解二

List list = new ArrayList();
// 添加
list.add(1);
list.add(2);
list.add(3);
list.add(4);
for (int i = 0; i < list.size(); i++) {
    list.remove(i);
}
System.out.println(list);

测试结果:
    [2, 4]

④ 错解三

List list = new ArrayList();
// 添加
list.add(1);
list.add(2);
list.add(3);
list.add(4);
int size = list.size();
for (int i = 0; i < size; i++) {
    list.remove(i);
}
System.out.println(list);
}

测试结果:
    Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 2, Size: 2

⑤ 正解一,从前往后删

List list = new ArrayList();
// 添加
list.add(1);
list.add(2);
list.add(3);
list.add(4);
int size = list.size();
for (int i = 0; i < size; i++) {
    list.remove(0);
}
System.out.println(list);
}

测试结果:
    []

⑥ 正解二,从后往前删

// 通过一般for循环,必须从后往前删除!
for (int i=(arraylist.size()-1);i>=0;i--){
    arraylist.remove(i);
}
测试结果:
    []

⑦ 正解三,利用迭代器删

//迭代器的方式顺序移除节点
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    iterator.next();//没有这一行, 就会抛出java.lang.IllegalStateException异常
    iterator.remove();//这里只能调用迭代器对应的remove()方法
}
测试结果:
    []

img

  • ArrayList三种遍历方式
List list = new ArrayList();
// 添加
list.add(1);
list.add(2);
list.add(3);
list.add(4);
// 普通for循环
for (int i = 0; i < arrayList.size(); i++) {
    System.out.print(list.get(i));
}
// 增强for循环
for (Object s : list) {
    System.out.print((String) s);
}
// 迭代器删除
Iterator it = list.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}
测试结果:
    1
    2
    3
    4

1.5 ArrayList面试高频

场景1:从并发的角度看待ArrayList

例如:开启20个线程,每个线程给生成一个随机数,并且将随机数添加到list容器中

public class ArrayListUnsafe {
    public static void main(String[] args) {
        List<String> lists = new ArrayList <>();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                lists.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(lists);
            },String.valueOf(i)).start();
        }
    }
}

通常情况下,这个程序会报java.util.ConcurrentModificationException,这也是高并发情况下常见的错误,并发修改异常

为什么会产生这种问题呢?

在多线程下,每次往容器中写数据时,不保证顺序,谁抢占到了容器谁开始写入数据,因此可能存在覆盖情况,导致每次执行的结果都不一致。

有什么办法可以避免这种问题吗?

现在jdk提供了多种方式保证List的线程安全。

  1. 使用传统的Vector集合类。但是该类在方法上加上Synchronize关键字,保证线程安全。但是jdK已经不推荐了,因为用了重量级加锁方式,导致执行效率低。
  2. 使用工具,Collections.synchronizedList保证线程安全。比如
    List lists = Collections.synchronizedList(new ArrayList <>());
  3. 利用写时复制的集合类。CopyOnWriteArrayList

能够简单说说什么是CopyOnWriteArrayList吗?

写时复制类似于将读数据和写数据过程分离开来。比如A线程和B线程都开始写数据,A、B每次写数据之前,都需要拿到一个许可证(类似于锁),主内存中数据复制到工作内存中,然后再进行修改,修改完毕之后将容器的引用指向新的数据集,然后再允许别的线程修改。每次添加元素的时候,先获取锁(也就是许可),之后才去更新元素。

public boolean add(E e) {
    // 先获取锁,也就是前文说的许可证
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 将原数组复制一份
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 添加新值
        newElements[len] = e;
        // 将数组索引指向新数组
        setArray(newElements);
        return true;
    } finally {
        // 最后释放锁
        lock.unlock();
    }
}

场景2:ArrayList初始化时Object数组的长度是多少呢?无参构造函数初始化的ArrayList执行一次add方法后数组长度又是多少呢?

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

// 构造一个初始容量为10的空列表,但只在第一次add元素后将容量调整为10
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 存储ArrayList的元素的数组缓冲区, ArrayList的容量是此数组缓冲区的长度。
// 添加第一个元素时,任何具有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList都将扩展为DEFAULT_CAPACITY(10)。
transient Object[] elementData; // 非私有以简化嵌套类访问

首先是初始化数据的长度是多少,ArrayList一共有三个构造函数。

参构造函数会把DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋给elementData,也就是说无参构造函数初始化的数组长度为0

带初始化长度的的则会初始化指定长度的数组,如果参数为0,则会把EMPTY_ELEMENTDATA赋给elementData。

还有一个构造函数可以传递一个Collection对象,那么对应数组的长度就是集合的长度,同样如果集合对象长度为0的话也会把EMPTY_ELEMENTDATA赋给elementData。

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

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

ensureCapacityInternal之前会调用calculateCapacity调整容量。

如果是elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA采用空参构造器初始化,则返回minCapacity与DEFAULT_CAPACITY(10)的最大值。

如果是第一次添加minCapacity为1,所以采用空参初始化当第一次add后数组长度为10

那之后调用add(),会扩容嘛?

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

    // 当第一add后,elementData的长度为10,根本不会调用扩容的方法
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

那要是存满10个后的数组长度又是怎么变化的呢?

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

**当满10个继续添加元素一定会调用扩容方法,但是返回的数组长度是用 新长度=旧长度+(旧长度/21)**,按位计算,左移是乘以2移位数,右移是除以2^移位数

当满10个后,注意此时数组长度为(10+10/2)15,因此只有当满了15个以后添加才会按位扩容,以此例推,下次扩容为(15+15/2)22,满22才扩容。

注意按位计算不会保留小数!

场景3:ArrayList是如何扩容的?

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 刚好初始化
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
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);
}

扩容的第一步是验证属性elementData是否等于默认的初始化数组(也就是elementData是否等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA),是则表明集合刚被初始化,那么minCapacity就去minCapacity与10的最大值;

最后在验证minCapacity是否大于elementData的length,大于才扩容;

newCapacity=oldCapacity + (oldCapacity >> 1);可以看到每次扩容都是在原有的长度上加二分之一的长度;

最后再取传递进来的minCapacity与newCapacity的最大值作为真正的下一个elementData的长度。

最后利用Arrays.copyOf()把elementData复制到新数组中并赋值给elementData;

最后把新加数据放到数组中elementData[size+1]=object;

场景4:在 foreach 循环里可否进行元素的 remove/add 操作?为什么?

public static void main(String[] args) {
    ArrayList<Object> list = new ArrayList<>();
    list.add("h");
    list.add("h");
    list.add("h");
    list.add("h");
    for (Object o : list) {
        list.remove(2);
        System.out.println(o);
    }

    测试结果:
        h
        Exception in thread "main" java.util.ConcurrentModificationException

ArrayList还有一个int属性modCount,不管是添加方法还是移除方法,每次成功都会对modCount加1,也就是相当于记录ArrayList的修改次数。

最后就是迭代方法,我们知道集合的foreach 最终都是依赖内部的一个Iterator的实现类Itr。

在new一个Itr的实例的时候会保存当前的modCount,next方法首先就会校验记录modCount与集合的modCount是否一致

所以是不允许在 foreach 循环里进行元素的 remove/add 操作。

那么为什么要这么做呢?为什么这种遍历的时候不让删除呢?前面分析我们知道当删除元素后,后面的元素会往前移,比如现在遍历到索引2然后删除了它,原来3的位置就到了2,下次遍历就是3了,那么实际上原来在3位置的元素就没有遍历到。

实在需要遍历删除,不要使用ArrayList的remove,改为用Iterator的remove即可。

场景5:在索引中ArrayList的增加或者删除某个对象的运行过程?效率很低吗?解释一下为什么?

效率是很低的,因为ArrayList无论是增加或者删除某个对象,我们都要通过对数组中的元素进行移位来实现。

  • 增加元素时,我们要把要增加位置及以后的所有元素都往后移一位,先腾出一个空间,然后再进行添加。
  • 删除某个元素时,我们也要把删除位置以后的元素全部元素往前挪一位,通过覆盖的方式来删除。

而这种移位就需要不断的arraycopy,是很耗时间的,所以效率自然也很低。

场景6:ArrayList如何顺序删除节点?

  • 一种是通过普通for循环的方式,但是要注意从后往前删出。否则的话会出现删不干净的问题。
//通过一般for循环,必须从后往前删除!
for (int i=(arraylist.size()-1);i>=0;i--){
    arraylist.remove(i);
}
  • 另一种是,通过迭代器,并调用迭代器的remove方法来删除。
//迭代器的方式顺序移除节点
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    iterator.next();//没有这一行, 就会抛出java.lang.IllegalStateException异常
    iterator.remove();//这里只能调用迭代器对应的remove()方法
}

注意:不调用iterator.next();这一行, 会抛出IllegalStateException异常。

原因是:通过Iterator来删除,首先需要使用next方法迭代出集合中的元素,然后才能调用remove方法,否则集合可能抛出IllegalStateException异常。

  • 普通for循环也能从前往后删,需要注意细节
int size = list.size();
for (int i = 0; i < size; i++) {
    list.remove(0);
}

注意:这里需要将list.size()放到外面,每次删第0个元素即可!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值