CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 语言基础/课时/集合框架入门
RawBlame
课时目录14 节

Java 语言基础

  1. 01Java 与 JVM 是什么
  2. 02基本语法与 main
  3. 03基本类型与引用类型
  4. 04条件与循环
  5. 05方法与参数
  6. 06数组
  7. 07类与封装
  8. 08继承接口与多态
  9. 09集合框架入门
  10. 10字符串
  11. 11异常处理
  12. 12文件 IO 入门
  13. 13Maven 与项目结构
  14. 14综合练习与 dsa-java 分工

集合框架入门

List、Set、Map、泛型擦除直觉。

45 分钟

集合框架入门

这一课你将能做到

  1. 独立使用 List/Set/Map 三大容器完成增删改查和遍历,并按场景正确选型
  2. 解释泛型的作用与类型擦除的直觉,避免 Arrays.asList、ConcurrentModificationException 等经典坑
  3. 用代码演示 ArrayList 扩容机制和 HashMap 判重原理——两道集合区必考面试题

为什么要学

本站后端每一层都在和集合打交道:Mapper 从 MySQL 查回 List<User>,Service 用 Map<Long, User> 组装索引,Redis 客户端返回 Set<String>,接口传参常见 List<Long> productIds。可以说,不会集合 = 不会写后端。

数组是定长的原始容器,集合是「可伸缩 + API 丰富 + 类型安全(泛型)」的现代容器。上一课的 HashMap 你已经见过一次,这一课把它讲透。

先建立图像

集合框架三大家族:

Collection(单列)
├── List  有序、可重复      → ArrayList(本站最常用)
├── Set   不重复            → HashSet(去重)
└── Queue  队列             → 少用,算法阶段再见

Map(双列,键值对,不属于 Collection)
└── HashMap:key 唯一,value 可重复(本站做缓存/索引最常用)

泛型 <>:给容器贴「只装什么类型」的标签
List<String>  = 只装 String 的 List

选型口诀:要顺序可重复 → List;要去重 → Set;要按 key 找 value → Map。

跟着做

步骤 1:List 基本操作

新建 ListDemo.java:

import java.util.ArrayList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) {
        // 推荐写法:左边声明接口类型 List,右边给实现 ArrayList
        List<String> techs = new ArrayList<>();
        techs.add("java");          // 尾部追加
        techs.add("mysql");
        techs.add("redis");
        techs.add("mysql");         // List 允许重复
        System.out.println(techs);  // [java, mysql, redis, mysql](List 重写了 toString)

        System.out.println(techs.size());      // 4
        System.out.println(techs.get(0));      // java(按下标取,O(1))
        System.out.println(techs.indexOf("redis"));  // 2

        techs.set(1, "postgresql");            // 替换指定位置
        techs.remove("mysql");                 // 删除第一个匹配项
        System.out.println(techs.contains("redis"));  // true
        System.out.println(techs);             // [java, postgresql, redis, mysql]

        // 截取子列表:含头不含尾
        System.out.println(techs.subList(1, 3));  // [postgresql, redis]
    }
}

步骤 2:遍历与安全删除

import java.util.ArrayList;
import java.util.List;

public class ListLoopDemo {
    public static void main(String[] args) {
        List<String> logs = new ArrayList<>(List.of("INFO-1", "ERROR-2", "INFO-3", "ERROR-4"));

        // 方式 1:增强 for(只读场景首选)
        for (String log : logs) {
            System.out.print(log + " ");
        }
        System.out.println();

        // 方式 2:普通 for(需要下标时)
        for (int i = 0; i < logs.size(); i++) {
            if (logs.get(i).startsWith("INFO")) {
                System.out.println("跳过 " + logs.get(i));
            }
        }

        // 方式 3:forEach + lambda(JDK 8+)
        logs.forEach(l -> System.out.print(l + " "));
        System.out.println();

        // 经典坑:增强 for 遍历中直接 remove → ConcurrentModificationException
        // for (String log : logs) { if (log.startsWith("ERROR")) logs.remove(log); } // 别这么干!

        // 正确姿势 1:removeIf(最推荐)
        logs.removeIf(l -> l.startsWith("ERROR"));
        System.out.println(logs);   // [INFO-1, INFO-3]

        // 正确姿势 2:倒序普通 for 删除
        List<String> list2 = new ArrayList<>(List.of("A", "B", "C", "B"));
        for (int i = list2.size() - 1; i >= 0; i--) {
            if (list2.get(i).equals("B")) list2.remove(i);
        }
        System.out.println(list2);  // [A, C]
    }
}

