四则运算自动生成与批改结对项目
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/ |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703 |
| 这个作业的目标 | 完成结对项目,分享经验,总结教训 |
一、团队信息
- 成员 1:王安亮 学号:3124004439
- 成员 2:袁伟轩 学号:3124004449
- Github 项目地址:https://github.com/Wh1teFe/arithmetic-project
二、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() 递归生成方法。
原因:
- 程序对表达式有严格约束:减法结果非负、除法必须为真分数,随机生成大量非法表达式会直接丢弃、重新递归生成,产生大量无效重试。
- 题目去重依靠 HashSet 签名判断,题目数量越大,重复概率越高,进一步加剧重试开销。
- 每次重试都会重复执行分数约分 gcd、四则运算、字符串格式化,造成大量 CPU 资源浪费。
- 大批量生成题目时,频繁创建 Fraction、Expression、String 临时对象,内存分配频繁,造成轻微 GC 压力。
3.3 性能优化思路
- 优化生成逻辑,前置合法性判断,减少无效递归重试次数。
- 优化字符串拼接,减少频繁 toString 调用,降低格式化开销。
- 文件写入采用批量缓冲区写入,减少 IO 次数。
- 优化签名去重逻辑,减少字符串比对开销。
3.4 性能分析截图

四、设计实现过程
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 | ✅ 通过 |
正确性说明
- 分数运算正确性:Fraction 类内部使用 long 存储分子分母,运算后自动约分,避免精度丢失。
- 表达式合法性:减法非负、除法真分数的约束在递归生成时强制校验,不满足直接重试。
- 去重有效性:通过规范化签名(交换律对称化)保证数学等价的表达式不会重复出现。
- 批改准确性:Evaluator 重新计算标准答案,与用户答案逐行比对,避免人工判分误差。
七、项目小结
7.1 结对感受
本次结对项目让我们第一次真正体验了两人协作开发的完整流程。从需求分析、设计讨论到编码实现、测试调试,两个人的思维碰撞让我们少走了很多弯路。一个人写代码时容易忽略的边界情况,另一个人很快就能发现。
7.2 闪光点与建议
成员 1 闪光点:负责核心算法设计,递归表达式生成逻辑清晰,签名去重方案巧妙,大幅减少了重复题目问题。
成员 1 建议:可以更早地写单元测试,避免后期集中调试浪费时间。
成员 2 闪光点:负责文件 IO 和主流程调度,代码规范整洁,异常处理完善,程序鲁棒性好。
成员 2 建议:设计阶段可以画更详细的类图,减少后期返工。
7.3 经验教训
- 设计先行:先画好类图和流程图再写代码,比边写边改效率高很多。
- 小步提交:Git 频繁 commit,出问题可以快速回滚。
- 性能意识:写完功能后用 JFR 做一次性能分析,能发现很多肉眼看不到的瓶颈。
- 结对不是分工:两个人要一起讨论关键设计,不能各写各的最后拼代码。
浙公网安备 33010602011771号