一、概要

  • ArrayList 不是线程安全的;
  • ArrayList 底层是使用一个 Object 数组来存储数据的;
  • ArrayList 可以存放任何元素,包括 null;
  • 查找效率高,但增删慢(在 ArrayList 末尾增删很快,但在中间增删需要移动后面一部分的数组元素,效率低下);
  • ArrayList 基本等同于 Vector ,除了 ArrayList 是线程不安全(执行效率相对高一些),在多线程情况下,不建议使用 ArrayList 作为共享变量。

  • 扩容操作会阻塞 add 操作,造成新增元素效率低下,可以通过指定初始化容量,在一定程度上缓解这个问题;

  • 数组无法存储大数据量(因为很难找到一块很大的连续的内存空间)

二、示例代码

  1. package test;
  2. import java.util.ArrayList;
  3. import java.util.Iterator;
  4. import java.util.ListIterator;
  5. import java.util.Objects;
  6. public class CollectionTest {
  7. public static void main(String[] args) {
  8. //2、添加元素
  9. Studen s1 = new Studen("张三", 15);
  10. Studen s2 = new Studen("李四", 16);
  11. Studen s3 = new Studen("王五", 17);
  12. Studen s4 = new Studen("钱六", 18);
  13. arrayList.add(s1);
  14. arrayList.add(s2);
  15. arrayList.add(s3);
  16. arrayList.add(s4);
  17. //3、删除元素
  18. //3.1 通过下标删除
  19. arrayList.remove(1);
  20. //3.2 使用对象引用删除
  21. arrayList.remove(s2);
  22. //3.3 使用对象删除(需要重写对象的equals方法)
  23. arrayList.remove(new Studen("李四", 16));
  24. //4、遍历元素
  25. //4.1 迭代器循环
  26. System.out.println("*********** 迭代器循环 *****************************");
  27. Iterator it = arrayList.iterator();
  28. while (it.hasNext()) {
  29. Studen s = (Studen) it.next();
  30. System.out.println(s.getName());
  31. }
  32. //4.2 列表迭代器 正向循环
  33. System.out.println("*********** 列表迭代器正向循环 *****************************");
  34. ListIterator lit = arrayList.listIterator();
  35. while (lit.hasNext()) {
  36. Studen s = (Studen) lit.next();
  37. System.out.println(s.getName());
  38. }
  39. //4.3 列表迭代器逆向循环
  40. System.out.println("*********** 列表迭代器逆向循环 *****************************");
  41. while (lit.hasPrevious()) {
  42. Studen s = (Studen) lit.previous();
  43. System.out.println(s.getName());
  44. }
  45. //4.4 增强for
  46. System.out.println("*********** 增强for *****************************");
  47. for (Object obj : arrayList) {
  48. Studen s = (Studen) obj;
  49. System.out.println(s);
  50. }
  51. //5、查找
  52. System.out.println("*********** 查找 *****************************");
  53. System.out.println(arrayList.indexOf(new Studen("李四", 16)));
  54. //6、判断对象是否存在
  55. System.out.println(arrayList.contains(new Studen("李四", 16)));
  56. System.out.println(arrayList.contains(s2));
  57. }
  58. }
  59. class Studen {
  60. String name;
  61. int age;
  62. public Studen(String name, int age) {
  63. this.name = name;
  64. this.age = age;
  65. }
  66. public String getName() {
  67. return name;
  68. }
  69. public void setName(String name) {
  70. this.name = name;
  71. }
  72. public int getAge() {
  73. return age;
  74. }
  75. public void setAge(int age) {
  76. this.age = age;
  77. }
  78. @Override
  79. public boolean equals(Object o) {
  80. if (this == o) {
  81. return true;
  82. }
  83. if (o == null || getClass() != o.getClass()) {
  84. return false;
  85. }
  86. Studen studen = (Studen) o;
  87. return this.age == studen.age && this.name.equals(studen.name);
  88. }
  89. @Override
  90. public String toString() {
  91. return "Studen{" +
  92. "name='" + name + '\'' +
  93. ", age=" + age +
  94. '}';
  95. }
  96. @Override
  97. public int hashCode() {
  98. return Objects.hash(name, age);
  99. }
  100. }

三、源码分析

3.1、继承关系

1)Serializable 标记性接口

标记为可序列化

2)Cloneable 标记性接口

标记为可克隆

3)RandomAccess 标记性接口

标记为可随机访问

3.1、底层数据结构

