四则运算自动生成与批改结对项目

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703
这个作业的目标 完成结对项目,分享经验,总结教训

一、团队信息


二、PSP2.1 软件开发过程记录

表格

PSP2.1 阶段 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
・Estimate 估计任务总时间 10 15
Development 开发
・Analysis 需求分析(含学习新技术) 30 40
・Design Spec 生成设计文档 20 25
・Design Review 设计复审 10 15
・Coding Standard 代码规范制定 10 10
・Design 具体设计 30 40
・Coding 具体编码 60 80
・Code Review 代码复审 10 15
・Test 测试(自我测试、修改、提交) 40 60
Reporting 报告
・Test Report 测试报告 10 15
・Size Measurement 计算工作量 5 5
・Postmortem & Process Improvement Plan 事后总结与改进计划 10 15
合计 245 330

三、效能分析

本次性能分析使用 JDK 自带 JFR(JDK Flight Recorder)低开销采样 + JDK Mission Control(JMC)可视化分析 完成,全程无第三方付费工具,适配 IDEA 社区版环境。

3.1 性能分析过程

在程序启动时添加 JFR 虚拟机参数,录制程序大批量生成题目(10000 题)的完整运行数据,生成 record.jfr 性能文件,通过 JMC 解析 CPU 热点、内存分配、方法耗时。本次优化耗时约 40 分钟,主要用于定位瓶颈、分析重试机制、设计优化方案。

3.2 性能瓶颈分析

通过 JMC 火焰图、方法概要分析可以清晰看出:程序消耗最大的函数为 Expression.generate() 递归生成方法

原因:

  1. 程序对表达式有严格约束:减法结果非负、除法必须为真分数,随机生成大量非法表达式会直接丢弃、重新递归生成,产生大量无效重试。
  2. 题目去重依靠 HashSet 签名判断,题目数量越大,重复概率越高,进一步加剧重试开销。
  3. 每次重试都会重复执行分数约分 gcd、四则运算、字符串格式化,造成大量 CPU 资源浪费。
  4. 大批量生成题目时,频繁创建 Fraction、Expression、String 临时对象,内存分配频繁,造成轻微 GC 压力。

3.3 性能优化思路

  1. 优化生成逻辑,前置合法性判断,减少无效递归重试次数。
  2. 优化字符串拼接,减少频繁 toString 调用,降低格式化开销。
  3. 文件写入采用批量缓冲区写入,减少 IO 次数。
  4. 优化签名去重逻辑,减少字符串比对开销。

3.4 性能分析截图

image


四、设计实现过程

4.1 整体结构设计

本项目一共分为五个核心类,分工明确、低耦合高内聚:

表格

类名 职责
Fraction 分数类,封装分数存储、约分、最大公约数、加减乘除运算、带分数格式化、字符串解析
Expression 表达式类,递归随机生成合法四则运算表达式,控制运算规则、去重签名、约束合法结果
Evaluator 求值类,解析表达式字符串,还原运算逻辑、精准计算标准答案
Util 工具类,封装文件读取、文件写入,统一处理 IO 操作
Main 主类,程序入口,解析命令行参数,区分题目生成模式、自动批改模式

4.2 类与函数关系

  • Main 类调用 Expression 生成题目、调用 Util 读写文件
  • Evaluator 依赖 Fraction 完成表达式计算
  • Expression 全程依赖 Fraction 完成数值运算与合法性校验
  • 所有类通过工具类统一完成文件持久化,模块职责完全分离

4.3 核心逻辑流程

程序启动后判断命令行参数:

  • 生成模式:循环调用递归生成表达式 → 校验合法 → 签名去重 → 写入本地 txt 文件
  • 批改模式:读取题目与答案文件 → 逐行计算标准答案 → 比对用户答案 → 统计对错并生成批改文件

五、代码说明

5.1 分数约分核心 gcd 算法(Fraction.java)

// 辗转相除法求最大公约数,保证分数最简形式
private static long gcd(long a, long b) {
    a = Math.abs(a);
    b = Math.abs(b);
    while (b != 0) {
        long t = a % b;
        a = b;
        b = t;
    }
    return a == 0 ? 1 : a;
}

思路:每次构造 Fraction 时自动约分,保证所有分数以最简形式存储,避免重复题目因约分不同而无法去重。

5.2 表达式递归生成(Expression.java)

public static Expression generate(int opCount, int maxOp, int range) {
    if (opCount == 0) {
        // 随机生成自然数或分数作为叶子节点
        if (rand.nextDouble() < 0.4) {
            long num = rand.nextLong(range);
            return new Expression(String.valueOf(num), new Fraction(num, 1), String.valueOf(num));
        } else {
            long n = rand.nextLong(range);
            long d = rand.nextLong(range - 1) + 1;
            Fraction f = new Fraction(n, d);
            return new Expression(f.toString(), f, f.toString());
        }
    }
    // 递归生成左右子表达式
    Expression left = generate(opCount - 1, maxOp, range);
    Expression right = generate(opCount - 1, maxOp, range);
    // 随机选择运算符,校验合法性,不合法则重试
    // ...
}

思路:递归拆分表达式,左右子表达式独立生成;通过签名(signature)记录表达式的规范化结构,用于去重;减法校验非负、除法校验真分数,不满足则递归重试。

5.3 表达式求值(Evaluator.java)

public static Fraction evaluate(String expr) {
    // 解析表达式字符串,按运算符优先级计算
    // 支持 +、-、×、÷ 四则运算
    // 返回 Fraction 类型的标准答案
}

思路:批改模式下,读取题目字符串,调用 Evaluator 计算标准答案,与用户答案逐行比对。

5.4 主程序入口(Main.java)

public static void main(String[] args) {
    // 解析命令行参数
    // 含 -e 和 -a → 批改模式
    // 含 -n 和 -r → 生成模式
    // 调用对应模块完成任务
}

思路:根据命令行参数自动切换生成 / 批改两种模式,统一入口,职责清晰。


六、测试运行

测试用例(共 10 组)

表格

序号 测试输入 预期结果 实际结果 是否通过
1 -n 10 -r 10 生成 10 道题目,数值范围 0~10 生成 10 道题目,数值均在范围内 ✅ 通过
2 -n 100 -r 20 生成 100 道不重复题目 生成 100 道,无重复 ✅ 通过
3 -n 1000 -r 30 生成 1000 道题目,无负数答案 全部非负,无负数 ✅ 通过
4 减法结果校验 所有减法题答案 ≥ 0 随机抽查 50 题,全部 ≥ 0 ✅ 通过
5 除法结果校验 所有除法题答案为真分数 随机抽查 50 题,均为真分数 ✅ 通过
6 带分数输出 答案 > 1 时输出带分数格式(如 2'1/3) 抽查答案均为带分数格式 ✅ 通过
7 -e Exercises.txt -a Answers.txt 批改正确答案,全部判对 Correct: 10 (1,2,...,10) ✅ 通过
8 批改模式(故意改错 2 题) 正确 8 题,错误 2 题 Correct: 8, Wrong: 2 ✅ 通过
9 缺少 -r 参数 提示参数错误 输出 "参数错误!必须提供 -r" ✅ 通过
10 重复题目去重 连续生成 500 题,无重复 签名集合大小 = 500 ✅ 通过

正确性说明

  1. 分数运算正确性:Fraction 类内部使用 long 存储分子分母,运算后自动约分,避免精度丢失。
  2. 表达式合法性:减法非负、除法真分数的约束在递归生成时强制校验,不满足直接重试。
  3. 去重有效性:通过规范化签名(交换律对称化)保证数学等价的表达式不会重复出现。
  4. 批改准确性:Evaluator 重新计算标准答案,与用户答案逐行比对,避免人工判分误差。

七、项目小结

7.1 结对感受

本次结对项目让我们第一次真正体验了两人协作开发的完整流程。从需求分析、设计讨论到编码实现、测试调试,两个人的思维碰撞让我们少走了很多弯路。一个人写代码时容易忽略的边界情况,另一个人很快就能发现。

7.2 闪光点与建议

成员 1 闪光点:负责核心算法设计,递归表达式生成逻辑清晰,签名去重方案巧妙,大幅减少了重复题目问题。
成员 1 建议:可以更早地写单元测试,避免后期集中调试浪费时间。

成员 2 闪光点:负责文件 IO 和主流程调度,代码规范整洁,异常处理完善,程序鲁棒性好。
成员 2 建议:设计阶段可以画更详细的类图,减少后期返工。

7.3 经验教训

  1. 设计先行:先画好类图和流程图再写代码,比边写边改效率高很多。
  2. 小步提交:Git 频繁 commit,出问题可以快速回滚。
  3. 性能意识:写完功能后用 JFR 做一次性能分析,能发现很多肉眼看不到的瓶颈。
  4. 结对不是分工:两个人要一起讨论关键设计,不能各写各的最后拼代码。