前端工程师写惯了 JavaScript 的数组,一开始接触 Java 的集合时很容易迷茫:
- "为什么 List 要分 ArrayList、LinkedList?"
- "为什么不能像 JS 一样直接 arr[0] 用?"
- "为什么 List 删除要区分 remove(索引) 和 remove(对象)?"
- "为什么要写 List 而不是 List?"
本篇将从 JavaScript 的视角,完整讲清楚 Java 的 List 体系,并帮助你在后端开发中真正用明白。
🟦 1. JavaScript 数组 vs Java List:本质差异
✔ JavaScript 数组的本质:动态对象 & 哈希结构
JavaScript 数组不是严格意义的"数组",底层类似:
JavaScript
{
"0": 10,
"1": "abc",
"2": true,
length: 3
}
特点:
- 类型随便放(动态类型)
- 可随便扩容
- 可缺省元素([1, , 3])
- 很多场景是 JS 引擎优化过的对象
对前端来说,这非常自由。
✔ Java 的 List 本质:严格类型、固定结构、接口规范
Java 的 List 是一个接口:
Java
public interface List<E> extends Collection<E> {
...
}
所有 List 只能存放同一类型 E(泛型)。
List 有多种实现方式:
| 实现类 | 底层结构 | 类比 JS |
|---|---|---|
| ArrayList | 动态数组 | 标准数组 |
| LinkedList | 双向链表 | 无直接等价(手写链表) |
| CopyOnWriteArrayList | 线程安全 | JS 无对应 |
🟩 2. ArrayList:前端最先上手的 List
⭐ 底层原理(重点)
ArrayList 底层是 动态数组:
Java
Object[] elementData;
默认容量 10,满了之后 1.5 倍扩容。
扩容做的事类似:
Java
新数组 = new Object[旧容量 * 1.5]
把旧数组 copy 过去
扩容成本较高,所以:
后端开发中频繁往 ArrayList 加东西时,建议预估一下初始容量。
示例:
Java
List<Integer> list = new ArrayList<>(1000);
⭐ 常用 API 全对照 JS
| 操作 | JavaScript | Java ArrayList |
|---|---|---|
| 创建 | const arr = [] |
List<T> list = new ArrayList<>(); |
| 添加尾部 | arr.push(x) |
list.add(x) |
| 插入 | arr.splice(i,0,x) |
list.add(i, x) |
| 获取 | arr[i] |
list.get(i) |
| 设置 | arr[i] = x |
list.set(i, x) |
| 删除(按索引) | arr.splice(i,1) |
list.remove(i) |
| 删除(按值) | arr = arr.filter(v => v != x) |
list.remove("A") |
| 长度 | arr.length |
list.size() |
| 查找索引 | arr.indexOf(x) |
list.indexOf(x) |
⭐ 性能分析(简单记住即可)
| 操作 | 复杂度 | 原因 |
|---|---|---|
| get(i) | O(1) | 数组寻址 |
| set(i) | O(1) | 数组寻址 |
| add(x) 尾部 | 摊销 O(1) | 偶尔扩容 |
| add(i,x) 中间插入 | O(n) | 需要移动元素 |
| remove(i) | O(n) | 需要移动元素 |
结论:
ArrayList = 读多写少的场景最佳选择
在后端,90% 业务都是 "读多写少"。
🟨 3. LinkedList:链表 List
如果你写过 LeetCode 链表题,你就懂 LinkedList。
⭐ 底层结构
双向链表:
Java
prev ← [node] → next
插入/删除只需要:
- 找到节点
- 改指针
但无法随机访问。
⭐ 性能对比
| 操作 | ArrayList | LinkedList | 为什么 |
|---|---|---|---|
| get(i) | O(1) | O(n) | 链表需要遍历 |
| add(i,x) | O(n) | O(n) | ArrayList 移动元素,LinkedList 找节点 |
| addFirst | O(n) | O(1) | LinkedList 不移动元素 |
| removeFirst | O(n) | O(1) | 同上 |
因此:
❗ LinkedList 的中间插入删除不是真正的 O(1),因为必须先找到节点!
很多前端误解:"链表插入 O(1) 啊",那是指"找到节点之后"。
🟦 4. List 遍历方式(对比 JS)
✔ JavaScript(简单)
JavaScript
arr.forEach(item => console.log(item));
✔ Java 有 4 种常见遍历方式
① 普通 for(可随机访问)
Java
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
② 增强 for
Java
for (String s : list) {
System.out.println(s);
}
③ Iterator(可安全删除元素)
Java
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("A")) {
it.remove(); // 安全
}
}
对照 JS:
JavaScript
arr = arr.filter(x => x !== "A");
④ Stream(最像 JS 函数式)
JavaScript
list.stream()
.filter(x -> x.startsWith("A"))
.forEach(System.out::println);
对应 JS:
JavaScript
arr.filter(x => x.startsWith("A")).forEach(console.log);
🟥 5. List 的 7 个常见坑(前端最容易踩)
❗坑 1:不能 list[0]
JS:
JavaScript
arr[0]
Java:
Java
list.get(0)
为什么不给支持?
因为 List 是接口,不保证一定是数组结构,比如 LinkedList。
❗坑 2:删除整数值可能错用 remove(int)
Java
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.remove(1);
你以为删除 "1",实际删除索引 1(值 2)...
正确写法:
Java
list.remove(Integer.valueOf(1));
❗坑 3:List.of() 是不可变的(Java 9+)
Java
List<String> list = List.of("A", "B");
list.add("C"); // ❌ 运行时报错 UnsupportedOperationException
JS 的 Object.freeze() 类似。
❗坑 4:Arrays.asList() 不是完全可变
Java
List<String> list = Arrays.asList("A", "B");
list.add("C"); // ❌ 不支持添加/删除
因为它的底层是固定数组。
❗坑 5:不要在增强 for 中删除元素
Java
for(String s : list) {
list.remove(s); // ❌ ConcurrentModificationException
}
要用 Iterator:
Java
Iterator<String> it = list.iterator();
it.remove();
❗坑 6:代码中频繁 new ArrayList() 会影响性能
后端开发中常见:
Java
List<User> users = new ArrayList<>();
for (...) {
users = new ArrayList<>(); // ❌ 无意义开销
}
要注意避免反复建立对象。
❗坑 7:在高并发下 ArrayList 不是线程安全的
如果在多线程中使用:
Java
List<String> unsafe = new ArrayList<>();
可能引发异常、数据错乱。
正确方式:
Java
List<String> safe = Collections.synchronizedList(new ArrayList<>());
或使用:
Java
CopyOnWriteArrayList<String> safeList = new CopyOnWriteArrayList<>();
🟧 6. 后端实战场景示例
下面举几个开发中常见的 List 使用示例。
场景 1:查询数据库返回 List
Java
List<User> users = userDao.queryAllUsers();
JS 类比:
JavaScript
const users = await api.getUsers();
场景 2:遍历用户并构造 DTO
Java:
Java
List<UserDTO> dtos = new ArrayList<>();
for (User user : users) {
dtos.add(new UserDTO(user.getId(), user.getName()));
}
JS:
JavaScript
const dtos = users.map(u => ({ id: u.id, name: u.name }));
场景 3:去重(Java 要借助 Set)
Java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "A"));
List<String> result = new ArrayList<>(new HashSet<>(list));
JS:
JavaScript
const result = [...new Set(["A", "B", "A"])];
场景 4:按条件过滤
Java:
Java
List<User> adults = users.stream()
.filter(u -> u.getAge() >= 18)
.toList();
JS:
JavaScript
const adults = users.filter(u => u.age >= 18);
🟫 7. ArrayList vs LinkedList:详细对比总结
| 类型 | 底层 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|---|
| ArrayList | 动态数组 | 随机访问快,内存连续 | 插入/删除慢,扩容成本高 | 大部分业务使用 |
| LinkedList | 双向链表 | 插入删除快(找到节点后) | 随机访问慢,内存分散 | 队列、头部操作频繁 |
后端实际经验:
- 95% 用 ArrayList
- LinkedList 很少用,因为场景不常见
🎉 总结
对于从前端转后端的开发者: