文章目录
- 一、List
- 二、Vector
- 三、ArrayList
-
- 1.构造方法
- [2.创建 ArrayList 对象](#2.创建 ArrayList 对象)
- [3.ArrayList 的常用方法](#3.ArrayList 的常用方法)
- [4.ArrayList 的遍历](#4.ArrayList 的遍历)
-
- [(1)普通 for + 下标](#(1)普通 for + 下标)
- (2)foreach
- (3)Iterator
- (4)ListIterator
- [5.ArrayList 的扩容机制](#5.ArrayList 的扩容机制)
-
- (2)扩容过程
- [(3)扩容为什么是 1.5 倍?](#(3)扩容为什么是 1.5 倍?)
- 6.二维ArrayList
- 四、LinkedList
-
- 1.构造方法及LinkedList的创建
- 2.LinkedList常用方法
- 3.LinkedList的遍历
-
- [(1)普通 for 循环](#(1)普通 for 循环)
- [(2)增强 for 循环 / Iterator](#(2)增强 for 循环 / Iterator)
- (3)ListIterator
- [五、ArrayList 和 LinkedList 的区别](#五、ArrayList 和 LinkedList 的区别)
- 六、面试问题
一、List
List 是 Java 集合框架中最常用的接口之一,代表一个有序、可重复的元素集合。Java 提供了多种 List 实现类,其中最常见的是 ArrayList、LinkedList 和 Vector
List 接口继承自 Collection,提供了对元素的有序操作。常用方法包括:
| 方法 | 说明 |
|---|---|
| add(E e) | 将元素添加到列表末尾 |
| get(int index) | 根据索引获取列表中的元素 |
| remove(int index) | 删除指定位置的元素 |
| size() | 返回列表中的元素个数 |
| contains(Object o) | 判断列表中是否包含指定元素 |
List 的主要实现类包括 ArrayList、LinkedList、Vector、Stack 以及 JUC 包下的 CopyOnWriteArrayList
二、Vector
Vector 与 ArrayList 一样基于动态数组实现,但它通过 synchronized 关键字保证了线程安全。不过,正是由于这把"全局锁",它在高并发场景下的性能表现远不如现代 JUC 并发容器。
Vector 的底层是一个 Object[] 数组(名为 elementData),它继承了 AbstractList,实现了 List、RandomAccess、Cloneable、Serializable 接口。
java
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
protected Object[] elementData; // 存储元素的数组
protected int elementCount; // 实际元素个数
protected int capacityIncrement; // 扩容增量(可自定义)
}
1.线程安全机制
Vector 通过在所有公开方法上添加 synchronized 关键字来保证线程安全,锁对象为当前 Vector 实例本身(即 this)
java
public synchronized boolean add(E e) { ... }
public synchronized E get(int index) { ... }
public synchronized E set(int index, E element) { ... }
public synchronized boolean remove(Object o) { ... }
public synchronized int size() { ... }
这种设计确保了单次方法调用的原子性,但也意味着任意时刻只能有一个线程执行操作,读写全部互斥,并发吞吐量极低。
2.扩容机制
Vector 的扩容策略更加灵活:
-
默认扩容:容量翻倍,即新容量 = 旧容量 × 2。
-
自定义增量:通过构造函数传入 capacityIncrement 参数,扩容时新容量 = 旧容量 + capacityIncrement(若该值 ≤ 0,则仍采用翻倍策略)。
java
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 若 capacityIncrement > 0,则按增量扩容;否则翻倍
int newCapacity = oldCapacity +
((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
3.Vector的淘汰
尽管 Vector 是线程安全的,但它属于方法级的粗暴同步:
-
锁粒度太粗(锁保护范围太大):读操作也加锁,多线程并发读取时仍需排队,无法发挥多核优势。(读操作本可以并发执行,却被强制串行化。)
-
复合操作不安全:单个方法原子,但 check-then-act仍会出错,需手动加锁。如下例所示,
java
Vector<Integer> vec = new Vector<>();
// 两个线程同时执行:如果 vec 中不包含 1,就添加 1
Thread t1 = new Thread(() -> {
if (!vec.contains(1)) { // 步骤1:检查
vec.add(1); // 步骤3:行动
}
});
Thread t2 = new Thread(() -> {
if (!vec.contains(1)) { // 步骤2:检查
vec.add(1); // 步骤4:行动
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(vec); // 可能输出 [1, 1] ------ 1 被添加了两次!
此时需要外部手动加锁,或使用并发容器提供的原子复合操作(例如 ConcurrentHashMap 提供的 putIfAbsent 等方法)
java
synchronized (vec) {
if (!vec.contains(1)) {
vec.add(1);
}
}
- 现代替代品更优:
- 读多写少 → CopyOnWriteArrayList(读无锁)。
- 读写均衡 → Collections.synchronizedList(new ArrayList<>())。
- 高并发随机写 → 手动使用 ReentrantReadWriteLock。
三、ArrayList
ArrayList源码如下,
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组实例(用于指定容量为0时的共享空数组)
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量的空数组实例(用于无参构造)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 真正存储元素的数组,transient 表示不参与序列化
transient Object[] elementData;
// 集合中实际元素个数
private int size;
// ...
}
- ArrayList 是泛型集合,使用时需要指定类型参数(如 ArrayList<Integer>)
- 实现 RandomAccess:底层数组存储,支持按下标快速随机访问(get(i) 通常 O(1))
- 实现 Cloneable:支持 clone(),但属于浅拷贝(元素对象本身不深拷贝)
- 实现 Serializable:支持序列化/反序列化(用于持久化/网络传输等场景)
- 线程不安全:多线程场景可选 Vector(同步开销大)或 CopyOnWriteArrayList(读多写少更合适)。
- 底层是连续数组,可动态扩容,属于动态顺序表;插入/删除中间元素需要搬移(通常 O(n))
1.构造方法
| 方法 | 解释 |
|---|---|
| ArrayList() | 无参构造 |
| ArrayList(int initialCapacity) | 指定顺序表初始容量 |
| ArrayList(Collection<? extends E> c) | 利用其他Collection构建 ArrayList |
(1)无参构造:先让 elementData 指向一个 默认空数组占位 (DEFAULTCAPACITY_EMPTY_ELEMENTDATA),此时 size=0、elementData.length=0。第一次 add 才分配底层数组,直接分配默认容量 10。
java
// 1. 无参构造:懒加载,初始是一个空数组
public ArrayList() {
//DEFAULTCAPACITY_EMPTY_ELEMENTDATA是只是暂时还没分配的数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
(2)指定初始容量:
java
public ArrayList(int initialCapacity) { // 指定初始容量的构造方法
if (initialCapacity > 0) { // 1) 如果容量 > 0
this.elementData = new Object[initialCapacity]; // 直接分配一个指定长度的 Object 数组作为底层存储
} else if (initialCapacity == 0) { // 2) 如果容量 == 0
this.elementData = EMPTY_ELEMENTDATA; // 不分配新数组,引用一个共享的空数组(明确表示"容量就是0")
} else { // 3) 如果容量 < 0
throw new IllegalArgumentException(
"Illegal Capacity: " + initialCapacity
); // 抛异常:容量不允许为负数
}
}
(3)传入一个已有的集合
java
// 3. 传入一个已有的集合:用集合里的元素来初始化 ArrayList
public ArrayList(Collection<? extends E> c) {
// 把集合转成数组,作为底层存储(注意:返回类型是 Object[],但运行时实际类型不一定真的是 Object[])
elementData = c.toArray();
// 把 size 设为数组长度;如果不为 0,说明集合里有元素
if ((size = elementData.length) != 0) {
// c.toArray() 可能返回"不是 Object[] 的数组"
// 例如某些集合实现可能返回 T[](运行时类型是 String[]、Integer[] 等)
// 但 ArrayList 内部希望 elementData 的运行时类型就是 Object[]
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class); // 拷贝成真正的 Object[],避免后续存取/扩容时的类型问题
} else {
// 如果集合为空:复用共享空数组,不额外分配
elementData = EMPTY_ELEMENTDATA;
}
}
2.创建 ArrayList 对象
java
// 1) 无参构造:默认容量策略
ArrayList<Integer> list1 = new ArrayList<>();
List<Integer> list1 = new ArrayList<>();
//2) 指定初始容量构造
ArrayList<Integer> list2 = new ArrayList<>(10);
List<Integer> list2 = new ArrayList<>(10);
// 3) 传入已有集合构造(用集合元素初始化)
List<Integer> src = Arrays.asList(1, 2, 3);
ArrayList<Integer> list3 = new ArrayList<>(src);
List<Integer> list3 = new ArrayList<>(src);
- ArrayList list = new ArrayList<>(); 既能用 List 里定义的方法,也能用 ArrayList 自己额外提供的方法(例如 ensureCapacity()、trimToSize() 等)
- List list1 = new ArrayList<>();是一个向上转型的操作,只能调用 List 接口暴露的方法(add/get/remove/size/...),不能直接调用 ArrayList 的特有方法
3.ArrayList 的常用方法
| 方法 | 解释 |
|---|---|
| boolean add(E e) | 尾插е |
| void add (int index, E element) | 将e插入到index位置 |
| boolean addAll(Collection<? extends E> c) | 尾插c中的元素 |
| E remove(int index) | 删除index位置元素 |
| boolean remove(Object o) | 删除遇到的第一个o |
| E get(int index) | 获取下标 index位置元素 |
| E set(int index, E element) | 将下标index位置元素设置为 element |
| void clear() | 清空 |
| boolean contains(Object o) | 判断o是否在线性表中 |
| int indexOf(Object o) | 返回第一个o所在下标 |
| int lastIndexOf(Object o) | 返回最后一个o的下标 |
| List<E> subList(int fromIndex,int tolndex) | 截取部分list |
java
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("JavaSE");
list.add("JavaWeb");
list.add("JavaEE");
list.add("JVM");
list.add("测试课程");
System.out.println(list);//[JavaSE, JavaWeb, JavaEE, JVM, 测试课程]
// 获取list中有效元素个数
System.out.println(list.size());//5
// 获取和设置index位置上的元素,注意index必须介于[0, size)间
System.out.println(list.get(1));//JavaWeb
list.set(1, "JavaWEB");
System.out.println(list.get(1));//JavaWEB
// 在list的index位置插入指定元素,index及后续的元素统一往后搬移一个位置
list.add(1, "Java数据结构");
System.out.println(list);//[JavaSE, Java数据结构, JavaWEB, JavaEE, JVM, 测试课程]
// 删除指定元素,找到了就删除,该元素之后的元素统一往前搬移一个位置
list.remove("JVM");
System.out.println(list);//[JavaSE, Java数据结构, JavaWEB, JavaEE, 测试课程]
// 删除list中index位置上的元素,注意index不要超过list中有效元素个数,否则会抛出下标越界异常
list.remove(list.size()-1);
System.out.println(list);//[JavaSE, Java数据结构, JavaWEB, JavaEE]
// 检测list中是否包含指定元素,包含返回true,否则返回false
if(list.contains("测试课程")){
list.add("测试课程");//未输出
}
// 查找指定元素第一次出现的位置:indexOf从前往后找,lastIndexOf从后往前找
list.add("JavaSE");//此时ArrayList为[JavaSE, Java数据结构, JavaWEB, JavaEE,JavaSE]
System.out.println(list.indexOf("JavaSE"));//0
System.out.println(list.lastIndexOf("JavaSE"));//4
// 使用list中[0, 4)之间的元素构成一个新的SubList返回,但是和ArrayList共用一个elementData数组
List<String> ret = list.subList(0, 4);
System.out.println(ret);//[JavaSE, Java数据结构, JavaWEB, JavaEE]
ret.set(0, "NewJavaSE");
System.out.println(ret);//[NewJavaSE, Java数据结构, JavaWEB, JavaEE]
System.out.println(list);//[NewJavaSE, Java数据结构, JavaWEB, JavaEE, JavaSE]
list.clear();
System.out.println(list.size());//0
}
注意:对于subList来说,不是创建了一个ArrayList的副本,而是直接持有了原 ArrayList 的 elementData 引用
由于 SubList 是原列表的一个视图,在通过 ret 迭代或操作期间,禁止对原列表 list 进行结构性修改(增删元素),否则会抛出 ConcurrentModificationException。
java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = list.subList(0, 2);
list.add("E"); // 原列表结构性修改(增加元素)
System.out.println(sub); // 抛出 ConcurrentModificationException
4.ArrayList 的遍历
(1)普通 for + 下标
java
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
注意:遍历时不建议直接结构性修改(容易漏/越界)
这里的结构性修改指list.add(e)、list.remove(index)改变列表大小的操作
例如在下面的循环里 remove(i),后面的元素会左移,很容易漏掉元素或 IndexOutOfBoundsException:
java
// 不推荐:可能漏删
for (int i = 0; i < list.size(); i++) {
if (list.get(i) % 2 == 0) {
list.remove(i); // 删除后元素左移,i++ 会跳过下一个元素
}
}
可以使用倒序来避免这个问题(更推荐使用 Iterator / removeIf)
java
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i) % 2 == 0) {
list.remove(i);
}
}
(2)foreach
java
for (Integer x : list) {
System.out.println(x);
}
注意:底层是 Iterator,如果在循环体里 list.add/remove(...) 等结构性修改,会触发 CME(ConcurrentModificationException),
java
for (Integer x : list) {
if (x % 2 == 0) {
list.remove(x); // 运行时很可能抛 ConcurrentModificationException
}
}
(3)Iterator
Iterator 是集合遍历的标准安全方式,允许在遍历过程中通过 iterator.remove() 安全删除元素,而不会触发 CME。
java
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
Integer x = it.next();
if (x % 2 == 0) {
it.remove(); // 正确:删除当前迭代到的元素
}
}
System.out.println(list); // [1, 3, 5]
iterator.remove() 删除的是上一次 next() 返回的元素,因此必须先调用 next() 再调用 remove();该方式仅支持删除,不支持结构性修改。
(4)ListIterator
ListIterator 是 Iterator 的增强版,专为 List 设计,提供了更丰富的操作能力,其支持以下方法,
| 方法 | 说明 |
|---|---|
| hasNext() / next() | 正向遍历 |
| hasPrevious() / previous() | 反向遍历 |
| nextIndex() / previousIndex() | 获取当前元素的前后索引 |
| add(E e) | 在当前迭代位置插入元素 |
| set(E e) | 替换上一次返回的元素 |
| remove() | 删除上一次返回的元素 |
a.正向与反向遍历
java
List<String> list = new ArrayList<>(List.of("A", "B", "C", "D"));
ListIterator<String> listIt = list.listIterator();
// 正向遍历
System.out.print("正向:");
while (listIt.hasNext()) {
System.out.print(listIt.next() + " ");
}
System.out.println();
// 反向遍历(必须先移动迭代器到末尾)
System.out.print("反向:");
while (listIt.hasPrevious()) {
System.out.print(listIt.previous() + " ");
}
System.out.println();
b.遍历中安全地增删改
java
List<String> list = new ArrayList<>(List.of("Java", "Python", "C++"));
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
String s = it.next();
if ("Python".equals(s)) {
it.set("Go"); // 修改当前元素
}
if ("C++".equals(s)) {
it.remove(); // 删除当前元素
}
if ("Java".equals(s)) {
it.add("Kotlin"); // 在当前元素之后插入新元素
}
}
System.out.println(list); // [Java, Kotlin, Go]
5.ArrayList 的扩容机制
ArrayList 扩容主要分为两种情况:
- 首次扩容:当使用 new ArrayList() 创建对象时,JDK 8+ 采用了延迟初始化(懒加载) 策略,此时底层的 elementData 是一个空数组。只有当我们第一次调用 add 方法时,才会触发扩容,分配一个长度为默认值 10 的新数组。
- 后续扩容:当连续添加元素,直到 size + 1(即所需的最小容量)大于当前数组 elementData.length 时,扩容才会被再次触发。
(2)扩容过程
创建新的数组容量(原来的1.5倍),并进行数组的复制
扩容的核心逻辑封装在 grow 方法中,下面逐行解析关键代码(基于 JDK 8+):
-
计算新容量 (newCapacity):首先计算一个默认的新容量,其值为 oldCapacity + (oldCapacity >> 1)。这里的 >> 1 是位运算,等价于除以 2,因此整个表达式的结果就是旧容量的 1.5 倍。
-
容量修正 (minCapacity):如果计算出的 newCapacity 仍然小于当前添加元素所需的最小容量 minCapacity(例如通过 addAll 批量添加时),则直接将 minCapacity 作为新的容量值,确保能够容纳所有元素。
-
处理超大容量 (hugeCapacity):如果新容量超过了 MAX_ARRAY_SIZE(值为 Integer.MAX_VALUE - 8),会调用 hugeCapacity 方法做最终处理,确保容量不会越界。这是 JVM 为存储对象头保留的 8 个字节空间。
-
执行复制 (Arrays.copyOf):最后调用 Arrays.copyOf,这个方法内部会创建一个指定长度的新数组 ,并使用高效的 System.arraycopy(native 方法)将旧数组元素复制过去。这意味着扩容本质上是一个 O(n) 的操作,代价相对较高。
扩容操作涉及到数组的复制和内存的重新分配,在频繁添加大量元素时扩容可能会影响性能。可以在初始化ArrayList时预分配足够大的容量,避免带来性能损耗。
(3)扩容为什么是 1.5 倍?
1.5 倍是 Java 设计者在时间(扩容频率) 和空间(内存利用率) 之间寻找到的最佳平衡点:
- 若倍数太小(如 1.1 倍):优点是可减少内存浪费;但缺点也很明显,扩容会变得非常频繁,导致性能下降。例如从 10 扩容到 100,1.5倍只需要约 4 次扩容,而 1.1 倍则需要约 24 次。
- 若倍数太大(如 2 倍):优点是扩容次数少,性能损耗小;但缺点是可能造成巨大的内存浪费,尤其是在容量接近 2 的幂次方时
6.二维ArrayList
Java 里没有真正"二维 ArrayList"这种类型,通常用 嵌套泛型来表示二维结构:List<List<T>>。它本质是 外层列表存每一行(row)的引用,内层列表存该行的元素 。
下面代码中创建二维 ArrayList,外层 List表示多行;内层 List<Integer>表示一行的数据。
java
public static void main(String[] args) {
List<List<Integer>> list = new ArrayList<>();
List<Integer> row1 = new ArrayList<>();
row1.add(1);
row1.add(2);
list.add(row1);
List<Integer> row2 = new ArrayList<>();
row2.add(11);
row2.add(21);
list.add(row2);
System.out.println(list); // [[1, 2], [11, 21]]
int v = list.get(1).get(0); // 取第2行第1列 -> 11
list.get(0).set(1, 200);// // 把第1行第2列改成 200
System.out.println(list);//[[1, 200], [11, 21]]
list.add(new ArrayList<>()); // 新增空行
list.get(0).add(999); // 给第1行追加一个元素(相当于新增一列)
list.get(1).add(0, 777); // 在第2行指定位置插入
System.out.println(list); //[[1, 200, 999], [777, 11, 21], []]
}
可以使用下标遍历或者for- each 遍历二维 ArrayList
java
List<List<Integer>> jagged = new ArrayList<>();
jagged.add(new ArrayList<>(List.of(1, 2, 3)));
jagged.add(new ArrayList<>(List.of(4)));
//下标遍历
for (int i = 0; i < jagged .size(); i++) {
for (int j = 0; j < jagged .get(i).size(); j++) {
System.out.print(jagged .get(i).get(j) + " ");
}
System.out.println();
}
//for- each 遍历
for (List<Integer> row : list) {
for (Integer x : row) {
System.out.print(x + " ");
}
System.out.println();
}
四、LinkedList
LinkedList的源码如下
java
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// 链表长度(节点个数)
transient int size = 0;
// 指向第一个节点
transient Node<E> first;
// 指向最后一个节点
transient Node<E> last;
......
}
- AbstractSequentialList:提供顺序访问的骨架实现
- List接口:提供列表的基本操作
- Deque接口:支持双端队列操作(这是LinkedList的特色)
- Cloneable:支持克隆
- Serializable:支持序列化
三个成员变量都用transient修饰,意味着它们不会参与默认序列化
LinkedList的底层是一个双向链表,其节点定义如下,每个节点都持有对前驱和后继节点的引用。LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1),比较适合任意位置插入的场景。
java
private static class Node<E> {
E item; // 节点存储的元素
Node<E> next; // 指向下一个节点
Node<E> prev; // 指向上一个节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
1.构造方法及LinkedList的创建
| 方法 | 解释 |
|---|---|
| LinkedList() | 无参构造 |
| public LinkedList(Collection<? extends E> c) | 使用其他集合容器中元素构造List |
java
public static void main(String[] args) {
// 构造一个空的LinkedList
// 只能使用List接口定义的方法
List<Integer> list1 = new LinkedList<>();
//可以调用LinkedList类的所有公开方法
LinkedList<Integer> LinkedList = new LinkedList<>();
List<String> list2 = new java.util.ArrayList<>();
list2.add("JavaSE");
list2.add("JavaWeb");
list2.add("JavaEE");
// 使用ArrayList构造LinkedList
List<String> list3 = new LinkedList<>(list2);
}
2.LinkedList常用方法
| 方法 | 解释 |
|---|---|
| boolean add(E e) | 尾插е |
| void add(int index, E element) | 将e插入到 index位置 |
| boolean addAll(Collection<? extends E> c) | 尾插c中的元素 |
| E remove(int index) | 删除 index 位置元素 |
| boolean remove(Object o) | 删除遇到的第一个o |
| E get(int index) | 获取下标index位置元素 |
| E set(int index, E element) | 将下标index位置元素设置为element |
| void clear() | 清空 |
| boolean contains(Object o) | 判断o是否在线性表中 |
| int indexOf(Object o) | 返回第一个o所在下标 |
| int lastIndexOf(Object o) | 返回最后一个o的下标 |
| List<E> subList(int fromIndex, int toIndex) | 截取部分 list |
java
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(1); // add(elem): 表示尾插
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
System.out.println(list.size());//7
System.out.println(list);//[1, 2, 3, 4, 5, 6, 7]
// 在起始位置插入0
list.add(0, 0); // add(index, elem): 在index位置插入元素elem
System.out.println(list);//[0, 1, 2, 3, 4, 5, 6, 7]
list.remove(); // remove(): 删除第一个元素,内部调用的是removeFirst()
list.removeFirst(); // removeFirst(): 删除第一个元素
list.removeLast(); // removeLast(): 删除最后元素
list.remove(1); // remove(index): 删除index位置的元素
System.out.println(list);//[2, 4, 5, 6]
// contains(elem): 检测elem元素是否存在,如果存在返回true,否则返回false
if(!list.contains(1)){
list.add(0, 1);
}
list.add(1);
System.out.println(list);//[1, 2, 4, 5, 6, 1]
System.out.println(list.indexOf(1)); // indexOf(elem): 从前往后找到第一个elem的位置,输出0
System.out.println(list.lastIndexOf(1)); // lastIndexOf(elem): 从后往前找第一个1的位置,输出5
int elem = list.get(0); // get(index): 获取指定位置元素
list.set(0, 100); // set(index, elem): 将index位置的元素设置为elem
System.out.println(list);//[100, 2, 4, 5, 6, 1]
// subList(from, to): 用list中[from, to)之间的元素构造一个新的LinkedList返回
List<Integer> copy = list.subList(0, 3);
System.out.println(list);//[100, 2, 4, 5, 6, 1]
System.out.println(copy);//[100, 2, 4]
list.clear(); // 将list中元素清空
System.out.println(list.size());//0
}
3.LinkedList的遍历
(1)普通 for 循环
java
// 这种方式性能最差!
for (int i = 0; i < linkedList.size(); i++) {
System.out.print(linkedList.get(i) + " ");
}
LinkedList 的 get(int index) 方法需要从头或尾开始逐个遍历节点才能定位到目标位置
java
// LinkedList 源码中的 get 方法
public E get(int index) {
checkElementIndex(index);
return node(index).item; // node(index) 需要遍历链表
}
Node<E> node(int index) {
// 判断离头近还是离尾近,决定从哪端开始遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
此时,get(0) 需要遍历 0 次(直接返回头节点)。get(1) 需要遍历 1 次。get(n) 最坏需要遍历 n/2 次。因此使用普通 for 循环遍历整个 LinkedList,总时间复杂度为 O(n²)。
(2)增强 for 循环 / Iterator
java
// 底层使用迭代器,性能为 O(n)
for (String s : linkedList) {
System.out.print(s + " ");
}
增强 for 循环在编译后会被转换为 Iterator 遍历,等价于以下代码:
java
Iterator<String> it = linkedList.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.print(s + " ");
}
LinkedList 的迭代器内部维护了一个指向当前节点的指针,每次调用 next() 时只需将指针移动到下一个节点,时间复杂度为 O(1)。遍历整个链表的总时间复杂度为 O(n)。
(3)ListIterator
ListIterator 支持双向遍历,安全修改、插入、删除和获取索引
java
// 支持双向遍历 + 遍历中增删改
ListIterator<String> listIt = linkedList.listIterator();
// 正向遍历
while (listIt.hasNext()) {
String s = listIt.next();
if ("Java".equals(s)) {
listIt.set("Go"); // 修改当前元素
}
if ("Python".equals(s)) {
listIt.add("Kotlin"); // 在当前元素后插入新元素
}
}
// 反向遍历(必须先移动到末尾)
while (listIt.hasPrevious()) {
System.out.print(listIt.previous() + " ");
}
五、ArrayList 和 LinkedList 的区别
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层数据结构 | 动态数组(Object[]) | 双向链表(Node 节点) |
| 实现接口 | List, RandomAccess, Cloneable, Serializable | List, Deque, Cloneable, Serializable |
| 随机访问 get(index) | O(1) ✅ | O(n) ❌ |
| 尾部添加 add(e) | 均摊 O(1) | O(1) |
| 头部添加 add(0, e) | O(n)(需搬移元素) | O(1) ✅ |
| 中间插入 add(index, e) | O(n) | O(n)(定位慢)+ O(1)(指针修改) |
| 删除操作 | O(n)(需搬移元素) | O(1)(定位到目标节点后) |
| 内存占用 | 仅存储元素引用,内存紧凑 | 每个节点额外存储 prev 和 next 指针 |
| 遍历性能 | 普通 for 最优(O(n)) | 普通 for 是 O(n²) 灾难,必须用迭代器 |
| 扩容/扩容开销 | 1.5 倍扩容,需复制数组(O(n)) | 无需扩容,按需创建节点 |
| 线程安全 | ❌ | ❌ |
| 适用场景 | 读多写少,随机访问频繁 | 头尾增删频繁,用作队列/栈 |
六、面试问题
1.ArrayList源码中为什么需要两个不同的空数组?
- EMPTY_ELEMENTDATA:通过 new ArrayList(0) 或 new ArrayList(emptyCollection) 显式创建容量为 0 的 ArrayList。之后按照 1.5 倍规则扩容。
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA:通过 new ArrayList() 无参构造器创建 ArrayList,直接扩容到默认容量 10。
2.ArrayList的缺陷
- ArrayList 底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,不适合做任意位置插入和删除比较多的场景。
3.arraylist和vector 区别
- 两者底层都是基于动态数组实现的 List,但 Vector 是线程安全的(方法级 synchronized),而 ArrayList 是非线程安全的。
| 对比维度 | ArrayList | Vector |
|---|---|---|
| 线程安全 | 非线程安全 | 线程安全(所有方法 synchronized 修饰) |
| 性能 | 高(无锁开销) | 低(读写均需竞争同一把锁) |
| 扩容默认倍数 | 1.5 倍 | 2 倍(newCapacity = oldCapacity * 2,或按 capacityIncrement 增长) |
| 扩容增量自定义 | 不支持 | 支持构造参数 capacityIncrement |
| 初始容量 | 10(JDK 8+ 懒加载,首次 add 分配) | 10(构造时立即分配) |
| 遍历推荐方式 | 普通 for 最快(RandomAccess) | 普通 for 或迭代器(但均有锁竞争) |