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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 进阶/课时/综合练习
RawBlame
课时目录12 节

Java 进阶

  1. 01Java 进阶概览
  2. 02枚举与模式
  3. 03Set/Queue/Stack
  4. 04OOP 进阶
  5. 05方法链与绑定
  6. 06Record 与不可变
  7. 07模块系统
  8. 08并发进阶
  9. 09依赖注入概念
  10. 10Java Web 框架
  11. 11Quarkus 入门
  12. 12综合练习

综合练习

用集合与 record 重构一个小模块。

40 分钟

综合练习

这一课你将能做到

  1. 独立实现「record DTO + enum 状态 + Set 去重 + Queue 发布队列 + Builder」的完整小模块,java 一条命令跑通
  2. 用 JUnit 5 写出覆盖重复 slug、FIFO 出队、非法参数的单元测试,并说出每条测试在保护什么
  3. 完成本 track 自检清单,把领域模型对接 Spring Boot 的下一步路线落到纸面

为什么要学

前 11 课的知识点都是「单点」,企业代码考验的是「组合」:一个课时目录模块同时用到 enum(状态)、record(DTO)、Map/Set(唯一性与顺序)、Queue(异步发布模拟)、Builder(构造)、异常(脏数据拦截)。自己从头敲一遍、再用测试锁住行为,才算把这些单点焊成能力。

这也是进入 Spring Boot 实战 前的最佳验收:本课的 TrackCatalog 在 sb-ioc 课会变成被容器托管的 Bean,DTO 会直接变成 REST 响应体——同一个模型,两种宿主。

先建立图像

模块结构(对应知识点标注):

TrackCatalogApp
├─ LessonStatus        enum(第 2 课):DRAFT / PUBLISHED / ARCHIVED
├─ LessonView          record + 紧凑构造器(第 6 课):不可变 DTO
├─ LessonBuilder       方法链(第 5 课):链式创建 + build 校验
└─ TrackCatalog
   ├─ Map<String,LessonView>  slug 唯一索引(putIfAbsent,第 3/4 课)
   ├─ Queue<LessonView>       发布队列(ArrayDeque,FIFO)
   ├─ add()                   重复 slug 返回 false
   ├─ enqueuePublish()        校验存在性与状态
   └─ drainOne()              出队 → 状态流转 → 回写 Map

跟着做

步骤 1:实现完整模块(单文件可运行)

新建 TrackCatalogApp.java,完整可运行:

import java.util.ArrayDeque;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.HashSet;

public class TrackCatalogApp {

    enum LessonStatus { DRAFT, PUBLISHED, ARCHIVED }

    record LessonView(String slug, String title, int orderIndex, LessonStatus status) {
        LessonView {
            if (slug == null || slug.isBlank())
                throw new IllegalArgumentException("slug 不能为空");
            if (orderIndex < 0)
                throw new IllegalArgumentException("orderIndex 不能为负");
        }
        LessonView publish() {
            return new LessonView(slug, title, orderIndex, LessonStatus.PUBLISHED);
        }
    }

    static class LessonBuilder {
        private String slug;
        private String title = "";
        private int orderIndex;
        private LessonStatus status = LessonStatus.DRAFT;

        LessonBuilder slug(String v)   { this.slug = v; return this; }
        LessonBuilder title(String v)  { this.title = v; return this; }
        LessonBuilder order(int v)     { this.orderIndex = v; return this; }
        LessonBuilder status(LessonStatus v) { this.status = v; return this; }

        LessonView build() {
            return new LessonView(slug, title, orderIndex, status);
        }
    }

    static class TrackCatalog {
        private final Map<String, LessonView> lessons = new LinkedHashMap<>(); // 保留插入序
        private final Queue<LessonView> publishQueue = new ArrayDeque<>();

        /** slug 唯一:重复返回 false,不让脏数据进来 */
        boolean add(LessonView lesson) {
            return lessons.putIfAbsent(lesson.slug(), lesson) == null;
        }

        boolean enqueuePublish(String slug) {
            LessonView lesson = lessons.get(slug);
            if (lesson == null || lesson.status() != LessonStatus.DRAFT) return false;
            return publishQueue.offer(lesson);
        }

        /** 模拟异步发布:出队 → 状态流转 → 回写目录 */
        LessonView drainOne() {
            LessonView queued = publishQueue.poll();
            if (queued == null) return null;
            LessonView published = queued.publish();
            lessons.put(published.slug(), published);
            return published;
        }

