共计 13064 个字符,预计需要花费 33 分钟才能阅读完成。
本文已收录到 GitHub · AndroidFamily,有 Android 进阶常识体系,欢送 Star。技术和职场问题,请关注公众号 [彭旭锐] 退出 Android 交换群。
前言
大家好,我是小彭。
在后面的文章里,咱们学习了很多数据结构与算法思维。在理论的业务开发中,往往不须要咱们手写数据结构,而是间接应用规范库的数据结构 / 容器类。
在后续的文章里,咱们将以 Java 语言为例,剖析从 ArrayList 到 LinkedHashMap 等一系列规范库容器类,最初再有一篇总结回顾,请关注。
学习路线图:
1. 说一下 ArrayList 和 LinkedList 的区别?
- 1、数据结构: 在数据结构上,ArrayList 和 LinkedList 都是“线性表”,都继承于 Java 的
List
接口。另外 LinkedList 还实现了 Java 的Deque
接口,是基于链表的栈或队列,与之对应的是ArrayDeque
基于数组的栈或队列; - 2、线程平安: ArrayList 和 LinkedList 都不思考线程同步,不保障线程平安;
-
3、底层实现: 在底层实现上,ArrayList 是基于动静数组的,而 LinkedList 是基于双向链表的。事实上,它们很多个性的区别都是因为底层实现不同引起的。比如说:
- 在遍历速度上: 数组是一块间断内存空间,基于局部性原理可能更好地命中 CPU 缓存行,而链表是离散的内存空间对缓存行不敌对;
- 在访问速度上: 数组是一块间断内存空间,反对 O(1) 工夫复杂度随机拜访,而链表须要 O(n) 工夫复杂度查找元素;
- 在增加和删除操作上: 如果是在数组的开端操作只须要 O(1) 工夫复杂度,但在数组两头操作须要搬运元素,所以须要 O(n) 工夫复杂度,而链表的删除操作自身只是批改援用指向,只须要 O(1) 工夫复杂度(如果思考查问被删除节点的工夫,复杂度剖析上仍然是 O(n),在工程剖析上还是比数组快);
- 额定内存耗费上: ArrayList 在数组的尾部减少了闲置地位,而 LinkedList 在节点上减少了前驱和后继指针。
2. ArrayList 源码剖析
这一节,咱们来剖析 ArrayList 中次要流程的源码。
2.1 ArrayList 的属性
ArrayList 的属性很好了解,底层是一个 Object 数组,我要举手发问:
- 🙋🏻♀️疑难 1: 为什么 elementData 字段不申明
private
关键字? - 🙋🏻♀️疑难 2: 为什么 elementData 字段申明
transient
关键字? - 🙋🏻♀️疑难 3: 为什么 elementData 字段不申明为泛型类型
E
? - 🙋🏻♀️疑难 4: 为什么 ArrayList 的最大容量是
Integer.MAX_VALUE
,Long.MAX_VALUE
不行吗? - 🙋🏻♀️疑难 5: 为什么 ArrayList 的最大容量是
MAX_VALUE - 8
,肯定会减8
吗?
这些问题咱们在剖析源码的过程中答复。疑难这么多,ArrayList 霎时不香了。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {// new ArrayList() 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// new ArrayList(0) 的全局空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// new ArrayList() 的全局空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 批改次数记录
protected transient int modCount = 0;
// 数组的最大长度
// 疑难 4:为什么 ArrayList 的最大容量是 Integer.MAX_VALUE,Long.MAX_VALUE 不行吗?// 疑难 5:为什么 ArrayList 的最大容量是 MAX_VALUE - 8,肯定会减 8 吗?private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
// 疑难 1:为什么不申明 private(后文答复)// 疑难 2:为什么申明 transient(后文答复)// 疑难 3:为什么不申明为泛型类型 E
// 底层数组
transient Object[] elementData;
// 数组的无效长度(不是 elementData.length)private int size;
// size() 返回的是数组的无效长度(正当,底层数组咱们不关怀)public int size() {return size;}
}
2.2 ArrayList 的构造方法
ArrayList 有三个构造函数:
- 1、带初始容量的构造方法: 如果初始容量大于 0,则创立指定长度的数组。如果初始容量是 0,则指向第 1 个全局空数组;
EMPTY_ELEMENTDATA
; - 2、无参构造方法: 指向第 2 个全局空数组
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
; - 3、带汇合的构造方法: 将汇合转为数组,如果数组为空,则指向第 1 个全局空数组
EMPTY_ELEMENTDATA
;
能够看到,除了指定大于 0 的初始容量外,ArrayList 在结构时不会创立数组,而是指向全局空数组,这是懒初始化的策略。
结构器的源码不难,但小朋友总有太多的问号,举手发问 🙋🏻♀️:
- 🙋🏻♀️疑难 6:既然都是容量为 0,为什么 ArrayList 要辨别出 2 个空数组?
这个问题间接答复吧:ArrayList 认为无参构造函数应该应用默认行为,在首次增加数据时会创立长度为 10(DEFAULT_CAPACITY)
的默认初始数组;而显示设置初始容量为 0 是开发者的显式用意,所以不应用默认初始数组,在首次增加数据时只会创立长度为 1(size + 1)
的数组(能够联合后文源码了解下)。
- 🙋🏻♀️疑难 7: 在带汇合的构造方法中,为什么会存在汇合转化后的数组类型不是 Object[].class 的状况?
// 带初始容量的构造方法
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {
// 创立 initialCapacity 长度的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 指向第 1 个全局空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 不非法的初始容量
throw new IllegalArgumentException("Illegal Capacity:"+initialCapacity);
}
}
// 无参构造方法
public ArrayList() {
// 指向第 1 个全局空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 带汇合的构造方法
public ArrayList(Collection<? extends E> c) {
// 将汇合转为数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {// 疑难 7:这一个条件语句好奇怪,toArray() 的返回值类型就是 Object[] 啊?if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {this.elementData = EMPTY_ELEMENTDATA;}
}
public Object[] toArray() {return Arrays.copyOf(elementData, size);
}
2.3 ArrayList 的增加与扩容办法
ArrayList 能够在数组开端或数组两头增加元素:
- 如果是在数组开端增加,均摊工夫只须要 O(1) 工夫复杂度;
- 如果在数组两头增加,因为须要搬运数据,所以须要 O(n) 工夫复杂度。
增加前会先检查数据容量,有余会先扩容:
- 在应用无参结构器初始化时,首次增加元素时会间接扩容到 10 的容量;
- 在其余状况,会间接扩容到旧容量的 1.5 倍,而不是扩容到最小容量要求。
不论是扩容到 10 还是扩容到 1.5 倍,都是为了避免频繁扩容,防止每次 add 增加数据都要扩容一次。
当初,咱们能够回到一些小朋友的疑难:
- 🙋🏻♀️疑难 4:为什么 ArrayList 的最大容量是
Integer.MAX_VALUE
,Long.MAX_VALUE
不行吗?
实质上看,应该说是数组长度的限度,而不是 ArrayList 长度的限度。在《对象的内存分为哪几个局部?》这篇文章里,咱们探讨过对象的内存布局。数组对象的长度是记录在对象头中的“数组长度”字段中,这个字段是 4 字节,正好就是 Integer 也是 4 个字节,所以限度为 Integer.MAX_VALUE
,而不能应用 Long.MAX_VALUE
。
不对啊,Java Integer 是有符号整数,所以 Integer.MAX_VALUE
只有 31 位无效位,还少了 1 位呢。没错,是少了一位。如果要榨干这 1 位容量,当然能够用 long 类型并且限度到 32 位可能示意的最大正整数上,并且在源码中到处加上数组越界判断,想想就不稳固的。相比之下,限度数组长度为 int 类型且最大长度为 Integer.MAX_VALUE
,如果有超过 Integer.MAX_VALUE
存储容量的需要,那就创立两个数组呀:)你感觉哪种更好。
Java 对象内存布局
- 🙋🏻♀️疑难 5:为什么 ArrayList 的最大容量是
MAX_VALUE - 8
,肯定会减8
吗?
仍然与对象的内存布局无关。在 Java 虚拟机垃圾回收算法中,须要计算对象的内存大小,计算结果是存储在 jint
类型变量(Java int 类型在 JNI 中的映射)中的。如果数组的长度是 MAX_VALUE
,那么加上对象头之后就整型溢出了,所以 ArrayList 会事后减掉对象头可能占用的 8 个字节。对象头的具体大小取决于虚拟机实现,减 8 是绝对激进的。
其实,ArrayList 的最大容量也不肯定会减 8,如果最小容量要求是超过 MAX_ARRAY_SIZE
的,那么还是会扩容到 MAX_VALUE
。这有点摆烂的意思,会不会溢出运行时再说。
数组长度溢出
OutOfMemoryError: Requested array size exceeds VM limit
- 🙋🏻♀️疑难 8:不应该是 elementData.length – minCapacity > 0 吗? 这是思考到整型溢出的状况,minCapacity 溢出就变成正数了。
// 在数组开端增加元素
public boolean add(E e) {
// 先确保底层数组容量足够包容 size + 1,有余会扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 在 size + 1 的地位赋值
elementData[size++] = e;
return true;
}
// 在数组两头插入元素
public void add(int index, E element) {
// 范畴查看
rangeCheckForAdd(index);
// 先确保容量足够包容 size + 1,有余会扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 先搬运数据腾出空位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 在 index 的地位赋值
elementData[index] = element;
// 长度加一
size++;
}
// 在数组开端增加汇合
public boolean addAll(Collection<? extends E> c) {
// 汇合转数组
Object[] a = c.toArray();
// 先确保底层数组容量足够包容 size + numNew,有余会扩容
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
// 搬运原数据
System.arraycopy(a, 0, elementData, size, numNew);
// 长度加 numNew
size += numNew;
return numNew != 0;
}
// 在数组两头插入汇合
public boolean addAll(int index, Collection<? extends E> c) {// 略,原理相似}
// 尝试扩容
//(提醒:源码调用了 calculateCapacity() 函数,这里做内联简化)private void ensureCapacityInternal(int minCapacity) {
// 应用无参结构器初始化时,指定扩容为 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 疑难 8:不应该是 elementData.length - minCapacity > 0 吗?// 如果底层数组长度不够 minCapacity 最小容量要求,则须要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// 旧容量
int oldCapacity = elementData.length;
// 新容量 = 旧容量 * 1.5 倍(有可能整型溢出)int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量小于最小容量要求,则应用最小容量(addAll 大汇合的状况)if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}
//(提醒:上一个 if 的 newCapacity 有可能是溢出的)// 如果新容量超出最大数组长度限度,阐明无奈扩容 1.5 倍,回归到 minCapacity 上
//(提醒:源码调用了 hugeCapacity() 函数,这里做内联简化)if (newCapacity - MAX_ARRAY_SIZE > 0) {
// 最小容量要求产生整型溢出,无奈满足要求,只能间接抛出 OOM
if (minCapacity < 0) throw new OutOfMemoryError();
// 如果最小容量要求超出最大数组长度限度,则扩容到 MAX_VALUE(阐明不肯定会减 8)// 否则,扩容到最大数组长度限度(MAX_VALUE - 8)newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
// 扩容到 newCapacity 长度
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {// 曾经内联简化到 grow 办法中}
除了扩容之外,ArrayList 还反对缩容,将底层数组的容量放大到理论元素的数量:
// 缩容
public void trimToSize() {
modCount++;
if (size < elementData.length) {elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
}
}
另外,因为扩容波及到数据搬运操作,所以如果能当时晓得数据的容量,最好在创立 ArrayList 时就显式指定数据量大小。
2.4 ArrayList 的迭代器
Java 的 foreach 是语法糖,实质上也是采纳 iterator 的形式。ArrayList 提供了 2 个迭代器:
- iterator():Iterator(): 单向迭代器
- ListIterator<E> listIterator(): 双向迭代器
在迭代器遍历数组的过程中,有可能呈现多个线程并发批改数组的状况,造成数据不统一甚至数组越界,所以 Java 很多容器类的迭代器中都有 fail-fast 机制。
如果在迭代的过程中发现 expectedModCount 变动,阐明数据被批改,此时就会提前抛出 ConcurrentModificationException
异样(当然也不肯定是被其余线程批改)。
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;
...
Itr() {}
public boolean hasNext() {return cursor != size;}
@SuppressWarnings("unchecked")
public E next() {
// 查看
checkForComodification();
...
}
public void remove() {
...
// 更新
expectedModCount = modCount;
}
}
- 🙋🏻♀️疑难 1:为什么 elementData 字段不申明
private
关键字?
在正文中的解释是:“non-private to simplify nested class access”。但咱们晓得在 Java 中,外部类是能够拜访外部类的 private 变量的,所以这就说不通的。我的了解是:因为外部类在编译后会生成独立的 Class 文件,如果外部类的 elementData 字段是 private 类型,那么编译器就须要在 ArrayList 中插入 getter / setter,并通过办法调用,而 non-private 字段就能够间接拜访字段。
2.5 ArrayList 的序列化过程
- 🙋🏻♀️疑难 2:为什么 elementData 字段申明
transient
关键字?
ArrayList 重写了 JDK 序列化的逻辑,只把 elementData 数组中无效元素的局部序列化,而不会序列化整个数组。
// 序列化和反序列化只思考无效元素
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// 写入数组长度
s.writeInt(size);
// 写入无效元素
for (int i=0; i<size; i++) {s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {throw new ConcurrentModificationException();
}
}
2.6 ArrayList 的 clone() 过程
ArrayList 中的 elementData 数组是援用类型,因而在 clone() 中须要实现深拷贝,否则原对象与克隆对象会相互影响:
public Object clone() {
try {ArrayList<?> v = (ArrayList<?>) super.clone();
// 拷贝数组对象
v.elementData = Arrays.copyOf(elementData, size);
// 批改计数归零
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
2.7 为什么阿里巴巴要求审慎应用 subList API?
在《阿里巴巴 Java 开发手册》中,有对于 ArrayList#subList
API 的规定。为什么阿里巴巴要做这样的限度呢?
- 【强制】ArrayList 的 subList 后果不可强转成 ArrayList,否则会抛出 ClassCastException 异样;
- 【强制】在 subList 场景中,高度留神对原汇合元素的减少或删除,均会导致子列表的遍历、减少、删除产生 ConcurrentModificationException 异样。
这是因为 subList API 只是提供通过起始索引 fromIndex 和终止索引 toIndex 包装了一个原 ArrayList 的 “视图窗口”,并不是真的截取并创立了一个新的 ArrayList,所以强制类型转换就会抛出 ClassCastException 异样。
此时,在 ArrayList 或 SubList 上做批改,要留神相互之间的影响:
- 在 ArrayList 或 SubList 上批改元素,都会同步更新到对方(因为底层都是 ArrayList 自身);
- 在 SubList 上减少或删除元素,会影响到 ArrayList;
- 在 ArrayList 上减少或删除元素,会导致 SubList 抛出 ConcurrentModificationException 异样。
ArrayList.java
public List<E> subList(int fromIndex, int toIndex) {subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
private class SubList extends AbstractList<E> implements RandomAccess {
// 原 ArrayList
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
// modCount 记录
this.modCount = ArrayList.this.modCount;
}
public E set(int index, E e) {rangeCheck(index);
// 在 ArrayList 上减少或删除元素,会导致 SubList 抛出 ConcurrentModificationException 异样
checkForComodification();
// 在 SubList 上减少或删除元素,会影响到 ArrayList;E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
}
2.8 ArrayList 如何实现线程平安?
有 4 种形式:
- 办法 1 – 应用 Vector 容器: Vector 是线程平安版本的数组容器,它会在所有办法上减少 synchronized 关键字;
- 办法 2 – 应用 Collections.synchronizedList 包装类: 原理也是在所有办法上减少 synchronized 关键字;
- 办法 3 – 应用 CopyOnWriteArrayList 容器: 基于加锁的“读写拆散”和“写时复制”实现的动静数组,适宜于读多写少,数据量不大的场景。
- 办法 4 – 应用 ArrayBlockingQueue 容器: 基于加锁的阻塞队列,适宜于带阻塞操作的生产者消费者模型。
提醒: 对于 CopyOnWriteArrayList 的更多内容,去看看专栏文章《CopyOnWriteArrayList 是如何保障线程平安的?》
3. Arrays#ArrayList:世界上的另一个我
事实上,在 Java 环境中有两个 ArrayList,这或者是一个暗藏的彩蛋(坑):
- ArrayList: 个别认为的 ArrayList,是一个顶级类;
- Arrays#ArrayList: Arrays 的动态外部类,和下面这个 ArrayList 没有任何关系。
其实,Arrays#ArrayList 的定位就是在数组和和 List 间接切换而已。Arrays 提供了数组转 List 的 API,而 Arrays#ArrayList 也提供了 List 转数组的 API(这些 API 第一个 ArrayList 中也都有…)
回过头看剩下的 2 个问题:
- 🙋🏻♀️疑难 3:为什么 elementData 字段不申明为泛型类型
E
?
泛型擦除后等于 Object[] elementData,没有区别。
- 🙋🏻♀️疑难 7:在带汇合的构造方法中,为什么会存在汇合转化后的数组类型不是 Object[].class 的状况?
这是因为有些 List 汇合的底层数组不是 Object[] 类型,有可能是 String[] 类型。而在 ArrayList#toArray() 办法中,返回值的类型是 Object[] 类型,有类型谬误危险。
例如:在这段代码中,ArrayList 接管一个由 String 数组转化的 List,最初在 ArrayList#toArray() 返回的 Object 数组中增加一个 Object 对象,就出现异常了:
示例代码
// 假如没有非凡解决
List<String> list = new ArrayList<>(Arrays.asList("list"));
// class java.util.ArrayList
System.out.println(list.getClass());
Object[] listArray = list.toArray();
// 如果过没有非凡解决,理论类型是 [Ljava.lang
System.out.println(listArray.getClass());
// 如果过没有非凡解决,将抛出 ArrayStoreException 异样
listArray[0] = new Object();
源码摘要:
Arrays.java
public static <T> List<T> asList(T... a) {return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable {// 泛型擦除后:Object[] a;
private final E[] a;
// 泛型擦除后:Object[] array;
// Java 数组是协变的,可能接管 String[] 类型的数组
ArrayList(E[] array) {
// 赋值
a = Objects.requireNonNull(array);
}
// 理论返回的数组可能是 Object[] 类型,也可能是 String[] 类型
@Override
public Object[] toArray() {return a.clone();
}
}
ArrayList.java
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
transient Object[] elementData;
// 带汇合的构造方法
public ArrayList(Collection<? extends E> c) {
// 将汇合转为数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {// 疑难 7:这一个条件语句好奇怪,toArray() 的返回值类型就是 Object[] 啊?if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {this.elementData = EMPTY_ELEMENTDATA;}
}
public Object[] toArray() {return Arrays.copyOf(elementData, size);
}
}
4. ArrayList 这么好用,能够齐全代替数组吗?
大多数场景能够,但不能齐全代替。
ArrayList 是基于 Object 数组封装的动静数组,咱们不须要关怀底层数组的数据搬运和扩容等逻辑,因而在大多数业务开发场景中,除非是为了最求极致的性能,否则间接应用 ArrayList 代替数组是更好的抉择。
那么,ArrayList 有哪些地方上比数组差呢?
- 举例 1 – ArrayList 等容器类不反对 int 等根本数据类型,所以必然存在装箱拆箱操作;
- 举例 2 – ArrayList 默认的扩容逻辑是会扩充到原容量的 1.5 倍,在大数据量的场景中,这样的扩容逻辑是否多余,须要打上问题;
-
举例 3 – ArrayList 的灵活性不够。ArrayList 不容许底层数据有空洞,所有的无效数据都会“压缩”到底层数组的首部。因而,当须要基于数组二次开发容器时,ArrayList 并不是一个好抉择。
- 例如,应用 ArrayList 开发栈的构造或者适合,能够在数组的尾部操作数据。但应用 ArrayList 开发队列就不适合,因为在数组的首部入队或出队须要搬运数据;
- 而数组没有这些束缚,咱们能够将数组设计为“环形数组”,就能够防止入队和出队时搬运数据。例如 Java 的
ArrayBlockingQueue
和 ArrayDeque 就是基于数组的队列。
5. 总结
- 1、ArrayList 是基于数组封装的动静数组,封装了操作数组时的搬运和扩容等逻辑;
- 2、在结构 ArrayList 时,除了指定大于 0 的初始容量外,ArrayList 在结构时不会创立数组,而是指向全局空数组,这是懒初始化的策略;
- 3、在增加数据时会先检查数据容量,有余会先扩容。首次增加默认会扩容到 10 容量,后续会扩容到旧容量的 1.5 倍,这是为了防止重复扩容;
- 4、因为扩容波及到数据搬运操作,所以如果能当时晓得数据的容量,最好在创立 ArrayList 时就显式指定数据量大小;
- 5、ArrayList 重写了序列化过程,只解决数组中无效的元素;
- 6、ArrayList 的 subList API 只是提供视图窗口,并不是创立新列表;
- 7、ArrayList 在大多数场景中能够代替数组,但在高性能和二次封装的场景中,ArrayList 无奈代替数组。
在下一篇文章里,咱们来探讨 ArrayList 的孪生兄弟 —— LinkedList。请关注。
参考资料
- 为什么阿里巴巴要求审慎应用 ArrayList 中的 subList 办法 —— HollisChuang 著
- ArrayList 源码解析,老哥,来一起温习一哈?—— iisheng 著
- [Arrays.asList(x).toArray().getClass() should be Object[].class](https://bugs.openjdk.org/brow…) —— OpenJDK
- Why I can’t create an array with large size? —— stack overflow