步骤 3:Set 去重

import java.util.HashSet;
import java.util.Set;

public class SetDemo {
    public static void main(String[] args) {
        Set<String> visitedIps = new HashSet<>();
        System.out.println(visitedIps.add("10.0.0.1"));   // true:新增成功
        System.out.println(visitedIps.add("10.0.0.2"));   // true
        System.out.println(visitedIps.add("10.0.0.1"));   // false:已存在,添加失败(去重)

        System.out.println(visitedIps.size());            // 2
        System.out.println(visitedIps.contains("10.0.0.2"));  // true,O(1) 判存在

        // 经典应用:List 快速去重
        java.util.List<String> tags = java.util.List.of("java", "redis", "java", "mysql", "redis");
        Set<String> distinct = new HashSet<>(tags);
        System.out.println(distinct);   // [java, redis, mysql](顺序不保证!)

        // 需要保持插入顺序:LinkedHashSet
        Set<String> ordered = new java.util.LinkedHashSet<>(tags);
        System.out.println(ordered);    // [java, redis, mysql, mysql 消失后的有序版]
    }
}

步骤 4:Map 键值对

import java.util.HashMap;
import java.util.Map;

public class MapDemo {
    public static void main(String[] args) {
        Map<Long, String> userNames = new HashMap<>();   // key=用户id, value=用户名

        userNames.put(1001L, "tom");
        userNames.put(1002L, "jerry");
        userNames.put(1001L, "tomcat");      // 同 key 再 put = 覆盖旧值
        System.out.println(userNames);       // {1001=tomcat, 1002=jerry}

        System.out.println(userNames.get(1001L));          // tomcat
        System.out.println(userNames.get(9999L));          // null(key 不存在返回 null,不报错)
        System.out.println(userNames.getOrDefault(9999L, "游客"));  // 游客
        System.out.println(userNames.containsKey(1002L));  // true

        // 经典用法:分组/计数
        String[] words = {"mysql", "redis", "mysql", "kafka", "redis", "mysql"};
        Map<String, Integer> counter = new HashMap<>();
        for (String w : words) {
            counter.put(w, counter.getOrDefault(w, 0) + 1);
        }
        System.out.println(counter);   // {mysql=3, redis=2, kafka=1}

        // 三种遍历方式
        for (Map.Entry<Long, String> e : userNames.entrySet()) {
            System.out.println("key=" + e.getKey() + ", value=" + e.getValue());
        }
        userNames.keySet().forEach(k -> System.out.print(k + " "));       // 只遍历 key
        userNames.values().forEach(v -> System.out.print(v + " "));       // 只遍历 value
        System.out.println();

        userNames.remove(1002L);       // 删除
        System.out.println(userNames.size());   // 1
    }
}

步骤 5:泛型与类型安全

import java.util.ArrayList;
import java.util.List;

public class GenericDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("tom");
        // names.add(123);            // 编译错:泛型在编译期拦截类型错误
        String first = names.get(0);  // 取出来不用强转,自动就是 String

        // 不用泛型的老写法(JDK 5 之前):什么都装,取出来全是 Object
        List raw = new ArrayList();
        raw.add("tom");
        raw.add(123);                 // 居然能装进去
        Object o = raw.get(1);
        // String s = (String) o;     // 运行时才炸:ClassCastException
        System.out.println(first);
    }
}