        int size() { return lessons.size(); }
        Set<String> slugs() { return new HashSet<>(lessons.keySet()); }
    }

    public static void main(String[] args) {
        TrackCatalog catalog = new TrackCatalog();
        catalog.add(new LessonBuilder().slug("jva-intro").title("Java 进阶概览").order(1).build());
        catalog.add(new LessonBuilder().slug("jva-enums").title("枚举与模式").order(2).build());

        boolean dup = catalog.add(new LessonBuilder().slug("jva-intro").title("重复").order(9).build());
        System.out.println("重复 slug 添加结果:" + dup);            // false

        catalog.enqueuePublish("jva-intro");
        catalog.enqueuePublish("jva-enums");
        catalog.enqueuePublish("不存在");                            // 静默失败,返回 false

        LessonView published;
        while ((published = catalog.drainOne()) != null) {
            System.out.println("已发布:" + published.slug() + " -> " + published.status());
        }
        System.out.println("目录大小:" + catalog.size() + ",slug 集合:" + catalog.slugs());
    }
}

运行:java TrackCatalogApp.java。逐行核对输出:重复添加是 false、发布按 FIFO 顺序、状态从 DRAFT 流转到 PUBLISHED。

步骤 2:JUnit 5 锁住行为

Maven 项目(mvn -q archetype:generate 或直接建 pom.xml 引入 junit-jupiter 5.10+)中,把上面的类移到 src/main/java,测试放 src/test/java 同包:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class TrackCatalogTest {

    private TrackCatalogApp.TrackCatalog catalog;

    @BeforeEach
    void setUp() {
        catalog = new TrackCatalogApp.TrackCatalog();
        catalog.add(new TrackCatalogApp.LessonBuilder()
                .slug("a").title("A").order(1).build());
    }

    @Test
    void duplicateSlugShouldBeRejected() {
        assertFalse(catalog.add(new TrackCatalogApp.LessonBuilder()
                .slug("a").title("A2").order(2).build()));
        assertEquals(1, catalog.size()); // 目录未被污染
    }

    @Test
    void publishShouldDrainInFifoOrder() {
        catalog.add(new TrackCatalogApp.LessonBuilder()
                .slug("b").title("B").order(2).build());
        catalog.enqueuePublish("a");
        catalog.enqueuePublish("b");

        assertEquals("a", catalog.drainOne().slug()); // 先进先出
        assertEquals("b", catalog.drainOne().slug());
        assertNull(catalog.drainOne());               // 队列空了
    }

    @Test
    void publishedLessonCannotEnqueueAgain() {
        catalog.enqueuePublish("a");
        catalog.drainOne();                            // 已发布
        assertFalse(catalog.enqueuePublish("a"));      // 状态机拒绝二次发布
    }

    @Test
    void blankSlugShouldThrow() {
        assertThrows(IllegalArgumentException.class, () -> new TrackCatalogApp.LessonBuilder()
                .slug(" ").title("x").order(1).build());
    }
}

运行 mvn test,四条全绿。每条测试对应一条业务规则——这就是「测试即文档」。

步骤 3:对照「如果交给 Spring」做映射推演

纸笔完成,不需要代码:

TrackCatalog      → @Service Bean(单例,无请求级状态,线程安全吗?本例读写 Map 需 ConcurrentHashMap)
LessonView        → REST 响应体(record 直接返回)
enqueuePublish    → POST /lessons/{slug}/publish
drainOne 循环     → Spring @Async + 线程池(第 8 课)或 MQ 真异步
blankSlug 校验    → Bean Validation @NotBlank + 全局异常处理

讲清楚要点

  1. 组合优先于堆功能:这个 100 行模块用到了 6 课的知识点,但没有一行是为炫技而写——每个结构都在解决一个真实问题(唯一性、顺序、状态、不可变)。
  2. 校验放在边界:record 紧凑构造器拦非法值、putIfAbsent 拦重复 slug、enqueuePublish 拦错误状态——脏数据在最外层死掉,内部逻辑就能假设一切合法。
  3. 不可变让状态机可推理:LessonView.publish() 返回新实例而不是改 status 字段,任何时刻看到的状态都是某个确定版本,多线程下也无竞态。
  4. 测试保护的是规则:重复拒绝、FIFO、不可二次发布、空 slug 抛异常——每条规则一个测试;需求变了先改测试再改实现,行为不会悄悄漂移。
  5. 纯 Java 模块是框架的地基:本模块不依赖 Spring 也能完整工作;框架接管的是「怎么被发现、怎么被调用」,业务核心保持 POJO 才可测可移植。

