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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 语言基础/课时/综合练习与 dsa-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 分工

综合练习与 dsa-java 分工

小项目、Spring Boot 下一步。

45 分钟

综合练习与 dsa-java 分工

这一课你将能做到

  1. 独立完成一个 200 行以内的「用户管理系统」控制台项目,串起类型、集合、OOP、异常、IO 五大知识块
  2. 对照检查清单自评 13 课掌握度,明确薄弱项回去补哪一课
  3. 说清本课程与 dsa-java(算法路线)的分工边界,知道下一步进 Spring Boot 路线该带什么行囊

为什么要学

前面的课都是「单点技能」:一个知识点一个 Demo。真实工程是技能的组合:一个方法里同时有类型判断、集合操作、异常处理、对象设计。单点会 ≠ 组合会——中间隔着「面对需求不知道从哪下手」这道坎。

这一课用一个小项目打通任督二脉。场景选「用户管理」,因为它是本站后端的真实缩影:有实体(User)、有存储(Map 内存模拟 → 文件持久化模拟 MySQL)、有校验(异常)、有增删改查(Service 层逻辑)。

同时划清一个重要边界:语法课不负责刷题。链表反转、二叉树遍历、动态规划,请去 dsa-java 路线——两边目标不同,混着学两头空。

先建立图像

项目的分层雏形(Spring 后端三层架构的控制台极简版):

main(入口/菜单循环)        ← 相当于 Controller:接收指令、分发
   │
UserManager(业务层)        ← 相当于 Service:增删改查规则
   │
Map<Long, User>(内存存储)  ← 相当于数据库
   │
UserFileStore(持久层)      ← 相当于 MySQL:读写 kv-store.txt 落盘

退出前落盘、启动时加载 → 程序重启数据不丢(这就是「持久化」的直觉)

跟着做

步骤 1:定义实体与业务异常

新建 UserApp.java(单文件多类,java UserApp.java 直跑):

import java.io.IOException;
import java.nio.file.*;
import java.util.*;

// 实体:不可变属性少、可变状态多,用传统类;字段校验走 setter
class User {
    private long id;
    private String name;
    private int age;

    public User(long id, String name, int age) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        this.id = id;
        this.name = name;
        setAge(age);
    }

    public long getId() { return id; }
    public String getName() { return name; }
    public int getAge() { return age; }

    public void setAge(int age) {
        if (age < 0 || age > 150) throw new IllegalArgumentException("年龄非法: " + age);
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{id=%d, name=%s, age=%d}".formatted(id, name, age);
    }
}

// 业务异常:继承 RuntimeException,带错误码(呼应第 11 课)
class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(long id) {
        super("用户不存在: id=" + id);
    }
}

class DuplicateUserException extends RuntimeException {
    public DuplicateUserException(String name) {
        super("用户名已存在: " + name);
    }
}

步骤 2:持久层——文件存储

// 继续加到 UserApp.java 里
class UserFileStore {
    private final Path file = Path.of("users.txt");

    // 格式约定:每行 id,name,age(简易 CSV)
    void save(Map<Long, User> users) {
        StringBuilder sb = new StringBuilder();
        users.values().forEach(u ->
                sb.append(u.getId()).append(",")
                  .append(u.getName()).append(",")
                  .append(u.getAge()).append("\n"));
        try {
            Files.writeString(file, sb.toString());
        } catch (IOException e) {
            throw new RuntimeException("落盘失败", e);   // 包装受检异常为非受检上抛
        }
    }

    Map<Long, User> load() {
        Map<Long, User> users = new HashMap<>();
        if (Files.notExists(file)) return users;
        try (var lines = Files.lines(file)) {
            lines.forEach(line -> {
                if (line.isBlank()) return;              // 跳过空行
                String[] p = line.split(",");
                users.put(Long.parseLong(p[0]),
                        new User(Long.parseLong(p[0]), p[1], Integer.parseInt(p[2])));
            });
        } catch (IOException e) {
            throw new RuntimeException("加载失败", e);
        }
        return users;
    }
}