ArrayList 中维护了一个 Object 类型的数组 elmentData

  1. public class ArrayList<E> extends AbstractList<E>
  2. implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  3. {
  4. private static final long serialVersionUID = 8683452581122892189L;
  5. /**
  6. * Default initial capacity.
  7. */
  8. private static final int DEFAULT_CAPACITY = 10;
  9. /**
  10. * Shared empty array instance used for empty instances.
  11. */
  12. private static final Object[] EMPTY_ELEMENTDATA = {};
  13. /**
  14. * Shared empty array instance used for default sized empty instances. We
  15. * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
  16. * first element is added.
  17. */
  18. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  19. /**
  20. * The array buffer into which the elements of the ArrayList are stored.
  21. * The capacity of the ArrayList is the length of this array buffer. Any
  22. * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  23. * will be expanded to DEFAULT_CAPACITY when the first element is added.
  24. */
  25. transient Object[] elementData; // non-private to simplify nested class access

elementData: 数组缓冲区,ArrayList 的元素被存储在其中。ArrayList 的容量是这个数组缓冲区的长度。任何空的 ArrayList,如果 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA(静态空数组),当第一个元素被添加时,将被扩展到 DEFAULT_CAPACITY(10)。

也就是说,使用无参构造器实例化一个 ArrayList 对象时,它的初始容量为 0。第 1 次向其添加,则会将容量扩容为 10,如需要再次扩容,则会扩容至 1.5 倍(15)。

如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,下次扩容时,会将扩容至指定大小的 1.5 倍。(指定大小 * 150%)

3.2、ArrayList 的构造器

  • 当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA (一个静态空数组);

    1. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  • 当创建 ArrayList 对象时,如果使用的是 ArrayList(int initialCapacity) 构造器,则初始 elementData 容量为 capacity。

    1. this.elementData = new Object[initialCapacity];

三个构造器:

    /**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    public ArrayList(int initialCapacity) {
        // 若 initialCapacity 大于 0
        if (initialCapacity > 0) {
            // 创建一个长度为 initialCapacity 的 Object 数组, 将地址赋给 elementData
            this.elementData = new Object[initialCapacity];

        // 若 initialCapacity 等于 0
        } else if (initialCapacity == 0) {
            // 将 elementData 指向 ArrayList 内部的静态 Object 空数组
            // private static final Object[] EMPTY_ELEMENTDATA = {};
            this.elementData = EMPTY_ELEMENTDATA;

        //  若 initialCapacity 小于 0
        } else {
            // 抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        // 静态空数组
        // private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        // 将参数 Collection c 转换为一个新数组, 将地址赋给 elementData
        elementData = c.toArray();

        // 若 size 不等于 0
        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);

        // size 等于 0
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

3.3、默认扩容体积

当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0 (JDK7 是 10),在第一次添加元素时,先将 elementData 的体积扩容至 10。如果需要再次扩容的话,则将 elementData 扩容至原体积的 1.5 倍。

private static final int DEFAULT_CAPACITY = 10;

3.3、添加元素(add)与扩容机制(grow)

当添加元素时,先判断是否需要扩容,如果需要扩容,则调用 grow 方法,否则直接添加元素到合适位置。
当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 为空数组,在第一次添加元素时,先将 elementData 的体积扩容至 10。如果需要再次扩容的话,则将 elementData 扩容至原体积的 1.5 倍。

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

// 确保 ArrayList 内部容量足够
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 取 ArrayList 当前容量与传入的最低容量 二者的最大值
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 确保 ArrayList 内部容量至少有 minCapacity 个
private void ensureExplicitCapacity(int minCapacity) {
    // modCount 是用于存储当前这个 ArrayList 被修改的次数,是为了防止多线程操作出现的异常。
    // 修改次数+1
    modCount++;

    // minCapacity 是有符号的 int, 如果大于 2147483647 会变成负数, 即溢出
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        // 如果 elementData 的大小不够,就调用 grow() 去扩容;
        grow(minCapacity);
}

/*
 * 真正执行扩容的方法
 */
private void grow(int minCapacity) {
    // overflow-conscious code

    // 获取当前容量
    int oldCapacity = elementData.length;
    // 按照当前长度的 1.5 倍,计算扩容后的容量
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 如果扩容后的容量小于参数 minCapacity,就把参数 minCapacity 作为扩容后的容量
    // newCapacity 是有符号 int, 意在防止溢出
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    // 如果扩容后的容量大于 MAX_ARRAY_SIZE ,调用 hugeCapacity(int minCapacity) 函数
    // private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // minCapacity is usually close to size, so this is a win:
    // 调用 Arrays.copyOf 完成扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 超大容量计算
private static int hugeCapacity(int minCapacity) {
    // minCapacity 溢出, 抛出 OOM 错误
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

四、面试题

基本操作、构造函数、扩容机制、缩容机制、底层数据结构、线程安全问题

ArrayList 如何执行向特定 index 添加元素的 add 方法?

  • 判断index位置是否合法;
  • 判断数组容量是否满足,不满足进行扩容操作;
  • 将index位置开始,到最后一个位置的所有元素,拷贝到原数组index+1开始的位置;
  • 在index位置插入元素;

ArrayList 是如何移除一个元素的?

首先根据索引或者删除元素,找到预删除元素位置,将该位置后面的所有元素向前移动一个位置,并将数组最后一个位置值置为 null 。

如何复制某个 ArrayList 到另一个 ArrayList ?

1)使用clone()方法

使用 clone 方法时,需要注意深拷贝和浅拷贝的问题。

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<String>();
    list.add("云");
    list.add("烟");
    list.add("成");
    list.add("雨");
    Object o = list.clone();
    System.out.println(o);
    System.out.println(list);
}

2)使用 ArrayList 构造方法

 public static void main(String[] args) {
     ArrayList<String> list = new ArrayList<String>();
     list.add("aaa");
     list.add("bbb");
     list.add("ccc");
     ArrayList<String> list1 = new ArrayList<>(list);
     for (String s : list1) {
         System.out.println(s);
     }
 }

