实战:待办事项 CLI

学了这么多——集合、IO、并发、反射、Stream、日期时间——但知识是离散的珍珠,需要一根线把它们串成项链。这根线就是项目实战

这一阶段我们做一个待办事项 CLI(Command Line Interface)——像 git 一样在终端敲命令管理你的待办。它不大,但五脏俱全:命令解析、数据持久化、优先级排序、标签过滤、Stream 统计。我们会把前面学的 Java 核心知识全用上,体会它们怎么协作。

一、需求分析

我们要做的 CLI 支持这些命令:

todo add "买菜" -p high -t 生活 -t 周末     # 添加待办 (优先级 + 标签)
todo list [--all] [--tag 生活] [--priority high]  # 列出待办
todo done <id>                              # 标记完成
todo delete <id>                            # 删除
todo edit <id> -t "新内容"                  # 编辑
todo clear --done                           # 清除已完成
todo stats                                  # 统计

功能需求:

  • 增删改查——基本的 CRUD。
  • 优先级——high / medium / low,影响排序。
  • 标签——一个待办可有多个标签,支持按标签过滤。
  • 持久化——保存到文件,重启不丢。
  • 统计——用 Stream 算完成率、按标签分组等。

二、架构设计

好的项目从设计开始。我们用两个模式让代码清晰:

2.1 命令模式(Command Pattern)

每条命令是一个对象,有 execute 方法。主程序解析输入后分发到对应命令。这样加新命令不用改主流程——符合开闭原则。

输入 "add 买菜 -p high"

解析: command=add, args=[买菜], options={p: high}

分发: CommandRegistry.get("add").execute(args, options)

AddCommand 执行: 创建 Todo, 存入 Repository

2.2 仓储模式(Repository Pattern)

数据访问封装在 TodoRepository——业务逻辑不关心数据存哪(内存/文件/数据库),只调 repository.save(todo)。换存储只改 Repository,业务层不动。

Command → Repository → Storage (文件/内存)

2.3 整体类图

Main (入口)
 ├ CommandLineParser (解析命令行)
 ├ CommandRegistry (命令注册表)
 │   ├ AddCommand
 │   ├ ListCommand
 │   ├ DoneCommand
 │   ├ DeleteCommand
 │   ├ EditCommand
 │   ├ ClearCommand
 │   └ StatsCommand
 └ TodoRepository (数据访问)
     ├ Todo (实体)
     └ TodoStorage (文件持久化)

三、核心实体设计

3.1 Todo 实体

public class Todo {
    private Long id;
    private String title;
    private Priority priority;       // HIGH / MEDIUM / LOW
    private Set<String> tags;
    private boolean done;
    private LocalDateTime createdAt;
    private LocalDateTime completedAt;

    // 枚举优先级, 自带排序权重
    public enum Priority {
        HIGH(1), MEDIUM(2), LOW(3);
        public final int weight;
        Priority(int w) { this.weight = w; }
    }
}

用枚举表示优先级——比字符串安全,编译期就能发现拼写错误。weight 字段用于排序。

3.2 命令行解析

命令行有三种成分:

  • 命令名——add/list/done
  • 位置参数——"买菜"<id>
  • 选项——-p high-t 生活(短选项)。
class CommandLine {
    String command;
    List<String> args = new ArrayList<>();
    Map<String, List<String>> options = new HashMap<>();
}

四、命令实现思路

4.1 AddCommand

解析 title 和选项 -p(优先级)、-t(标签,可多次),创建 Todo 对象,存入 Repository。

public void execute(CommandLine cli) {
    String title = String.join(" ", cli.args);
    Todo todo = new Todo(title);
    if (cli.options.containsKey("p")) {
        todo.setPriority(Priority.valueOf(cli.options.get("p").get(0).toUpperCase()));
    }
    if (cli.options.containsKey("t")) {
        todo.setTags(new HashSet<>(cli.options.get("t")));
    }
    repository.save(todo);
    System.out.println("已添加: " + todo);
}

4.2 ListCommand

查所有待办,按优先级和创建时间排序。支持过滤选项 --all(含已完成)、--tag--priority

public void execute(CommandLine cli) {
    Stream<Todo> stream = repository.findAll().stream();
    if (!cli.options.containsKey("all")) {
        stream = stream.filter(t -> !t.isDone());   // 默认只显示未完成
    }
    if (cli.options.containsKey("tag")) {
        String tag = cli.options.get("tag").get(0);
        stream = stream.filter(t -> t.getTags().contains(tag));
    }
    // 按优先级 + 创建时间排序
    List<Todo> list = stream
        .sorted(Comparator.comparingInt((Todo t) -> t.getPriority().weight)
                          .thenComparing(Todo::getCreatedAt))
        .collect(Collectors.toList());
    // 打印表格
    printTable(list);
}

Stream API 在这里大展身手——过滤、排序、收集,链式调用一气呵成。

4.3 StatsCommand

用 Stream 算统计——总数、完成数、完成率、按优先级分组、按标签分组。

