结对项目:自动生成小学四则运算题目的命令行程序
本作业属于:软件工程(计科 24 级 78 班)
作业要求:https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703
作业目标:结对完成一个命令行四则运算题目生成与判分程序,实践"需求 → 设计 → 编码 → 测试 → 性能 → 交付"全流程。
姓名:张吉安 学号:3124004489
姓名:侯剑锋 学号:3124004468
GitHub:https://github.com/2553a/arithmetic-generator
一、项目简介
本项目使用 Java 17 编写命令行程序,随机生成包含自然数、真分数和 1~3 个运算符的小学四则运算题。程序保证任何减法子表达式不产生负数、任何除法节点不除以零且商为真分数;通过表达式树 canonical form 去重,并提供独立批改模式。
# 生成
java -cp target\classes com.eso.arithmetic.Main -n 10 -r 10
# 批改
java -cp target\classes com.eso.arithmetic.Main -e Exercises.txt -a Answers.txt
程序生成 UTF-8 编码的 Exercises.txt、Answers.txt 和 Grade.txt。
二、PSP 表格
开始编码前,我们按照附录中的 PSP2.1 表格拆分并估计了工作量。项目结束后,两人结合开发、测试和博文整理过程,对实际用时进行了回溯记录。
| PSP2.1 | Personal Software Process Stages | 预计耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | ||
| · Estimate | · 估计这个任务需要多少时间 | 20 | 25 |
| Development | 开发 | ||
| · Analysis | · 需求分析(包括学习新技术) | 60 | 75 |
| · Design Spec | · 生成设计文档 | 70 | 80 |
| · Design Review | · 设计复审(和同事审核设计文档) | 40 | 35 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 20 | 20 |
| · Design | · 具体设计 | 90 | 110 |
| · Coding | · 具体编码 | 240 | 300 |
| · Code Review | · 代码复审 | 50 | 60 |
| · Test | · 测试(自我测试、修改代码、提交修改) | 120 | 150 |
| Reporting | 报告 | ||
| · Test Report | · 测试报告 | 40 | 50 |
| · Size Measurement | · 计算工作量 | 20 | 15 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 90 | 120 |
| 合计 | 860 | 1040 |
最初预计生成算法风险最高,实际最容易忽略的是括号必须保持原树语义,以及去重只能交换 +/× 节点,不能使用结合律把整棵树展开排序。
三、设计实现过程
3.1 类与职责
项目采用 Maven 标准结构,共 12 个主要业务类或接口:
| 模块 | 类 | 职责 |
|---|---|---|
| 入口 | Main |
选择生成/批改模式,统一处理错误 |
| CLI | CommandLineParser |
校验 -n/-r/-e/-a 和文件 |
| 模型 | Fraction |
精确运算、约分、解析、混合数格式化 |
| 模型 | Expression |
表达式统一接口 |
| 模型 | NumberExpression |
表达式树叶子 |
| 模型 | BinaryExpression |
二元节点、求值、格式化、canonical form |
| 模型 | Operator |
运算符、优先级和计算行为 |
| 生成 | ExerciseGenerator |
随机树、递归约束、有界重试、去重 |
| 服务 | ExerciseService |
输出题目和答案 |
| 服务 | ExpressionParser |
把题目文本重新解析为表达式树 |
| 服务 | GradeService |
比较数学值并生成成绩 |
| 验证 | ProjectVerifier |
递归复核最终输出的全部约束 |
3.2 生成流程
我们没有直接拼接字符串,而是先构造表达式树,因此可以在每个内部节点检查约束,并按照真实树结构决定括号和重复关系。
四、关键代码说明
4.1 Fraction:精确运算
Fraction 用 BigInteger 保存分子、分母,不使用 double/float。构造时统一符号并约分:
if (denominator.signum() == 0) throw new IllegalArgumentException("分母不能为 0");
if (denominator.signum() < 0) {
numerator = numerator.negate();
denominator = denominator.negate();
}
BigInteger gcd = numerator.gcd(denominator);
this.numerator = numerator.divide(gcd);
this.denominator = denominator.divide(gcd);
因此 1/6 + 1/8 精确得到 7/24;批改时 5/6 与 10/12 相等;11/8 输出为 1’3/8。
4.2 递归保证减法和除法约束
if (operator == Operator.SUBTRACT
&& left.evaluate().compareTo(right.evaluate()) < 0) {
Expression temporary = left;
left = right;
right = temporary;
}
if (operator == Operator.DIVIDE) {
if (right.evaluate().isZero()) throw new InvalidCandidate();
Fraction quotient = left.evaluate().divide(right.evaluate());
if (!quotient.isProperFraction()) throw new InvalidCandidate();
}
检查发生在每一个递归节点,而非只看最终答案。我们将除法真分数解释为严格满足 0 < numerator < denominator。无效候选会被丢弃,但尝试次数最多为 max(10000, n × 500),所以不会无限循环。
4.3 保留树结构的去重
String l = left.canonicalForm();
String r = right.canonicalForm();
if ((operator == Operator.ADD || operator == Operator.MULTIPLY)
&& l.compareTo(r) > 0) {
String temporary = l; l = r; r = temporary;
}
return operator.name() + "(" + l + "," + r + ")";
生成器通过 canonicalForms.add(...) 同时完成查重和插入,平均查询为 O(1)。算法不展开整棵加法树:
3 + (2 + 1)与(1 + 2) + 3重复;(1 + 2) + 3与(3 + 2) + 1不重复。
4.4 括号和批改
格式化时,子节点优先级较低必须加括号;右子树优先级相同也保留括号,避免左结合改变原树:
boolean parentheses = binary.operator.precedence() < operator.precedence()
|| rightChild && binary.operator.precedence() == operator.precedence();
批改不是字符串比较,而是重新解析题目并比较分数值:
Fraction expected = ExpressionParser.parse(expressionText).evaluate();
Fraction actual = Fraction.parse(contentAfterNumber(answerLine));
(expected.equals(actual) ? correct : wrong).add(number);
非法答案只把当前题记为 Wrong,不会中断整次批改。
五、效能分析
5.1 方法与数据
我们分别生成 100、1000、5000、10000 题,每个规模运行三次;然后使用 Java Flight Recorder 对一次 -n 100000 -r 50 运行采样。性能测试、热点分析和图表整理实际用时约 45 分钟。