步骤 3:业务层——增删改查

class UserManager {
    private final Map<Long, User> users = new HashMap<>();
    private final UserFileStore store = new UserFileStore();
    private long nextId = 1;

    public UserManager() {
        users.putAll(store.load());                  // 启动加载已有数据
        nextId = users.keySet().stream().max(Long::compare).orElse(0L) + 1;
    }

    public User add(String name, int age) {
        boolean exists = users.values().stream().anyMatch(u -> u.getName().equals(name));
        if (exists) throw new DuplicateUserException(name);
        User u = new User(nextId++, name, age);
        users.put(u.getId(), u);
        store.save(users);                           // 每次变更即落盘
        return u;
    }

    public User get(long id) {
        User u = users.get(id);
        if (u == null) throw new UserNotFoundException(id);
        return u;
    }

    public List<User> list() {
        // 按 id 排序输出:HashMap 无序,转 List 再排
        return users.values().stream()
                .sorted(Comparator.comparingLong(User::getId))
                .toList();
    }

    public void updateAge(long id, int newAge) {
        get(id).setAge(newAge);                      // get 顺带做了存在性校验
        store.save(users);
    }

    public void remove(long id) {
        if (users.remove(id) == null) throw new UserNotFoundException(id);
        store.save(users);
    }
}

步骤 4:入口层——菜单循环

public class UserApp {
    public static void main(String[] args) {
        UserManager manager = new UserManager();
        Scanner sc = new Scanner(System.in);
        System.out.println("=== 用户管理系统 (help 查看命令) ===");

        while (true) {
            System.out.print("> ");
            String cmd = sc.nextLine().strip();
            try {
                switch (cmd) {
                    case "" -> { /* 空输入跳过 */ }
                    case "help" -> System.out.println(
                            "add <名字> <年龄> | get <id> | list | age <id> <新年龄> | del <id> | exit");
                    case "list" -> manager.list().forEach(System.out::println);
                    case "exit" -> {
                        System.out.println("再见!");
                        return;
                    }
                    default -> handle(manager, cmd);
                }
            } catch (UserNotFoundException | DuplicateUserException e) {
                System.out.println("业务提示: " + e.getMessage());    // 已知业务错误,友好提示
            } catch (NumberFormatException e) {
                System.out.println("参数应为数字: " + e.getMessage());
            } catch (IllegalArgumentException e) {
                System.out.println("输入不合法: " + e.getMessage());
            }
        }
    }

    static void handle(UserManager m, String input) {
        String[] p = input.split("\\s+");
        switch (p[0]) {
            case "add" -> System.out.println("已添加: " + m.add(p[1], Integer.parseInt(p[2])));
            case "get" -> System.out.println(m.get(Long.parseLong(p[1])));
            case "age" -> { m.updateAge(Long.parseLong(p[1]), Integer.parseInt(p[2])); System.out.println("已更新"); }
            case "del" -> { m.remove(Long.parseLong(p[1])); System.out.println("已删除"); }
            default -> System.out.println("未知命令,输入 help 查看");
        }
    }
}

运行效果:

> add tom 20
已添加: User{id=1, name=tom, age=20}
> add jerry 22
已添加: User{id=2, name=jerry, age=22}
> add tom 30
业务提示: 用户名已存在: tom
> list
User{id=1, name=tom, age=20}
User{id=2, name=jerry, age=22}
> exit
(重启程序后 > list 依然能列出两条 —— 落盘生效)

步骤 5:改造挑战(自选,验证迁移能力)

按顺序尝试,每一条都对应一个「将来 Spring 里会怎么写」的映射:

  1. 去掉 users.txt,换成 MySQL 时代的思考:UserManager 的方法签名不变,只换存储实现——体会「面向接口编程」:先抽 UserRepository 接口,再写 FileUserRepository 实现
  2. 给 age 命令加防并发:思考两个线程同时 add 会怎样(HashMap 非线程安全),读读 ConcurrentHashMap 的文档
  3. 把 CSV 换成 JSON:用 Maven 建 maven-demo 工程(上一课),引入 gson 依赖,toJson/fromJson 重写落盘