public void execute(CommandLine cli) {
    List<Todo> all = repository.findAll();
    long total = all.size();
    long done = all.stream().filter(Todo::isDone).count();
    double rate = total == 0 ? 0 : (double) done / total * 100;

    System.out.printf("总数: %d, 完成: %d, 完成率: %.1f%%%n", total, done, rate);

    // 按优先级分组统计
    Map<Priority, Long> byPriority = all.stream()
        .collect(Collectors.groupingBy(Todo::getPriority, Collectors.counting()));
    System.out.println("按优先级: " + byPriority);

    // 按标签分组 (flatMap 展开)
    Map<String, Long> byTag = all.stream()
        .flatMap(t -> t.getTags().stream())
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
    System.out.println("按标签: " + byTag);
}

flatMap 把每个 Todo 的标签集合”摊平”成一个流——这是 Stream 处理嵌套结构的利器。

五、持久化设计

TodoRepository 接口不绑定具体存储。我们实现一个 FileTodoRepository——用文件保存。但 Piston 在线环境文件路径受限,所以我们同时实现一个 InMemoryTodoRepository 保证可运行。

interface TodoRepository {
    Todo save(Todo todo);
    Optional<Todo> findById(Long id);
    List<Todo> findAll();
    void deleteById(Long id);
    void update(Todo todo);
}

// 文件实现: 每行一个 Todo, 格式 id|title|priority|done|tags|createdAt
class FileTodoRepository implements TodoRepository {
    private final Path file;
    public FileTodoRepository(Path file) { this.file = file; }

    public List<Todo> findAll() {
        if (!Files.exists(file)) return new ArrayList<>();
        return Files.lines(file)
            .map(this::parseLine)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }
    // save / delete / update 类似, 全量重写文件
}

实际项目会用数据库(SQLite/H2),这里文件方案够用且简单。

六、完整可运行代码

下面是一个完整可运行的 Todo CLI。由于 Piston 环境没有真正的终端交互,我们用模拟的命令序列演示所有功能——相当于把用户输入硬编码进 main 方法,让程序”自己跟自己对话”。

Java · 在线运行

观察重点CommandLine.parse 处理引号和选项;doList 用 Stream 链式过滤+排序;doStatsflatMap 展开标签、groupingBy 分组统计;EnumMap 用于枚举键的高效映射;命令分发用 switch,加新命令只改一处。

七、用到的知识点回顾

这个项目综合运用了前面学的大量知识:

知识点在哪用
集合(List/Set/Map)Todo 存储、标签集合、命令参数
Stream API过滤、排序、分组统计
枚举优先级定义
异常命令解析、ID 查找
日期时间创建时间记录
泛型Optional\<Todo\>
OptionalfindById 返回值
仓库模式TodoRepository 隔离存储
命令模式每条命令一个方法
LambdaStream 的 filter/map/collect
方法引用Todo::isDoneSystem.out::println
Comparable/Comparator按优先级+ID 排序

八、扩展方向

这个 CLI 还能往很多方向扩展:

  1. 文件持久化——实现 FileTodoRepository,每行一个 Todo,用 Files.lines 读取。
  2. JSON 存储——用 Jackson/Gson 序列化成 JSON,可读性更好。
  3. 多用户——加 User 实体,每个用户独立的 Todo 列表。
  4. 提醒功能——加 dueDate 字段,用 ScheduledExecutorService 定时检查过期。
  5. Web 界面——把 Repository 换成数据库,加一层 Spring Boot Controller 变 Web 应用。
  6. 导入导出——支持 CSV/Markdown 格式导出。
  7. 撤销重做——用命令模式 + 历史栈实现 undo/redo
  8. 彩蛋——用 ANSI 颜色码给终端输出加颜色(如优先级 high 显示红色)。
// ANSI 颜色示例
String RED = "\\u001B[31m";
String RESET = "\\u001B[0m";
System.out.println(RED + "[高优先级]" + RESET + " 紧急任务");

九、本章小结

阶段要点
需求分析明确功能边界、用户场景
架构设计命令模式分发、仓储模式隔离存储
实体设计Todo + Priority 枚举 + 标签集合
命令解析引号处理、选项提取
Stream 应用filter 过滤、sorted 排序、groupingBy 分组
扩展性加命令改一处,换存储改 Repository

记忆口诀

  • 项目三步走——需求定边界,设计定骨架,实现填血肉。
  • 命令模式——一条命令一个 handler,分发靠 switch。
  • 仓储模式——业务调接口,存储随便换。
  • Stream 三板斧——filter 挑、sorted 排、collect 收。
  • 优先级排序——枚举带 weight,Comparator 链式比。

结语

这个 Todo CLI 不大——三百多行代码——但它把 Java 核心知识串成了一条线。集合是骨架,Stream 是肌肉,异常是免疫,枚举是关节。一个项目下来,你会发现自己对 Java 的理解从”知道这些 API”变成了”会用这些 API 解决问题”。

下一阶段是最后一章——Java 面试题精讲。我们把前面所有章节的知识点提炼成高频面试题,每题给出”答到什么程度算合格”。带着这个项目的经验去理解面试题,你会发现很多题不再是死记硬背——而是你亲手写过、亲眼看过的东西。