泛型把「运行时类型炸弹」提前到「编译期报错」。<> 里只能写引用类型(List<Integer>,不是 List<int>——基本类型走包装类,呼应第 3 课)。

步骤 6:ArrayList 扩容实证

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class CapacityDemo {
    public static void main(String[] args) throws Exception {
        List<Integer> list = new ArrayList<>();     // 底层数组初始为空,首次 add 时容量 10
        list.add(1);
        System.out.println(capacity(list));         // 10

        for (int i = 1; i <= 10; i++) list.add(i);
        System.out.println(capacity(list));         // 10(刚好装满)

        list.add(11);
        System.out.println(capacity(list));         // 15 → 扩容为原来的 1.5 倍
    }

    // 反射偷看内部数组容量(学习用,生产别这么写)
    static int capacity(List<?> l) throws Exception {
        Field f = ArrayList.class.getDeclaredField("elementData");
        f.setAccessible(true);
        return ((Object[]) f.get(l)).length;
    }
}

扩容动作 = 新建 1.5 倍数组 + System.arraycopy 整体搬移,O(n)。预知规模时传初始容量:new ArrayList<>(10000) 免去多次搬移。

讲清楚要点

  1. 声明用接口、实现选实现类:List<String> l = new ArrayList<>()。以后换 LinkedList 或线程安全容器,调用方一行不改。
  2. ArrayList 查快改慢:按下标随机访问 O(1);头/中间插入删除 O(n)。频繁头部插入就换 ArrayDeque 或 LinkedList。
  3. HashSet 的去重依赖 hashCode + equals:自定义类当元素必须重写这两兄弟(或用 record 自动生成),否则「相同」的对象各进各的桶。
  4. HashMap 的 key 同理:且 key 语义上不可变(用 String、Long 这类不可变类型最安全);key 一般不允许为 null(HashMap 允许一个,其他实现如 Hashtable 不允许)。
  5. 遍历中增删必须走 removeIf / 迭代器 / 倒序下标:直接 remove 会触发并发修改检查(fail-fast)。
  6. 不可变集合:List.of、Map.of、Set.of(JDK 9+)创建后不能增删改,适合做常量表;往可变集合拷贝:new ArrayList<>(List.of(...))。

你可能会懵的地方

  • ArrayList 和 LinkedList 到底怎么选? 99% 选 ArrayList。LinkedList 的「插入快」只在「已持有中间节点引用」时成立;按下标访问它是 O(n) 遍历,实际反而更慢。它真正的舞台是当 Deque(双端队列)用。

  • 类型擦除是什么? 泛型信息只存在于编译期,编译后 List<String> 和 List<Integer> 都是同一个 List 字节码。推论:运行时拿不到 T 的 Class(new T()、T[] 都不行);list instanceof List<String> 不合法。

  • 为什么 int[] 不能传给 List<Integer>? 泛型不支持基本类型,且 int[] 本身是一个对象(装 int 数组的 List 而不是装 Integer 的 List)。转换要靠循环或流:Arrays.stream(arr).boxed().toList()。

  • HashMap 是有序的吗? 无序(按 hash 桶分布)。要插入顺序用 LinkedHashMap(维护双向链表);要按 key 排序用 TreeMap。本站做「最近访问」缓存常用 LinkedHashMap 开启 accessOrder 实现 LRU。

  • HashMap 的底层原理(面试必问)? 数组 + 链表 + 红黑树(JDK 8+):key 经 hash 定位数组下标;冲突挂链表;链表长度超 8 且数组容量 ≥ 64 转红黑树,查询 O(log n)。默认容量 16,负载因子 0.75,超阈值扩容翻倍并 rehash。

  • 遍历时想删除为什么报 ConcurrentModificationException? 集合内部有个 modCount 修改计数,迭代器发现计数对不上就快速失败(fail-fast),防止「边遍历边改」导致漏元素/错位。这是故意的保护机制。

