前端转战后端:JavaScript 与 Java 对照学习指南(第四篇 —— List)

113 阅读5分钟

前端工程师写惯了 JavaScript 的数组,一开始接触 Java 的集合时很容易迷茫:

  • “为什么 List 要分 ArrayList、LinkedList?”
  • “为什么不能像 JS 一样直接 arr[0] 用?”
  • “为什么 List 删除要区分 remove(索引) 和 remove(对象)?”
  • “为什么要写 List 而不是 List?”

本篇将从 JavaScript 的视角,完整讲清楚 Java 的 List 体系,并帮助你在后端开发中真正用明白。


🟦 1. JavaScript 数组 vs Java List:本质差异

✔ JavaScript 数组的本质:动态对象 & 哈希结构

JavaScript 数组不是严格意义的“数组”,底层类似:

{
  "0": 10,
  "1": "abc",
  "2": true,
  length: 3
}

特点:

  • 类型随便放(动态类型)
  • 可随便扩容
  • 可缺省元素([1, , 3])
  • 很多场景是 JS 引擎优化过的对象

对前端来说,这非常自由。


✔ Java 的 List 本质:严格类型、固定结构、接口规范

Java 的 List 是一个接口:

public interface List<E> extends Collection<E> {
    ...
}

所有 List 只能存放同一类型 E(泛型)。

List 有多种实现方式:

实现类底层结构类比 JS
ArrayList动态数组标准数组
LinkedList双向链表无直接等价(手写链表)
CopyOnWriteArrayList线程安全JS 无对应

🟩 2. ArrayList:前端最先上手的 List

⭐ 底层原理(重点)

ArrayList 底层是 动态数组

Object[] elementData;

默认容量 10,满了之后 1.5 倍扩容

扩容做的事类似:

新数组 = new Object[旧容量 * 1.5]
把旧数组 copy 过去

扩容成本较高,所以:

后端开发中频繁往 ArrayList 加东西时,建议预估一下初始容量。

示例:

List<Integer> list = new ArrayList<>(1000);

⭐ 常用 API 全对照 JS

操作JavaScriptJava 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] = xlist.set(i, x)
删除(按索引)arr.splice(i,1)list.remove(i)
删除(按值)arr = arr.filter(v => v != x)list.remove("A")
长度arr.lengthlist.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。

⭐ 底层结构

双向链表:

prev ← [node] → next

插入/删除只需要:

  1. 找到节点
  2. 改指针

但无法随机访问。


⭐ 性能对比

操作ArrayListLinkedList为什么
get(i)O(1)O(n)链表需要遍历
add(i,x)O(n)O(n)ArrayList 移动元素,LinkedList 找节点
addFirstO(n)O(1)LinkedList 不移动元素
removeFirstO(n)O(1)同上

因此:

LinkedList 的中间插入删除不是真正的 O(1),因为必须先找到节点!

很多前端误解:“链表插入 O(1) 啊”,那是指“找到节点之后”。


🟦 4. List 遍历方式(对比 JS)

✔ JavaScript(简单)

arr.forEach(item => console.log(item));

✔ Java 有 4 种常见遍历方式

① 普通 for(可随机访问)

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

② 增强 for

for (String s : list) {
    System.out.println(s);
}

③ Iterator(可安全删除元素)

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("A")) {
        it.remove(); // 安全
    }
}

对照 JS:

arr = arr.filter(x => x !== "A");

④ Stream(最像 JS 函数式)

list.stream()
    .filter(x -> x.startsWith("A"))
    .forEach(System.out::println);

对应 JS:

arr.filter(x => x.startsWith("A")).forEach(console.log);

🟥 5. List 的 7 个常见坑(前端最容易踩)

❗坑 1:不能 list[0]

JS:

arr[0]

Java:

list.get(0)

为什么不给支持?
因为 List 是接口,不保证一定是数组结构,比如 LinkedList。


❗坑 2:删除整数值可能错用 remove(int)

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.remove(1);

你以为删除 “1”,实际删除索引 1(值 2)…

正确写法:

list.remove(Integer.valueOf(1));

❗坑 3:List.of() 是不可变的(Java 9+)

List<String> list = List.of("A", "B");
list.add("C"); // ❌ 运行时报错 UnsupportedOperationException

JS 的 Object.freeze() 类似。


❗坑 4:Arrays.asList() 不是完全可变

List<String> list = Arrays.asList("A", "B");
list.add("C"); // ❌ 不支持添加/删除

因为它的底层是固定数组。


❗坑 5:不要在增强 for 中删除元素

for(String s : list) {
    list.remove(s); // ❌ ConcurrentModificationException
}

要用 Iterator:

Iterator<String> it = list.iterator();
it.remove();

❗坑 6:代码中频繁 new ArrayList() 会影响性能

后端开发中常见:

List<User> users = new ArrayList<>();
for (...) {
    users = new ArrayList<>(); // ❌ 无意义开销
}

要注意避免反复建立对象。


❗坑 7:在高并发下 ArrayList 不是线程安全的

如果在多线程中使用:

List<String> unsafe = new ArrayList<>();

可能引发异常、数据错乱。

正确方式:

List<String> safe = Collections.synchronizedList(new ArrayList<>());

或使用:

CopyOnWriteArrayList<String> safeList = new CopyOnWriteArrayList<>();

🟧 6. 后端实战场景示例

下面举几个开发中常见的 List 使用示例。


场景 1:查询数据库返回 List

List<User> users = userDao.queryAllUsers();

JS 类比:

const users = await api.getUsers();

场景 2:遍历用户并构造 DTO

Java:

List<UserDTO> dtos = new ArrayList<>();
for (User user : users) {
    dtos.add(new UserDTO(user.getId(), user.getName()));
}

JS:

const dtos = users.map(u => ({ id: u.id, name: u.name }));

场景 3:去重(Java 要借助 Set)

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "A"));
List<String> result = new ArrayList<>(new HashSet<>(list));

JS:

const result = [...new Set(["A", "B", "A"])];

场景 4:按条件过滤

Java:

List<User> adults = users.stream()
    .filter(u -> u.getAge() >= 18)
    .toList();

JS:

const adults = users.filter(u => u.age >= 18);

🟫 7. ArrayList vs LinkedList:详细对比总结

类型底层优点缺点使用场景
ArrayList动态数组随机访问快,内存连续插入/删除慢,扩容成本高大部分业务使用
LinkedList双向链表插入删除快(找到节点后)随机访问慢,内存分散队列、头部操作频繁

后端实际经验:

  • 95% 用 ArrayList
  • LinkedList 很少用,因为场景不常见

🎉 总结

对于从前端转后端的开发者:

✔ 把 ArrayList 当 JS 数组用足够了

✔ LinkedList 除非你确定需要,否则不要用

✔ List 操作与 JS 高度类比,功能更严格

✔ 泛型保证类型安全

✔ 遍历方式更多(for / for-each / Iterator / Stream)

✔ 注意一些常见坑(remove、不可变 List、线程安全等)