步骤 6:全课程自评清单

逐条自测,答不上来的标注回去补哪课:

能力点判据补课
编译运行体系说清 javac/java/JVM 关系,能跑通编译产物第 1 课
语法基础逐词解释 main 签名,包/import/命名不出错第 2 课
类型系统说出 Integer 缓存陷阱与金额精度方案第 3 课
控制流手写九九乘法表和箭头 switch第 4 课
方法用代码证明 Java 只有值传递第 5 课
数组Arrays.toString/sort/copyOf 用法条件反射第 6 课
OOP手写带校验 setter 的实体类 + record第 7 课
继承多态解释 @Override 的意义和动态绑定第 8 课
集合Map 计数、removeIf、HashMap 原理一段话第 9 课
字符串== vs equals 满分作答 + StringBuilder 时机第 10 课
异常受检/非受检边界 + 自定义业务异常第 11 课
IO三行读写 + try-with-resources + 编码第 12 课
Maven读懂 pom、跑通 package、说出 starter 机制第 13 课

步骤 7:与 dsa-java 的分工

维度java-fundamentals(本课)dsa-java
目标会用 Java 写业务代码会用 Java 解算法题
内容重点语法、OOP、集合 API、异常、Maven链表/树/图、排序/查找、双指针、DP
集合侧重ArrayList/HashMap 怎么用底层结构复杂度分析与手写实现
代码形态完整工程、分层、可维护单文件刷题风格、重正确性与复杂度
学习节奏看懂 → 抄写 → 独立写小系统一题多解 → 复杂度优化 → 限时

实践约定:两线并行时,先花 30 分钟在本路线补语法,再进 dsa-java 刷题。刷题碰到「Java 语法卡壳」(比如没见过 Deque 的 API),回本路线对应课程补,不要在算法题里硬啃语法。

讲清楚要点

  1. 小项目检验的不是记忆而是组合:需求来了先分层(入口/业务/存储/实体),再逐层实现——这个思维顺序在 Spring 开发里一模一样。
  2. 「存储可替换」是分层的全部意义:今天文件、明天 MySQL,业务层代码不动。第 1 个改造挑战就是提前体验依赖注入。
  3. 异常分层处理:业务层抛语义异常(UserNotFoundException),入口层统一接住翻译成人话——Spring 的 @RestControllerAdvice 同款思想。
  4. 持久化的直觉:程序退出内存清空、文件还在——重启后加载文件恢复状态。Redis 的 RDB 快照、MySQL 的数据文件,本质都是这个模型的工程化加强版。
  5. 查 API 的能力比背 API 重要:stream().sorted(...)、Files.lines 这类方法记不全很正常,会查 Javadoc、会在 IDE 里看方法签名,才是长期生产力。

你可能会懵的地方

  • 这个项目和真实 Spring 后端差距多大? 形似度很高:分层、实体、异常、持久化全对应。差距在「连接层」——HTTP 请求如何变成方法调用(DispatcherServlet)、SQL 如何执行(MyBatis/JPA)、事务如何保证。这些是 Spring Boot 路线的内容,本项目的每个类到时候都能找到对应位置。

  • 为什么要自己实现一遍,直接学 Spring 不是更快? 快是错觉。不懂 Map 就学 Redis 客户端、不懂接口多态就学 @Autowired、不懂异常就学全局处理器——全是在背咒语。基础课的作用就是让框架课「每一步都有根基」。

  • 代码里用了 stream(),前面课没细讲? Stream API 是 JDK 8 的数据处理语法糖(filter/map/sorted/collect),属于「应该会用」级别。这里的用法看注释能懂即可;系统学习建议在学完集合后按需补充,不必现在深入。

  • 感觉都学过但都记不住,正常吗? 正常且必然。编程能力 = 知识 × 练习次数。现在你缺的只是练习量:把这个小项目默写第二遍、第三遍,比再看三遍视频有效。自评清单低于 80% 的项,优先回去补对应课的「跟着做」。

  • 下一步先学 dsa-java 还是 Spring Boot? 看目标:求职笔试/面试算法 → dsa-java 优先;尽快做出能跑的 Web 项目 → Spring Boot 优先,边做边补算法。两条线不冲突,顺序由你的时间表决定。

