ArrayList
概念
一、先理解核心概念
- 标记接口(Marker Interface) :Cloneable 是一个空接口(没有任何方法),它的作用仅仅是标记 一个类,表示该类允许被克隆。如果一个类实现了 Cloneable,但没有重写 clone () 方法,调用 clone () 会抛出
CloneNotSupportedException。 - 浅拷贝(Shallow Copy) :拷贝对象时,只复制对象本身(包括对象的基本数据类型成员变量),但对象中的引用类型成员变量(比如 List、自定义类对象)仅复制引用地址,新对象和原对象的引用类型成员指向同一个内存地址。
二、LinkedList 的 clone () 实现原理
LinkedList 重写了 clone () 方法,核心逻辑是:
- 创建一个新的 LinkedList 实例;
- 遍历原 LinkedList 的所有节点,将节点中的元素引用逐一复制到新 LinkedList 中;
- 最终返回这个新的 LinkedList,但新链表中的元素(如果是引用类型)和原链表指向同一个对象。
三、代码示例:直观理解浅拷贝
下面通过代码演示 LinkedList.clone () 的浅拷贝效果:
java
import java.util.LinkedList;
// 自定义引用类型(用于测试浅拷贝)
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// getter/setter
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; }
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
public class LinkedListCloneDemo {
public static void main(String[] args) {
// 1. 创建原 LinkedList 并添加引用类型元素
LinkedList<User> originalList = new LinkedList<>();
User user1 = new User("张三", 20);
originalList.add(user1);
// 2. 调用 clone() 进行浅拷贝
LinkedList<User> clonedList = (LinkedList<User>) originalList.clone();
// 3. 验证:新链表和原链表是不同的对象
System.out.println("原链表地址:" + originalList);
System.out.println("克隆链表地址:" + clonedList);
System.out.println("是否为同一对象:" + (originalList == clonedList)); // false
// 4. 验证:引用类型元素指向同一个对象(浅拷贝核心特征)
User originalUser = originalList.get(0);
User clonedUser = clonedList.get(0);
System.out.println("原链表元素地址:" + originalUser);
System.out.println("克隆链表元素地址:" + clonedUser);
System.out.println("元素是否为同一对象:" + (originalUser == clonedUser)); // true
// 5. 修改原元素,克隆链表的元素也会变(因为指向同一对象)
originalUser.setName("李四");
System.out.println("原链表元素:" + originalList.get(0)); // User{name='李四', age=20}
System.out.println("克隆链表元素:" + clonedList.get(0)); // User{name='李四', age=20}
}
}
四、关键细节解释
-
clone () 的返回值 :LinkedList.clone () 返回的是
Object类型,需要强制类型转换为LinkedList。 -
基本数据类型 vs 引用类型 :
- 如果 LinkedList 中存的是
Integer、String(不可变类型)等,浅拷贝几乎和 "深拷贝" 效果一致(因为不可变类型无法修改内部值); - 如果存的是自定义类(如上面的 User)、HashMap 等可变引用类型,修改原对象会影响克隆链表的元素。
- 如果 LinkedList 中存的是
-
深拷贝的实现(补充) :如果需要真正的深拷贝(新链表的元素是全新对象),需要自己实现:
java// 手动实现 LinkedList 深拷贝示例 public static LinkedList<User> deepCloneLinkedList(LinkedList<User> original) { LinkedList<User> deepClone = new LinkedList<>(); for (User user : original) { // 为每个元素创建新对象(深拷贝核心) User newUser = new User(user.getName(), user.getAge()); deepClone.add(newUser); } return deepClone; }
总结
- LinkedList 实现 Cloneable 接口的核心目的是允许通过 clone () 实现浅拷贝,生成新的 LinkedList 实例,但新实例和原实例共享引用类型元素;
- 浅拷贝的关键特征:新链表是独立对象,但链表中的引用类型元素仅复制地址,修改原元素会同步影响克隆链表;
- 若需深拷贝,需手动遍历链表并为每个引用类型元素创建新对象,而非直接使用 clone () 方法。
arraylist详细的讲解
一、ArrayList 核心定义与底层原理
ArrayList 是 Java 集合框架中 java.util 包下的动态数组实现 ,实现了 List 接口,弥补了 Java 普通数组长度固定的缺陷,核心特征如下:
1. 底层数据结构
- 本质是可变长度的数组 ,底层维护一个
transient Object[] elementData数组(transient表示该数组不参与默认序列化); - 初始化时如果不指定容量,默认初始容量是
10(JDK 8 及以上),也可以通过构造器new ArrayList(int initialCapacity)指定初始容量。
2. 扩容机制(核心)
当添加元素导致数组容量不足时,ArrayList 会自动扩容,核心逻辑:
- 扩容触发条件:
size + 1 > elementData.length(size是当前元素个数); - 扩容规则:新容量 = 原容量 × 1.5(JDK 8 中通过
oldCapacity + (oldCapacity >> 1)实现,位运算更高效); - 扩容过程:创建新的更大数组 → 复制原数组元素到新数组 → 替换原数组,这个过程是耗时操作,所以初始化时指定合适容量能提升性能。
二、ArrayList 关键特性
1. 访问效率高
- 基于数组实现,支持随机访问 (通过索引
get(int index)),时间复杂度为O(1); - 这是 ArrayList 对比 LinkedList 最核心的优势(LinkedList 随机访问是
O(n))。
2. 增删效率(分场景)
- 尾部增删(add (E e)/remove (size-1)) :效率高,时间复杂度
O(1)(无扩容时); - 中间 / 头部增删(add (int index, E e)/remove (int index)) :需要移动后续元素,时间复杂度
O(n),效率低; - 原因:数组元素在内存中是连续存储的,中间插入 / 删除会导致后续元素整体移位。
3. 线程不安全
- ArrayList 是非线程安全 的集合,多线程环境下同时读写会导致数据错乱(如
ConcurrentModificationException); - 解决方案:
- 使用
Collections.synchronizedList(new ArrayList<>())包装成线程安全版本; - 用
CopyOnWriteArrayList(并发包下,写操作时复制数组,读写分离,适合读多写少场景)。
- 使用
4. 空值与重复元素
- 允许存储
null值(可以通过add(null)添加); - 允许存储重复元素(如
add("a"); add("a")是合法的)。
5. Cloneable 与浅拷贝
- ArrayList 实现了
Cloneable接口,调用clone()方法会返回一个新的 ArrayList 实例,但同样是浅拷贝 :- 新 ArrayList 的数组是新对象,但数组中的元素(引用类型)仅复制引用地址;
- 示例:如果 ArrayList 中存自定义类对象,修改原对象会影响克隆后的 ArrayList 元素。
三、ArrayList 常用操作示例
下面通过代码演示 ArrayList 的核心操作,帮助你理解实际使用方式:
java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ArrayListDemo {
public static void main(String[] args) {
// 1. 创建 ArrayList(指定初始容量,避免频繁扩容)
List<String> list = new ArrayList<>(10);
// 2. 添加元素
list.add("Java"); // 尾部添加
list.add("Python");
list.add(1, "C++"); // 索引1处插入(中间插入,会移动元素)
list.add(null); // 允许null
// 3. 访问元素
String first = list.get(0); // 随机访问,O(1)
System.out.println("索引0的元素:" + first); // Java
// 4. 修改元素
list.set(3, "Go"); // 将索引3的null改为Go
System.out.println("修改后列表:" + list); // [Java, C++, Python, Go]
// 5. 删除元素
list.remove(1); // 删除索引1的元素(C++),后续元素移位
list.remove("Go"); // 删除指定元素(值匹配)
System.out.println("删除后列表:" + list); // [Java, Python]
// 6. 遍历元素(三种常用方式)
// 方式1:for循环(随机访问,适合ArrayList)
System.out.println("for循环遍历:");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 方式2:增强for循环
System.out.println("增强for循环遍历:");
for (String s : list) {
System.out.println(s);
}
// 方式3:迭代器(支持遍历中删除,避免ConcurrentModificationException)
System.out.println("迭代器遍历并删除:");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("Python")) {
iterator.remove(); // 迭代器删除,安全
}
}
System.out.println("迭代器删除后:" + list); // [Java]
// 7. 克隆(浅拷贝)
ArrayList<String> cloneList = (ArrayList<String>) ((ArrayList<String>) list).clone();
System.out.println("原列表地址:" + list);
System.out.println("克隆列表地址:" + cloneList);
System.out.println("是否同一对象:" + (list == cloneList)); // false
}
}
四、ArrayList vs LinkedList(核心区别)
为了帮你更清晰理解 ArrayList 的定位,对比 LinkedList 的关键差异:
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问(get (index)) | O (1)(高效) | O (n)(需遍历) |
| 增删(中间 / 头部) | O (n)(需移位) | O (1)(只需修改节点引用) |
| 增删(尾部) | O (1)(无扩容时) | O(1) |
| 内存占用 | 连续内存,有扩容冗余 | 每个节点存数据 + 前后引用,内存开销大 |
| 适用场景 | 读多写少、随机访问 | 写多读少、频繁增删 |
五、ArrayList 性能优化建议
- 初始化指定容量 :如果知道元素个数,创建时指定
new ArrayList(预计大小),避免扩容的数组复制开销; - 批量添加用 addAll () :相比多次调用
add(),addAll()减少扩容检查次数; - 遍历优先用 for 循环 :ArrayList 随机访问高效,for 循环(
get(index))比迭代器 / 增强 for 更高效; - 避免频繁中间增删:如果需要频繁在中间增删,优先用 LinkedList。
实现的接口
1. 核心接口:List<E>(最基础)
- 作用 :这是
ArrayList最核心的接口,继承自Collection和Iterable。 - 带来的能力 :定义了有序集合的核心方法,比如
add()、get(int index)、remove(int index)、size()等,保证ArrayList具备 "通过索引访问元素、可重复、有序" 的基本特性。
2. 标记接口:RandomAccess
- 作用:这是一个 "空接口"(仅作标记),没有任何方法。
- 带来的能力 :标记该集合支持快速随机访问 (通过索引
get(i)访问元素的时间复杂度为 O (1))。- 比如遍历
ArrayList时,用普通 for 循环(for (int i=0; i<size; i++))比迭代器遍历效率更高;而没有实现该接口的LinkedList则相反。
- 比如遍历
3. 标记接口:Cloneable
- 作用 :标记该类支持调用
clone()方法进行浅拷贝。 - 带来的能力 :可以通过
arrayList.clone()创建一个新的ArrayList(注意:元素是引用类型时,仅拷贝引用,不是深拷贝)。
4. 标记接口:java.io.Serializable
- 作用:标记该类的对象可以被序列化(转换为字节流)和反序列化(从字节流恢复对象)。
- 带来的能力 :
ArrayList对象可以通过网络传输、写入文件,或在分布式场景中使用(比如序列化到 Redis)。
补充:ArrayList 没有实现的关键接口
- 没有实现
Deque接口:因此没有addFirst()、addLast()、pollFirst()等双端队列方法(这也是和LinkedList的核心区别之一)。 - 没有实现
Set接口:因此允许元素重复,而HashSet等实现Set接口的集合不允许重复。
总结
ArrayList最核心的接口是List<E>,决定了它 "有序、可索引访问、可重复" 的核心特性。RandomAccess标记它支持快速随机访问,Cloneable支持浅拷贝,Serializable支持序列化。ArrayList未实现Deque接口,因此没有addFirst()/addLast()等双端队列方法,需手动封装才能使用这些功能。
常用的方法:
| 类别 | 核心方法 |
|---|---|
| 新增 | add()、addAll()、add(int index, E e) |
| 删除 | remove()、removeAll()、clear()、removeIf() |
| 修改 | set(int index, E e) |
| 查询 | get()、indexOf()、lastIndexOf()、contains()、isEmpty()、size() |
| 遍历 | iterator()、listIterator()、forEach() |
| 其他 | clone()、toArray()、sort()、subList()、ensureCapacity() |
先初始化一个测试用的 ArrayList,后续所有方法都基于这个列表演示:
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class ArrayListMethodsDemo {
public static void main(String[] args) {
// 初始化 ArrayList
ArrayList<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
System.out.println("初始列表:" + list); // [Java, Python, C++]
1. 新增方法(增)
| 方法 | 作用 | 时间复杂度 | 注意事项 |
|---|---|---|---|
add(E e) |
尾部添加元素 | O (1)(无扩容) | 扩容时为 O (n)(数组复制) |
add(int index, E e) |
指定索引插入元素 | O(n) | 索引需在 0~size 之间,会触发后续元素移位 |
addAll(Collection<? extends E> c) |
尾部批量添加集合元素 | O (m)(m 为新增元素数) | 扩容时额外加 O (n) 开销 |
addAll(int index, Collection<? extends E> c) |
指定索引批量添加 | O(n+m) | 索引合法,且会触发元素移位 |
java
// 1.1 尾部添加
list.add("Go");
System.out.println("add(Go) 后:" + list); // [Java, Python, C++, Go]
// 1.2 指定索引插入
list.add(1, "JavaScript");
System.out.println("add(1, JavaScript) 后:" + list); // [Java, JavaScript, Python, C++, Go]
// 1.3 批量添加
List<String> newList = Arrays.asList("PHP", "Ruby");
list.addAll(newList);
System.out.println("addAll(newList) 后:" + list); // [Java, JavaScript, Python, C++, Go, PHP, Ruby]
// 1.4 指定索引批量添加
list.addAll(0, Arrays.asList("C#", "Swift"));
System.out.println("addAll(0, ...) 后:" + list); // [C#, Swift, Java, JavaScript, Python, C++, Go, PHP, Ruby]
2. 删除方法(删)
| 方法 | 作用 | 时间复杂度 | 注意事项 |
|---|---|---|---|
remove(int index) |
删除指定索引元素 | O(n) | 索引需在 0~size-1 之间,后续元素移位 |
remove(Object o) |
删除第一个匹配的元素 | O(n) | 元素为 null 时也能匹配(删除第一个 null) |
removeAll(Collection<?> c) |
删除所有与集合 c 交集的元素 | O(n*m) | 批量删除,需遍历比对 |
removeIf(Predicate<? super E> filter) |
按条件删除元素 | O(n) | JDK 8+ 新增,遍历一次即可,效率高 |
clear() |
清空所有元素 | O(1) | 仅重置 size 为 0,数组本身不销毁 |
java
// 2.1 删除指定索引
list.remove(0); // 删除索引0的C#
System.out.println("remove(0) 后:" + list); // [Swift, Java, JavaScript, Python, C++, Go, PHP, Ruby]
// 2.2 删除指定元素
list.remove("Python"); // 删除第一个Python
System.out.println("remove(Python) 后:" + list); // [Swift, Java, JavaScript, C++, Go, PHP, Ruby]
// 2.3 批量删除
List<String> delList = Arrays.asList("PHP", "Ruby");
list.removeAll(delList);
System.out.println("removeAll(delList) 后:" + list); // [Swift, Java, JavaScript, C++, Go]
// 2.4 按条件删除(删除包含"Java"的元素)
list.removeIf(s -> s.contains("Java"));
System.out.println("removeIf(包含Java) 后:" + list); // [Swift, C++, Go]
// 2.5 清空列表
// list.clear();
// System.out.println("clear() 后:" + list); // []
3. 修改方法(改)
| 方法 | 作用 | 时间复杂度 | 注意事项 |
|---|---|---|---|
set(int index, E e) |
修改指定索引的元素值 | O(1) | 索引需在 0~size-1 之间,直接替换值 |
java
// 3.1 修改指定索引元素
list.set(1, "C"); // 把索引1的C++改为C
System.out.println("set(1, C) 后:" + list); // [Swift, C, Go]
4. 查询方法(查)
| 方法 | 作用 | 时间复杂度 | 注意事项 |
|---|---|---|---|
get(int index) |
获取指定索引元素 | O(1) | 索引需合法,ArrayList 核心优势方法 |
contains(Object o) |
判断是否包含指定元素 | O(n) | 底层调用 indexOf (o) != -1 |
indexOf(Object o) |
返回第一个匹配元素的索引,无则返回 - 1 | O(n) | 支持 null 元素(返回第一个 null 的索引) |
lastIndexOf(Object o) |
返回最后一个匹配元素的索引,无则返回 - 1 | O(n) | 从尾部开始遍历 |
isEmpty() |
判断列表是否为空 | O(1) | 仅判断 size == 0 |
size() |
获取元素个数 | O(1) | 直接返回 size 变量 |
java
// 4.1 获取指定索引元素
String elem = list.get(2);
System.out.println("get(2):" + elem); // Go
// 4.2 判断是否包含元素
boolean hasC = list.contains("C");
System.out.println("是否包含C:" + hasC); // true
// 4.3 查找元素首次出现的索引
int index = list.indexOf("Swift");
System.out.println("indexOf(Swift):" + index); // 0
// 4.4 查找元素最后出现的索引
list.add("C");
int lastIndex = list.lastIndexOf("C");
System.out.println("lastIndexOf(C):" + lastIndex); // 3
// 4.5 判断是否为空
boolean empty = list.isEmpty();
System.out.println("是否为空:" + empty); // false
// 4.6 获取元素个数
int size = list.size();
System.out.println("列表大小:" + size); // 4
5. 遍历方法(遍历)
| 方法 | 作用 | 效率 | 注意事项 |
|---|---|---|---|
| 普通 for 循环(get (index)) | 按索引遍历 | 最高 | ArrayList 专属高效遍历方式 |
| 增强 for 循环 | 迭代遍历 | 中等 | 底层是迭代器,简洁但略慢于普通 for |
iterator() |
获取迭代器 | 中等 | 支持遍历中安全删除(iterator.remove ()) |
listIterator() |
获取列表迭代器 | 中等 | 支持正向 / 反向遍历、添加 / 修改元素 |
forEach() |
函数式遍历(JDK 8+) | 中等 | 简洁,支持 Lambda 表达式 |
java
// 5.1 普通for循环(推荐ArrayList使用)
System.out.println("普通for循环遍历:");
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " "); // Swift C Go C
}
System.out.println();
// 5.2 增强for循环
System.out.println("增强for循环遍历:");
for (String s : list) {
System.out.print(s + " "); // Swift C Go C
}
System.out.println();
// 5.3 迭代器遍历(支持安全删除)
System.out.println("迭代器遍历并删除:");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("C")) {
iterator.remove(); // 安全删除,不会抛ConcurrentModificationException
}
}
System.out.println("迭代器删除后:" + list); // [Swift, Go]
// 5.4 forEach函数式遍历
System.out.println("forEach遍历:");
list.forEach(s -> System.out.print(s + " ")); // Swift Go
System.out.println();
6. 其他常用方法
| 方法 | 作用 | 关键说明 |
|---|---|---|
clone() |
浅拷贝,返回新 ArrayList | 需强制类型转换,引用类型元素仅拷贝地址 |
toArray() |
转为 Object 数组 | 无泛型,需手动强转 |
toArray(T[] a) |
转为指定类型数组 | 推荐使用,避免强转 |
sort(Comparator<? super E> c) |
自定义排序 | JDK 8+ 支持 Lambda 表达式 |
subList(int fromIndex, int toIndex) |
截取子列表 | 子列表是原列表的视图,修改会同步影响原列表 |
ensureCapacity(int minCapacity) |
手动指定最小容量 | 提前扩容,避免添加元素时频繁扩容 |
java
// 6.1 克隆(浅拷贝)
ArrayList<String> cloneList = (ArrayList<String>) list.clone();
System.out.println("克隆列表:" + cloneList); // [Swift, Go]
System.out.println("原列表与克隆列表是否同一对象:" + (list == cloneList)); // false
// 6.2 转为数组
Object[] objArray = list.toArray();
String[] strArray = list.toArray(new String[0]);
System.out.println("指定类型数组:" + Arrays.toString(strArray)); // [Swift, Go]
// 6.3 排序
list.add("Apple");
list.sort((s1, s2) -> s1.compareTo(s2)); // 按字符串自然排序
System.out.println("排序后:" + list); // [Apple, Go, Swift]
// 6.4 截取子列表(索引0到2,左闭右开)
List<String> subList = list.subList(0, 2);
System.out.println("子列表:" + subList); // [Apple, Go]
subList.add("Banana"); // 修改子列表会影响原列表
System.out.println("原列表(子列表修改后):" + list); // [Apple, Go, Banana, Swift]
// 6.5 手动扩容
list.ensureCapacity(10); // 提前扩容到容量10,避免后续添加时扩容
}
}
Java 中 "增强 for 循环(foreach 语法)" 和 List.forEach()方法 的区别
- 是
List接口(Java 8+)提供的方法 ,底层依赖Iterable接口的forEach(),本质是调用函数式接口(比如Consumer)来处理元素。
2. 使用场景与限制
- 增强 for 循环 :
- 可以用
break/continue控制循环(因为是语句); - 遍历过程中不能修改集合结构 (比如
add/remove,否则会抛ConcurrentModificationException)。
- 可以用
list.forEach():- 是方法调用,不能用
break/continue(除非用return跳出当前元素的处理,但无法终止整个循环); - 遍历逻辑由集合自身实现(比如 ArrayList 是普通遍历,CopyOnWriteArrayList 是安全遍历),部分集合(如并发集合)支持遍历中修改结构。
- 是方法调用,不能用
3. 语法风格
- 增强 for 循环是命令式风格:自己写循环体逻辑;
forEach()是函数式风格 :通常配合 Lambda 表达式(比如list.forEach(s -> System.out.println(s)))。
| 对比维度 | 增强 for 循环(for (T t : list)) |
List.forEach() 方法 |
|---|---|---|
| 本质 | Java 语法糖,编译后转为 Iterator 遍历,属于循环语句 | Java 8+ 新增的集合方法,依赖函数式接口(Consumer),属于方法调用 |
| 语法风格 | 命令式编程(手动写循环体逻辑) | 函数式编程(配合 Lambda / 方法引用,更简洁) |
| 循环控制 | 支持 break(终止循环)、continue(跳过当前) |
不支持 break/continue;仅能用 return 跳过当前元素,无法终止整个循环 |
| 集合修改限制 | 遍历中修改集合结构(add/remove)会抛 ConcurrentModificationException |
取决于集合实现:- 普通集合(ArrayList):仍会抛异常- 并发集合(CopyOnWriteArrayList):支持安全修改 |
| 异常处理 | 循环体内的异常可直接用 try-catch 包裹整个循环 | Lambda 内的异常需单独捕获(或声明未检查异常) |
linkedList
LinkedList 核心定义与底层原理
LinkedList 是 Java 集合框架中 java.util 包下的双向链表实现,实现了 List、Deque 等接口,弥补了 ArrayList 中间增删效率低的缺陷,核心特征如下:
- 底层数据结构本质是双向链表 (JDK 1.6 及之前为循环链表,之后改为双向链表),底层没有数组,而是由一个个
Node节点串联而成:- 每个
Node节点包含 3 部分:元素值(item)、前驱节点引用(prev)、后继节点引用(next); - 链表维护
first(头节点)和last(尾节点)两个引用,头节点prev为 null,尾节点next为 null; - 元素在内存中非连续存储,通过节点间的引用关联。
- 每个
- 无扩容机制(核心区别于 ArrayList)LinkedList 没有固定容量的概念,也无需扩容:
- 新增元素时,只需创建新的
Node节点,修改相邻节点的引用即可,无需复制数据; - 不存在 "扩容冗余内存",元素个数与节点个数完全一致。
- 新增元素时,只需创建新的
LinkedList 关键特性
- 访问效率(随机访问低)基于双向链表实现,不支持快速随机访问(通过索引
get(int index)):- 访问指定索引元素时,会先判断索引靠近头 / 尾(优化遍历),再从近的一端开始遍历,时间复杂度
O(n); - 这是 LinkedList 对比 ArrayList 最核心的劣势(ArrayList 随机访问
O(1))。
- 访问指定索引元素时,会先判断索引靠近头 / 尾(优化遍历),再从近的一端开始遍历,时间复杂度
- 增删效率(分场景)
- 首尾增删(
addFirst()/addLast()/removeFirst()/removeLast()):仅修改头 / 尾节点引用,时间复杂度O(1),效率极高; - 中间增删(
add(int index, E e)/remove(int index)):需先遍历到指定索引位置(O(n)),但找到位置后仅修改节点引用(O(1)),整体时间复杂度O(n)(对比 ArrayList 中间增删的 "遍历 + 移位",实际效率更高); - 原因:链表元素非连续存储,增删无需移动其他元素,仅修改节点引用关系。
- 首尾增删(
- 线程不安全LinkedList 是非线程安全的集合,多线程环境下同时读写会导致数据错乱(如
ConcurrentModificationException);解决方案:- 使用
Collections.synchronizedList(new LinkedList<>())包装成线程安全版本; - 并发场景优先用
ConcurrentLinkedDeque(并发包下,无锁设计,适合高并发)。
- 使用
- 空值与重复元素
- 允许存储
null值(可以通过add(null)添加); - 允许存储重复元素(如
add("a"); add("a")合法)。
- 允许存储
- Cloneable 与浅拷贝LinkedList 实现了
Cloneable接口,调用clone()方法会返回新的 LinkedList 实例,但为浅拷贝 :- 新 LinkedList 的节点是全新对象,但节点中的元素(引用类型)仅复制引用地址;
- 示例:如果 LinkedList 中存自定义类对象,修改原对象会影响克隆后的 LinkedList 元素。
LinkedList 常用操作示例
下面通过代码演示 LinkedList 的核心操作,覆盖基础操作、队列 / 栈特性、克隆等关键场景:
java
import java.util.Iterator;
import java.util.LinkedList;
public class LinkedListDemo {
public static void main(String[] args) {
// 1. 创建 LinkedList(无需指定容量,无扩容概念)
LinkedList<String> list = new LinkedList<>();
// 2. 添加元素(核心:首尾/中间添加)
list.add("Java"); // 尾部添加(等价于 addLast())
list.addFirst("Python"); // 头部添加(O(1))
list.addLast("C++"); // 尾部添加(O(1))
list.add(2, "Go"); // 索引2处插入(先遍历找位置,再修改引用)
System.out.println("添加元素后:" + list); // [Python, Java, Go, C++]
// 3. 访问元素
String first = list.getFirst(); // 获取头节点(O(1))
String last = list.getLast(); // 获取尾节点(O(1))
String index2 = list.get(2); // 获取索引2元素(遍历,O(n))
System.out.println("头:" + first + ",尾:" + last + ",索引2:" + index2);
// 4. 修改元素
list.set(1, "JavaScript"); // 修改索引1元素(先遍历找位置,再替换值)
System.out.println("修改后:" + list); // [Python, JavaScript, Go, C++]
// 5. 删除元素
list.removeFirst(); // 删除头节点(O(1))
list.removeLast(); // 删除尾节点(O(1))
list.remove(0); // 删除索引0元素(遍历找位置,再修改引用)
System.out.println("删除后:" + list); // [Go]
// 6. 遍历元素(三种方式,优先迭代器/增强for)
// 方式1:普通for循环(效率低,每次get(index)都要遍历)
System.out.println("普通for循环遍历:");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 方式2:增强for循环(推荐,底层是迭代器,无需重复遍历)
System.out.println("增强for循环遍历:");
for (String s : list) {
System.out.println(s);
}
// 方式3:迭代器(支持遍历中删除,安全)
System.out.println("迭代器遍历并删除:");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("Go")) {
iterator.remove(); // 迭代器删除,避免并发修改异常
}
}
System.out.println("迭代器删除后:" + list); // []
// 7. 队列/栈/双端队列特性(实现Deque接口,核心优势)
LinkedList<String> deque = new LinkedList<>();
// 队列操作(FIFO)
deque.offer("A"); // 尾部添加
deque.poll(); // 头部删除(队列为空返回null)
// 栈操作(LIFO)
deque.push("B"); // 头部添加(等价于 addFirst())
deque.pop(); // 头部删除(等价于 removeFirst(),栈为空抛异常)
// 双端队列操作
deque.offerFirst("C"); // 头部添加
deque.offerLast("D"); // 尾部添加
System.out.println("双端队列操作后:" + deque); // [C, D]
// 8. 克隆(浅拷贝)
LinkedList<String> cloneList = (LinkedList<String>) deque.clone();
System.out.println("原队列地址:" + deque);
System.out.println("克隆队列地址:" + cloneList);
System.out.println("是否同一对象:" + (deque == cloneList)); // false
}
}
LinkedList 实现的接口
核心接口 1:
List<E>作用:继承自 Collection 和 Iterable,是 LinkedList 作为 "列表" 的基础。带来的能力:定义了有序集合的核心方法(add()/get(int index)/remove(int index)/size() 等),保证 LinkedList 具备 "有序、可重复、可通过索引访问" 的基本特性。
核心接口 2:
Deque<E>(双向队列)作用:继承自 Queue,是 LinkedList 区别于 ArrayList 的核心接口。带来的能力:赋予 LinkedList 队列(FIFO)、栈(LIFO)、双端队列的特性,提供 addFirst()/addLast()/offer()/push()/pop()/poll() 等方法。
标记接口:
Cloneable作用:标记该类支持调用 clone() 方法进行浅拷贝。带来的能力:可以通过 linkedList.clone() 创建新的 LinkedList 实例(元素为引用类型时仅拷贝引用)。
标记接口:
java.io.Serializable作用:标记该类的对象可以被序列化 / 反序列化。带来的能力:LinkedList 对象可网络传输、写入文件(如序列化到 Redis)。
补充:LinkedList 没有实现的关键接口
- 没有实现
RandomAccess接口:标记该集合不支持快速随机访问,遍历优先用迭代器而非普通 for 循环; - 没有实现
Set接口:允许元素重复,区别于 HashSet 等不重复集合。
LinkedList 性能优化建议
- 优先用首尾操作:充分利用
addFirst()/addLast()/removeFirst()/removeLast()等 O (1) 方法,避免中间增删; - 遍历选迭代器 / 增强 for:避免普通 for 循环(每次
get(index)都会重新遍历链表); - 批量添加用
addAll():减少节点创建和引用修改的次数,提升效率; - 避免频繁查询:如果需要频繁随机访问,优先替换为 ArrayList;
- 并发场景用
ConcurrentLinkedDeque:替代Collections.synchronizedList,高并发下性能更好。
LinkedList vs ArrayList(核心区别)
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层实现 | 动态数组(基于数组) | 双向链表(每个节点存数据 + 前后指针) |
| 随机访问(get/set) | 效率高(O (1)),实现 RandomAccess 接口 |
效率低(O (n)),未实现 RandomAccess |
| 增删操作(非头尾) | 效率低(O (n)),需移动后续元素 | 效率高(O (1)),仅需修改指针 |
| 增删操作(头尾) | 头部增删 O (n)(移动元素),尾部增删 O (1) | 头尾增删 O (1)(直接操作指针) |
| 内存占用 | 连续内存,有扩容冗余(默认扩容 1.5 倍) | 非连续内存,每个节点额外存前后指针(内存开销更大) |
| 实现的接口 | List、RandomAccess、Cloneable、Serializable | List、Deque、Cloneable、Serializable |
| 功能支持 | 仅 List 基础功能(索引访问、增删) | 兼具 List + Deque 功能(队列 / 栈 / 双端操作) |
| 遍历效率 | 普通 for 循环(索引)效率更高 | 迭代器(Iterator)效率更高 |
| 空元素 / 重复元素 | 支持 | 支持 |
| 初始容量 | 有默认初始容量(10),可指定 | 无初始容量概念(链表按需创建节点) |
关键区别展开解释
1. 底层实现(最核心区别)
- ArrayList :基于动态数组实现,数组是连续的内存空间,JVM 会为它分配一块连续的内存,访问时通过索引直接定位,速度极快。但数组长度固定,满了之后会自动扩容(默认新容量 = 原容量 * 1.5),扩容时需要复制数组,有额外开销。
- LinkedList :基于双向链表 实现,每个节点(Node)包含
prev(前驱指针)、next(后继指针)、item(数据)。链表节点分散在内存中,无需连续空间,也没有扩容概念,新增节点时只需创建新 Node 并修改指针即可。
2. 性能差异(开发中最常用的判断依据)
性能差异的核心是「数组的连续内存」vs「链表的离散节点」:
- 随机访问(get (index)) :ArrayList 直接通过
数组首地址 + 索引 * 元素大小定位元素,时间复杂度 O (1);LinkedList 必须从头 / 尾节点开始遍历,直到找到目标索引,时间复杂度 O (n)。 - 增删操作 :
- 非头尾位置:ArrayList 增删后需要移动后续所有元素(比如在索引 2 插入元素,索引 2 及之后的元素都要后移),O (n);LinkedList 只需找到目标节点,修改前后指针,O (1)(找节点的过程是 O (n),但移动 / 修改指针是 O (1))。
- 头尾位置:ArrayList 尾部增删 O (1)(无扩容时),头部增删 O (n)(需移动所有元素);LinkedList 头尾增删都是 O (1)(直接操作头尾指针)。
3. 功能与接口差异
- ArrayList 仅实现
List接口,只有 List 的基础功能(索引访问、增删改查); - LinkedList 同时实现
List和Deque接口,因此兼具:- List 功能:按索引访问、增删;
- Deque 功能:队列(offer/poll)、栈(push/pop)、双端操作(addFirst/addLast)。
4. 内存占用差异
- ArrayList:内存连续,扩容时会预留冗余空间(比如当前容量 10,扩容后 15,多出来的 5 个位置暂时空置),但每个元素仅存数据本身;
- LinkedList:每个节点除了存数据,还要存前后指针(额外的内存开销),且节点分散在内存中,可能产生内存碎片。