| 题量 | 第1轮 | 第2轮 | 第3轮 | 平均 |
|---|---|---|---|---|
| 100 | 39 ms | 39 ms | 36 ms | 38 ms |
| 1,000 | 70 ms | 60 ms | 63 ms | 64 ms |
| 5,000 | 131 ms | 119 ms | 124 ms | 125 ms |
| 10,000 | 214 ms | 219 ms | 187 ms | 207 ms |
JFR 的 100000 题运行耗时 751 ms,得到 16 个执行样本:11 个主要位于 ExerciseService.write、BinaryExpression.format、Fraction.format 及底层字符串/文件操作;3 个位于生成、canonical form 和 HashSet.add;2 个位于 BigInteger.gcd/divide。因此最大热点是输出与格式化。由于采样只有约 1 秒,这些比例用于定位方向,不作为精确 CPU 百分比。
5.2 改进思路
最重要的改进是把两两比较的 O(n²) 去重改成 canonicalForm + HashSet,使单次查重平均为 O(1),整体生成接近 O(n)。其他措施包括:
- 设置最大尝试次数,避免小范围无限重试;
- 批量构造文本行并以 UTF-8 写入;
- 单题最多 3 个运算符,限制树和 canonical form 成本;
- 保留 BigInteger 精确运算,因为数学正确性优先于微小的速度提升。
在 -n 10000 -r 10 压力测试中,两个输出文件均为 10000 行;重新解析验证后,canonical 重复、非法表达式、减法违规、除法违规均为 0。
六、测试运行
JUnit 5 最终结果:7 个测试类、22 个测试方法,Failures: 0, Errors: 0, Skipped: 0,Maven 为 BUILD SUCCESS。
| # | 测试 | 输入/构造 | 预期 | 意义 |
|---|---|---|---|---|
| 1 | 自动约分 | 2/4 |
1/2 |
验证 gcd |
| 2 | 精确加法 | 1/6+1/8 |
7/24 |
排除浮点误差 |
| 3 | 除零 | 1/2÷0 |
受控异常 | 保证安全 |
| 4 | 带分数 | 11/8 |
1’3/8 |
验证格式 |
| 5 | 两种引号 | 1'3/8、1’3/8 |
都等于 11/8 |
兼容输入 |
| 6 | 优先级 | 2×(3+4) |
括号保留,值14 | 保持树语义 |
| 7 | 加法交换 | 23+45、45+23 |
canonical相同 | 验证交换律 |
| 8 | 指定结合例 | 3+(2+1)、(1+2)+3 |
相同 | 对应题目要求 |
| 9 | 禁止扁平化 | (1+2)+3、(3+2)+1 |
不同 | 保留树结构 |
| 10 | 非交换运算 | 8-3、3-8 |
不同 | 减法有序 |
| 11 | 批量生成 | 固定种子500题 | 全部唯一合法 | 递归检查约束 |
| 12 | 范围边界 | -r 1 |
有限时间结束 | 排除死循环 |
| 13 | 等价答案 | 5/6 对 10/12 |
Correct | 非字符串比较 |
| 14 | 非法答案 | bad |
当前题 Wrong | 错误隔离 |
| 15 | 缺少参数 | 只给 -n 10 |
错误和帮助 | CLI健壮性 |
| 16 | 压力验收 | -n 10000 -r 10 |
10000行、0违规 | 性能与完整性 |
除单元测试外,ProjectVerifier 会把最终题目文本重新解析为新表达式树,再递归检查 canonical 唯一性、每个减法/除法节点、叶子范围和运算符数。这能发现仅检查内存对象无法发现的括号格式错误,因此我们能够较有把握地说明程序正确。
七、项目小结与结对感受
成功之处
- 先完成
Fraction再构建表达式树,降低调试复杂度。 - 表达式树统一解决求值、括号、计数和去重,避免字符串拼接失控。
- 为题目强调的结合结构单独编写测试。
HashSet与有界重试支持 10000 题且不会死循环。- 批改比较数学值,能处理等价分数和非法答案。
不足与教训
- “真分数”示例存在歧义,今后应更早向老师确认;本项目已在 README 说明解释。
- 极小范围与极大题量仍可能无法产生足量唯一题目,目前会清晰报错;可进一步研究枚举与随机混合策略。
- JFR 样本时间较短,后续可以引入 JMH,分别测量生成、格式化和 I/O。
- PSP 实际时间应开发过程中同步记录,而不是结束后回忆。
分工、感受及相互评价
- 张吉安主要负责程序代码的设计与实现,包括精确分数、表达式树、随机题目生成、canonical form 去重、批改功能、命令行参数处理以及单元测试。
- 侯剑锋主要负责博文撰写与项目材料整理,包括需求和设计过程说明、PSP 表格、性能数据整理、测试报告以及项目总结。
- 两人共同完成需求核对、边界条件讨论、运行结果检查和最终材料复审。
张吉安的结对感受:
这次结对让我认识到,完成代码并不等于完成整个软件工程任务。编写程序时我更关注算法和测试是否正确,而侯剑锋会从读者和答辩者的角度追问“为什么这样设计”“数据能否证明程序正确”,这促使我把原本只存在于代码中的思路讲清楚。尤其是在整理去重规则、除法限制和性能测试结果时,文档视角帮助我重新检查了实现是否真正对应题目原文。两人的分工比较明确,但并不是简单地各做一半,而是代码和博文互相验证,这一点对最终质量帮助很大。
侯剑锋的结对感受:
在撰写博文的过程中,我需要先真正理解程序结构,才能准确解释 Fraction、表达式树、canonical form 和 HashSet 去重。张吉安在代码中划分的模块比较清晰,测试也覆盖了许多容易遗漏的边界情况,使我整理设计过程和测试依据时有充分材料。通过这次合作,我体会到技术文档不能只是罗列代码,而要说明需求、设计决策、验证方法和实际数据之间的联系。今后我希望在编码阶段更早参与讨论和测试,这样能减少项目结束后集中整理材料的压力。
彼此的闪光点与建议:
- 侯剑锋的闪光点是信息整理能力强,能够把分散的需求、代码结构、测试结果和性能数据组织成清晰的博文,并能从读者角度发现说明不充分的地方。建议今后在项目开始时就同步建立博文框架和 PSP 记录,而不是等编码基本完成后再集中补充,这样实际用时和设计演变会记录得更准确。
- 张吉安的闪光点是编码思路严谨,对减法、除法、括号和去重等关键约束都设计了对应测试,而且遇到边界问题时愿意回到需求原文分析。建议今后在实现复杂模块时及时记录关键决策和性能数据,并增加阶段性的代码说明,这会让搭档更快理解代码,也能降低后期编写文档和答辩准备的成本。
这次项目最大的收获是:不能只让程序“跑起来”,还要把数学规则转化成可验证的不变量,再用测试、性能数据和文档证明实现符合要求。结对的意义也不只是平分代码,更是让设计假设尽早接受另一位开发者的检查。
八、构建与复现
mvn "-Dmaven.repo.local=$PWD\.m2repo" clean test
java "-Dfile.encoding=UTF-8" -cp target\classes com.eso.arithmetic.Main -n 10 -r 10
java "-Dfile.encoding=UTF-8" -cp target\classes com.eso.arithmetic.Main -e Exercises.txt -a Answers.txt

浙公网安备 33010602011771号