错法对照

错法问题正解
所有代码塞进 main 方法三百行无法维护分层拆类:实体/业务/存储/入口
存储层直接被 main 调用跳过业务层,校验无处安放main → Manager → Store 单向依赖
catch (Exception e) 包住整个 main程序错得莫名其妙按异常类型分级 catch,业务异常单独提示
每次 add 都重写整个文件觉得浪费小数据量完全可接受;先跑通再优化记住 Premature Optimization 教训
刷题时用本课思路写完整工程dsa-java 场景讲究快两套习惯分开:算法题单文件简洁为主
学完不写项目直接开 Spring框架照抄,报错全懵至少独立完成本项目一次(不看源码)

卡点急救

  1. 运行报「找不到或无法加载主类 UserApp」:文件名与 public 类名不一致,或没在文件所在目录运行。单文件直跑 java UserApp.java。
  2. 重启后数据没恢复:检查 users.txt 是否生成在工作目录(运行命令的目录);load 的 split 解析是否与 save 的格式一致(逗号、行序)。
  3. add 带空格的名字报数组越界:split("\\s+") 对空输入产生意外长度,入口处先 p.length 校验参数个数。
  4. NumberFormatException 反复出现:用户输入混入了非数字。已在 main 统一 catch,检查命令拼写(age 1 20 三个词一个不能少)。
  5. 中文在文件里乱码:Windows 记事本默认编码问题;代码里显式 Files.writeString(file, sb.toString(), StandardCharsets.UTF_8),读取同理。

面试常问

  • 你的小项目怎么设计的?为什么这么分层?(简历项目题的开场变体) 入口负责交互、业务层封装规则、存储层负责持久化、实体承载数据;层间单向依赖。收益:存储可替换(文件→MySQL)、异常可统一处理、每层可独立测试。

  • 怎么把文件存储换成 MySQL 而不改业务代码? 抽象 UserRepository 接口,业务层只依赖接口;文件实现与数据库实现都实现该接口,装配处切换实现类。这就是面向接口编程 + 依赖注入的雏形。

  • HashMap 非线程安全,你的程序会不会有问题? 单线程控制台程序无碍。多线程环境(Web 服务天然多线程)用 ConcurrentHashMap,或对临界区加锁;这也是 Spring Bean 默认单例时要求成员无状态的原因。

  • 如果数据量到了千万级,这个方案哪里先崩? 全量 load 进内存(OOM)、每次变更全量落盘(IO 风暴)、线性查找(遍历 O(n))。对应生产解法:数据库索引查找、增量持久化/事务日志、缓存热点数据——正好引出 MySQL + Redis 的分工。

自测三问

  1. 不看源码,画出「用户管理系统」的四个部分及依赖方向。
  2. 业务异常为什么继承 RuntimeException?如果继承 Exception,调用链会发生什么?
  3. 本课程和 dsa-java 各自解决什么问题?你接下来 4 周怎么分配两边的时间?

小结一句话

用一个小系统把 13 课知识拧成整体:分层、面向接口、异常分级、持久化直觉——这套骨架就是 Spring 后端的雏形;语法到此收官,算法去 dsa-java,Web 去下一站 Spring Boot。

延伸

  • Oracle:Java 官方教程总目录
  • 本站算法路线 dsa-java(两线并行,先补语法再刷题)
  • 下一站:Spring Boot 快速上手——用 @RestController 把今天的 UserManager 暴露成 HTTP API。
加载本节练习…
← 上一课Maven 与项目结构