3)使用 addAll 方法

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.add("Hello");
    list.add("world");
    list.add("!");
    ArrayList<String> list1 = new ArrayList<>();
    list1.addAll(list);
    System.out.println(list);
    System.out.println(list1);
}

ArrayList 的默认构造函数会不会初始化数组的容量?

不会,第一次 add 元素时候,才会初始化数组容量为 10。

ArrayList 添加大量元素导致频繁扩容,如何优化添加操作的性能?

使用 ArrayList 时,可以 new ArrayList(初始容量大小) 构造方法来指定集合初始化的大小,以减少扩容的次数,提高写入效率。

谈谈 ArrayList 的扩容机制?

  • ArrayList 以无参构造方法创建 ArrayList 时,初始化赋值的是一个静态 Object 空数组,当真正对数组进行添加元素操作(add)时,才真正分配容量。当向 ArrayList 中添加第一个元素时,数组容量默认扩容为 10。
  • 当需要扩容时,ArrayList每次扩容都以原来容量的1.5倍进行扩容。
  • 如果新数组长度不能满足容量要求 minCapacity,将数组长度扩容至 minCapacity 大小
  • 如果数组长度超过最大容量 MAX_ARRAY_SIZE(MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,-8 是为了避免 OOM),则会调用hugeCapacity()方法,将数组长度扩容至 Integer.MAX_VALUE 大小;
  • 最后通过 Arrays.copyOf() 方法将原数组中的内容放到扩容后的新数组里面。

扩容 1.5 倍后,有没有可能仍然满足不了容量要求?

有可能。当使用 addAll 方法时,新数组扩容1.5倍后,如果容量仍然不能满足容量要求,会将数组大小直接一次扩容至原数组长度与addAll 预添加数组的长度之和。

ArrayList 缩容了解吗?

ArrayList可以将数组长度缩小至当前元素个数,缩容函数 trimToSize() 方法,通常不会主动调用。

    /**
     * Trims the capacity of this <tt>ArrayList</tt> instance to be the
     * list's current size.  An application can use this operation to minimize
     * the storage of an <tt>ArrayList</tt> instance.
     */
    public void trimToSize() {
        // // modCount 是用于存储当前这个 ArrayList 被修改的次数,是为了防止多线程操作出现的异常。
        // 修改次数+1
        modCount++;

        // 如果当前存储的元素个数小于当前容量
        if (size < elementData.length) {
            // 如果当前存储的元素个数为 0, 则令 elementData 回到初始时的静态 Object 空数组
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

ArrayList 和 LinkedList 有什么区别?

1)底层数据结构

ArrayList 底层使⽤的是 Object 数组; LinkedList 底层使⽤的是双向链表数据结构。(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别。)

2)是否支持快速随机访问

LinkedList 不⽀持⾼效的随机元素访问,⽽ArrayList⽀持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)。

3)内存空间占用

ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList 插入或删除元素一定比 LinkedList 慢吗?

取决于删除的元素离数组末端有多远。

ArrayList 用来做队列合适吗?

队列一般是 FIFO(先入先出)的,如果用 ArrayList 做队列,需要在数组尾部追加数据,数组头部删除数组,反过来也可以。但是无论如何总会有一个操作会涉及到数组的数据搬迁,这个是比较耗费性能的。

如果是定长数组做环形队列是非常合适的,比如 ArrayBlockingQueue 内部实现就是一个环形队列,它是一个定长队列,内部是用一个定长数组来实现的。另外著名的 Disruptor 开源 Library 也是用环形数组来实现的超高性能队列,具体原理不做解释,比较复杂。简单点说就是使用两个偏移量来标记数组的读位置和写位置,如果超过长度就折回到数组开头,前提是它们是定长数组。