你可能会懵的地方

  • 为什么用 LinkedHashMap 而不是 HashMap? 目录有「按添加顺序展示」的语义;HashMap 不保证顺序。若还要求按 orderIndex 排序输出,可在展示层 values().stream().sorted(comparing(LessonView::orderIndex))。
  • drainOne 返回 null 合理吗? 这是教学取轻。工程做法返回 Optional<LessonView> 强迫调用方处理空值;队列耗尽在真实系统里由 BlockingQueue.take() 阻塞等待替代轮询。
  • add 返回 boolean 还是抛异常? 「重复 slug」在本模型里是预期内的可恢复情况,返回 false 合理;若调用方重复是程序 bug,抛 IllegalArgumentException 更能暴露问题。判断标准:调用方能不能也该不该处理这种情况。
  • 发布队列和 MQ 什么关系? 内存 Queue 进程一挂就丢,且只在本进程内流转;MQ(如 RabbitMQ/Kafka)提供持久化、跨服务投递与重试。本课的 Queue 让你先掌握「队列语义」,MQ 是它的分布式形态。
  • 下一步该先补什么? 两条线按需选:直接进 spring-boot-essentials 把本模型挂上 HTTP 与 JPA;或进 dsa-java 用同一批集合知识刷算法。语言层缺口(如 IO、JVM)在实战中按需回补。

错法对照

错法问题正解
用 List 存课时 + contains 查重O(n) 且语义模糊Map/Set 做唯一索引
直接改 LessonView 的 status 字段record 没有 setter;可变状态难推理返回新实例的状态流转方法
enqueue 不校验存在性与状态脏数据进队列,发布时爆炸边界校验 + 状态机拒绝
只写实现不写测试重构无从下手规则 → 测试 → 实现
一次写完 200 行再跑错误定位成本高小步跑通:先 record,再 add,再队列

卡点急救

  1. 测试里访问不到 TrackCatalog:类是包级私有静态嵌套类,测试类必须与 TrackCatalogApp 同包同源码集;或把业务类提为独立文件加 public。
  2. mvn test 找不到 JUnit:pom.xml 确认引入 junit-jupiter(test scope)并配置 surefire 3.x;Spring Boot 项目用 spring-boot-starter-test 一步到位。
  3. FIFO 测试偶发失败:检查是否误用了 Stack 语义(push/pop)或 PriorityQueue;发布顺序语义要求严格 FIFO 时,ArrayDeque 的 offer/poll 才是正解。

面试常问

  • 让你设计一个「课程目录」模块,你怎么入手? 答题框架:先定领域模型(不可变 record DTO + enum 状态机)→ 定不变量(slug 唯一、状态只能 DRAFT→PUBLISHED)→ 选数据结构(Map 索引唯一性、LinkedHashMap 保序、Queue 模拟异步)→ 定边界(构造器/工厂校验)→ 定测试(每条不变量一条用例)。展示的是「先约束后实现」的工程思维。
  • 为什么业务核心类不依赖框架注解更好? 可单测(不启动容器)、可移植(换框架不重写)、职责清晰(框架管生命周期与装配,业务管规则);Spring 自己的官方建议也是「POJO + 少量注解」,注解只在组装层出现。
  • 内存队列和 MQ 的取舍? 内存队列:零依赖、快、但不可持久化且限单进程,适合进程内解耦;MQ:持久化、跨服务、可重试削峰,但引入运维成本与一致性复杂度。按「数据丢了能不能接受、上下游是否同进程」两问决策。

自测三问

  1. 本模块里哪三处分别拦截了「重复 slug、非法参数、非法状态」?
  2. putIfAbsent 与 put 的返回值差异是什么?为什么去重要用前者?
  3. 把 TrackCatalog 交给 Spring 后,哪两处线程安全隐患需要处理?

小结一句话

枚举做状态、record 做数据、Map/Set 保唯一、Queue 做异步、Builder 造对象、测试锁行为——这 100 行就是进阶 Java 的合格答卷。

延伸

  • dev.java 官方学习路径
  • Oracle JDK 21 文档
  • 下一步:进入 Spring Boot 实战,把本课的 TrackCatalog 与 LessonView 挂到真实 HTTP 接口、MySQL 与 Redis 缓存上,走完整的 Nginx → Boot → MySQL 链路;之后可选 Spring Cloud 微服务 或用 dsa-java 巩固算法。
加载本节练习…
← 上一课Quarkus 入门