Skip to content

集合框架:装数据的正确姿势

难度:中级 | 预计时间:45 分钟 | 前置:面向对象(上)


📖 数组不够用了

故事背景

你用数组存用户列表——User[] users = new User[100]。新用户注册,数组满了,你得手动扩容。删一个用户,中间留个洞,遍历时还要判空。想找"所有 VIP 用户"——自己写循环。ArrayList、HashMap、HashSet 就是来救你的。Java 的集合框架(Collections Framework)是一套设计精良的数据结构库,学完你会发现 JS 的 Array/Object 是多么"粗糙"。

💻 代码演示

拿出你的 JS 经验——下面是对照表式演示:

java
import java.util.*;

// JS: const users = []           → Java: List<User> users = new ArrayList<>();
// JS: users.push(user)           → users.add(user);
// JS: users.find(u => u.vip)    → users.stream().filter(User::isVip).findFirst()
// JS: users.map(u => u.name)    → users.stream().map(User::getName).toList()

// ===== 常用集合演示 =====
public class CollectionDemo {
    public static void main(String[] args) {
        // ArrayList — 有序,可重复,按索引访问快
        List<String> names = new ArrayList<>();
        names.add("张三");
        names.add("李四");
        names.add("张三");  // 可以重复
        System.out.println("第1个: " + names.get(0));  // 张三

        // HashSet — 无序,不可重复,查"是否存在"快
        Set<String> uniqueNames = new HashSet<>(names);
        System.out.println("去重后: " + uniqueNames);  // [李四, 张三]

        // HashMap — 键值对,通过 key 快速查找 value
        Map<String, Integer> scores = new HashMap<>();
        scores.put("张三", 95);
        scores.put("李四", 87);
        System.out.println("张三的分数: " + scores.get("张三"));  // 95

        // Stream API — 声明式数据处理(类似 JS 的链式操作)
        List<Integer> highScores = scores.values().stream()
            .filter(s -> s >= 90)
            .sorted()
            .toList();
        System.out.println("90分以上: " + highScores);  // [95]
    }
}

运行输出:

第1个: 张三
去重后: [李四, 张三]
张三的分数: 95
90分以上: [95]

🎯 三个核心问题

这是什么?

Java 集合框架分为两大接口体系:Collection(List / Set / Queue)和 Map(键值对)。核心实现类:ArrayList(动态数组)、LinkedList(双向链表)、HashSet(哈希集)、TreeSet(排序集)、HashMap(哈希表)、TreeMap(排序树)。Java 8+ 提供了 Stream API 做函数式数据处理。

为什么需要它?

为什么有这么多实现?不同场景需要不同性能特点:ArrayList 随机访问 O(1) 但中间插入 O(n);LinkedList 中间插入 O(1) 但随机访问 O(n);HashSet 去重和查存在 O(1);TreeSet 天然有序但 O(log n)。选择合适的集合,可能让程序快 100 倍。

如果没有它会怎样?

如果只有数组——每次扩容手动复制,查找需要自己写循环,去重要 O(n²) 的双重循环。集合框架把常见需求标准化,你不需要重新发明轮子,而且经过几十年的优化和测试,性能和正确性都有保障。

📝 原理讲解

前端到 Java 集合速查表:

JS                       Java
─────────────────────────────────────────────
Array (动态数组)           ArrayList
Set                       HashSet
Map / Object              HashMap
Array.filter/map/reduce   Stream API
Array.find                Optional.orElse + Stream

选型决策树

  • 需要有序 + 可重复 → ArrayList

  • 需要去重 → HashSet

  • 需要去重 + 排序 → TreeSet

  • 需要键值对查找 → HashMap

  • 需要键值对 + 有序 → LinkedHashMapTreeMap

  • 需要线程安全 → ConcurrentHashMap(进阶)

注意:遍历 HashMap 时不要在循环里删除元素——用 Iterator 或 removeIf

🎨 生活类比

类比理解

ArrayList 是排队——有顺序、可以有重复的人、按编号找人快("第 5 个往前走")。 HashSet 是花名册——无序、每个人只签一次名、查"来了没"极快(实际上是通过哈希值定位,不看名字)。 HashMap 是词典——通过 key(单词)直接翻到 value(解释),不用从头扫到尾。一本词典可以几秒找到某个词,从头翻可能要十几分钟。 Stream API 是流水线——每个操作是一道工序(过滤、排序、转换),数据在流水线上走一遍,出来就是成品。

✏️ 动手练习

练习 1

写一个方法 countWords(String text),返回 Map<String, Integer> 统计每个单词出现次数。

<details> <summary>💡 查看提示</summary>

text.split("\\s+") 分词,遍历单词,map.put(word, map.getOrDefault(word, 0) + 1)

</details>

练习 2

用 Stream API:从一个 List<Integer> 中筛选出所有偶数、去重、排序、取前 5 个。

<details> <summary>💡 查看提示</summary>

链式调用:.stream().filter(n -> n % 2 == 0).distinct().sorted().limit(5).toList()

</details>

练习 3

实现一个简单任务优先级队列:Task 有 name 和 priority,用 PriorityQueue 让高优先级的先出队。

<details> <summary>💡 查看提示</summary>

PriorityQueue 默认是小顶堆。需要自定义比较器:new PriorityQueue<>((a, b) -> b.priority - a.priority) 实现大顶堆。

</details>

✅ 自检站

<details> <summary><strong>ArrayList 和 LinkedList 各适合什么场景?</strong></summary>

ArrayList 底层是数组,随机访问(get)O(1),末尾插入 O(1),中间插入/删除 O(n)(需要移动后续元素)。适合"读多写少、只在末尾添加"的场景。LinkedList 底层是双向链表,随机访问 O(n),头尾插入 O(1)。适合"频繁在头部插入/删除"的场景(如队列)。绝大多数场景 ArrayList 就够用了。

</details>

<details> <summary><strong>HashMap 的原理是什么?</strong></summary>

HashMap 基于哈希表。存入时对 key 调用 hashCode() 得到哈希值,映射到内部数组的索引位置。取时同样计算哈希值直接定位。理想情况下 O(1)。如果两个 key 哈希碰撞(映射到同一位置),该位置用链表/红黑树存储多个 entry。Java 8+ 当碰撞链长度超过 8 时转为红黑树,保证最坏 O(log n)。

</details>