- 一、概要
- 二、示例代码
- 三、源码分析
- 四、面试题
- ArrayList 如何执行向特定 index 添加元素的 add 方法?
- ArrayList 是如何移除一个元素的?
- 如何复制某个 ArrayList 到另一个 ArrayList ?
- ArrayList 的默认构造函数会不会初始化数组的容量?
- ArrayList 添加大量元素导致频繁扩容,如何优化添加操作的性能?
- 谈谈 ArrayList 的扩容机制?
- 扩容 1.5 倍后,有没有可能仍然满足不了容量要求?
- ArrayList 缩容了解吗?
- ArrayList 和 LinkedList 有什么区别?
- ArrayList 插入或删除元素一定比 LinkedList 慢吗?
- ArrayList 用来做队列合适吗?
- ArrayList 是线程安全的吗?
- ArrayList 和 LinkedList 线程不安全该怎么解决?
- 谈谈你对 CopyOnWriteArrayList 的理解
- 为什么使用 foreach 循环对 ArrayList 进行添加或删除操作会报错,而迭代器不会?
- REF
一、概要
- ArrayList 不是线程安全的;
- ArrayList 底层是使用一个 Object 数组来存储数据的;
- ArrayList 可以存放任何元素,包括 null;
- 查找效率高,但增删慢(在 ArrayList 末尾增删很快,但在中间增删需要移动后面一部分的数组元素,效率低下);
ArrayList 基本等同于 Vector ,除了 ArrayList 是线程不安全(执行效率相对高一些),在多线程情况下,不建议使用 ArrayList 作为共享变量。
扩容操作会阻塞 add 操作,造成新增元素效率低下,可以通过指定初始化容量,在一定程度上缓解这个问题;
- 数组无法存储大数据量(因为很难找到一块很大的连续的内存空间)
二、示例代码
package test;import java.util.ArrayList;import java.util.Iterator;import java.util.ListIterator;import java.util.Objects;public class CollectionTest {public static void main(String[] args) {//2、添加元素Studen s1 = new Studen("张三", 15);Studen s2 = new Studen("李四", 16);Studen s3 = new Studen("王五", 17);Studen s4 = new Studen("钱六", 18);arrayList.add(s1);arrayList.add(s2);arrayList.add(s3);arrayList.add(s4);//3、删除元素//3.1 通过下标删除arrayList.remove(1);//3.2 使用对象引用删除arrayList.remove(s2);//3.3 使用对象删除(需要重写对象的equals方法)arrayList.remove(new Studen("李四", 16));//4、遍历元素//4.1 迭代器循环System.out.println("*********** 迭代器循环 *****************************");Iterator it = arrayList.iterator();while (it.hasNext()) {Studen s = (Studen) it.next();System.out.println(s.getName());}//4.2 列表迭代器 正向循环System.out.println("*********** 列表迭代器正向循环 *****************************");ListIterator lit = arrayList.listIterator();while (lit.hasNext()) {Studen s = (Studen) lit.next();System.out.println(s.getName());}//4.3 列表迭代器逆向循环System.out.println("*********** 列表迭代器逆向循环 *****************************");while (lit.hasPrevious()) {Studen s = (Studen) lit.previous();System.out.println(s.getName());}//4.4 增强forSystem.out.println("*********** 增强for *****************************");for (Object obj : arrayList) {Studen s = (Studen) obj;System.out.println(s);}//5、查找System.out.println("*********** 查找 *****************************");System.out.println(arrayList.indexOf(new Studen("李四", 16)));//6、判断对象是否存在System.out.println(arrayList.contains(new Studen("李四", 16)));System.out.println(arrayList.contains(s2));}}class Studen {String name;int age;public Studen(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}Studen studen = (Studen) o;return this.age == studen.age && this.name.equals(studen.name);}@Overridepublic String toString() {return "Studen{" +"name='" + name + '\'' +", age=" + age +'}';}@Overridepublic int hashCode() {return Objects.hash(name, age);}}
三、源码分析
3.1、继承关系
1)Serializable 标记性接口
标记为可序列化
2)Cloneable 标记性接口
标记为可克隆
3)RandomAccess 标记性接口
标记为可随机访问
3.1、底层数据结构
ArrayList 中维护了一个 Object 类型的数组 elmentData
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable{private static final long serialVersionUID = 8683452581122892189L;/*** Default initial capacity.*/private static final int DEFAULT_CAPACITY = 10;/*** Shared empty array instance used for empty instances.*/private static final Object[] EMPTY_ELEMENTDATA = {};/*** Shared empty array instance used for default sized empty instances. We* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when* first element is added.*/private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** The array buffer into which the elements of the ArrayList are stored.* The capacity of the ArrayList is the length of this array buffer. Any* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA* will be expanded to DEFAULT_CAPACITY when the first element is added.*/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 (一个静态空数组);
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
当创建 ArrayList 对象时,如果使用的是 ArrayList(int initialCapacity) 构造器,则初始 elementData 容量为 capacity。
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