虽然 ArrayList 不适合做队列,不过 ArrayList 拿来作为堆栈来用还是挺合适的,push 和 pop 操作完全不涉及数据移动操作。事实上 JUC 的 Stack 就是继承 Vector 实现的,而 Vector 与 ArrayList 极为相像,但 Vector 是线程安全的。

ArrayList 是线程安全的吗?

ArrayList 不是线程安全的。

ArrayList 和 LinkedList 线程不安全该怎么解决?

1)Collections.synchronizedList(new LinkedList)

SynchronizedList 是 Collections 的内部类,Collections 提供了 synchronizedList 方法,可以将一个 线程不安全的 List 包装成线程安全的 List ,即 SynchronizedList 。它比 Vector 有更好的扩展性和兼容性,但是它所有的方法都带有同步锁,也不是性能最优的 List 。

2)将 ArrayList 和 LinkedList 换成线程安全的集合

如 CopyOnWriteArrayList 、ConcurrentLinkedQueue 等。

CopyOnWriteArrayLis t是 Java 1.5 在 java.util.concurrent 包下增加的类,它采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行 写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。在所有线程安全的 List 中,它是性能最优的方案。

3)使用 Vector
Vector 内部方法主要使用 synchronized 关键字

谈谈你对 CopyOnWriteArrayList 的理解

CopyOnWriteArrayList 是 Java 并发包(JUC)里提供的并发类,简单来说它就是一个线程安全且读操作无锁的 ArrayList 。正如其名字一样,在写操作时会复制一份新的 List ,在新的 List 上完成写操作,然后再将原引用指向新的 List 。这样就保证了写操作的线程安全。

CopyOnWriteArrayList 允许线程并发访问读操作,这个时候是没有加锁限制的,性能较高。而写操作的时候,则首先将容器复制一份,然后在新的副本上执行写操作,这个时候写操作是上锁的。结束之后再将原容器的引用指向新容器。注意,在上锁执行写操作的过程中,如果有需要读操作,会作用在原容器上。因此上锁的写操作不会影响到并发访问的读操作。

优点:读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。在遍历传统的 List 时,若中途有别的线程对其进行修改,则会抛出 ConcurrentModificationException 异常。而 CopyOnWriteArrayList 由于其”读写分离”的思想,遍历和修改操作分别作用在不同的 List 容器,所以在使用迭代器进行遍历时候,也就不会抛出 ConcurrentModificationException 异常了。

缺点:一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁 GC 。二是无法保证实时性,Vector 对于读写操作均加锁同步,可以保证读和写的强一致性。而 CopyOnWriteArrayList 由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

为什么使用 foreach 循环对 ArrayList 进行添加或删除操作会报错,而迭代器不会?

ArrayList 有个成员变量 modCount ,这个是用来记录 ArrayList 结构被改变的次数,add()、remove() 和 clear() 都会令 modCount++ 。

for (String str : list) 调用的实际上是 ArrayList 中的内部类 Itr,Itr 是对 Iterator 的实现。从源码中可以看到 Itr 实现了 Iterator 接口,同时声明了 expectedModCount 这个成员变量, expectedModCount 表示对 ArrayList 修改次数的期望值,它的初始值为 modCount。

Itr 源码:

/**
 * Returns an iterator over the elements in this list in proper sequence.
 *
 * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
 *
 * @return an iterator over the elements in this list in proper sequence
 */
public Iterator<E> iterator() {
    return new Itr();
}

/**
 * An optimized version of 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
    int expectedModCount = modCount;

    Itr() {}

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

从 ArrayList.Itr 的源码中可以看到,Itr 的 next 和 remove 方法里面都调用了一个 checkForComodification 方法来检查 modCount 和 expectedModCount 的值是否相等,若不相等,则抛出 ConcurrentModificationException 异常。

并且 remove 方法中有这样一行代码:expectedModCount = modCount;,说明 Itr 的 remove 方法有对 expectedModCount 重新赋值。

而 ArrayList 中的 remove 方法实际上是调用了 fastRemove 方法完成删除的,而 fastRemove 方法不会更新 expectedModCount 的值,因为 ArrayList.Itr 这个类不是静态内部类,都还没有实例化,何来更新 expectedModCount 的值这一说法呢?

REF

https://www.nowcoder.com/discuss/833699
https://www.nowcoder.com/discuss/821377
https://www.nowcoder.com/discuss/33405

https://www.nowcoder.com/discuss/94162
https://developer.aliyun.com/article/719156
https://www.nowcoder.com/discuss/916724