错法对照

错法问题正解
List<int> ids泛型不接受基本类型List<Integer>
增强 for 里 list.remove(x)ConcurrentModificationExceptionlist.removeIf(...) 或倒序 for
Arrays.asList(...) 后调 add定长视图,抛 UnsupportedOperationExceptionnew ArrayList<>(Arrays.asList(...))
自定义类不重写 hashCode 就进 HashSet去重失效重写 equals + hashCode,或改 record
map.get(key) 直接 .method()key 不存在返回 null → NPEgetOrDefault 或先 containsKey
new HashMap<>() 后判断 map.get(k) == null 区分「没存」和「存了 null」二义性用 containsKey
遍历 Map 用 for (String k : map)Map 不能直接 for先 keySet() / entrySet() / values()

卡点急救

  1. UnsupportedOperationException:十有八九你在改 List.of / Arrays.asList 的结果。包一层 new ArrayList<>(...) 变可变。
  2. ConcurrentModificationException:见步骤 2,removeIf 一招解决。
  3. HashSet 装自定义对象去重失败:重写 equals/hashCode(IDEA: Alt+Insert),或类改 record。
  4. 从 Map 取值后 NPE:养成 getOrDefault / Optional.ofNullable(map.get(k)) 习惯;后端代码 map 取值判空是肌肉记忆。
  5. 泛型报「不兼容的类型」:检查尖括号类型与 add 内容是否一致;包装类与基本类型混用是最常见原因。
  6. 想要有序 Map/Set:HashMap/HashSet 无序;LinkedHash* 保插入序;Tree* 按比较器排序。

面试常问

  • ArrayList 与 LinkedList 区别? 底层:动态数组 vs 双向链表;随机访问 O(1) vs O(n);尾部追加均摊 O(1) vs O(1);中间插入理论 O(n) vs O(1)(但含查找仍是 O(n))。内存:ArrayList 连续紧凑、LinkedList 每节点多两个引用。结论:默认 ArrayList。

  • HashMap 的 put 流程?(高频) 计算 key 的 hash → 定位桶下标 → 桶空直接放 → 冲突则比较首节点(hash+equals 相同覆盖,否则挂链尾/入树)→ 超过阈值(12=16×0.75)扩容翻倍。

  • HashSet 如何保证不重复? add 时先算 hashCode 定桶,再 equals 比内容;两者共同决定「相同」。所以元素必须正确实现这两个方法。

  • HashMap 和 Hashtable 区别? 线程安全(否 vs 全表锁)、null key(1 个 vs 不允许)、性能、初始容量(16 vs 11)。多线程场景现代写法用 ConcurrentHashMap。

  • fail-fast 是什么? 迭代期间检测到结构性修改(modCount 不一致)立即抛 ConcurrentModificationException 的保护机制。单线程用 removeIf 规避,多线程用 ConcurrentHashMap / CopyOnWriteArrayList。

自测三问

  1. 三种集合(List/Set/Map)各自的特性和典型场景?本站后端各举一例。
  2. 为什么增强 for 里 remove 会炸,而 removeIf 不会?
  3. 把 List.of("a","b") 返回的 List 调用 add 会发生什么?怎么改?

小结一句话

List 管顺序、Set 管唯一、Map 管索引;声明用接口、泛型拦截类型错误、遍历增删用 removeIf;ArrayList 1.5 倍扩容、HashMap 数组+链表+红黑树——这四组知识撑起后端日常 90% 的数据操作。

延伸

  • Oracle:集合(Collections)教程
  • 下一课:字符串——String 为什么不可变,== 与 equals 的世纪之争,StringBuilder 何时上场。
加载本节练习…
← 上一课继承接口与多态下一课 →字符串