第二次作业——小学四则运算题目生成器
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15694 |
| 这个作业的目标 | 两人结对合作实现一个自动生成小学四则运算题目的应用软件 |
一、项目成员与项目地址
- 吴东鹏:3124004141
- 姚祥捷:3124004147
- 项目地址:https://github.com/south-symphony/3124004141-Four-basic-arithmetic-problems
二、项目简介
项目基于 Java 语言开发,采用 Swing 构建图形化操作界面,完全兼容 JDK 8 及以上运行环境。
程序同时提供命令行调用与图形化界面两种使用模式
- 图形界面模式(默认):双击 Jar 包启动,可视化设置题目数量、数值范围,一键生成题目与答案,支持直接在界面内预览题目、答案和批改结果;
- 命令行兼容模式:保留原生命令行参数,支持 -n/-r 生成题目、-e/-a 批改作业,可无缝对接脚本批量调用。
项目核心包含题目生成与作业批改两大功能模块:支持自然数、真分数、带分数的加减乘除混合运算,可自定义题目数量与数值范围;严格遵循减法运算结果非负、除法运算结果为真分数、单题运算符不超过 3 个的规则;同时基于加法与乘法交换律实现题目自动去重,避免生成重复习题。内置的批改模块可独立解析表达式,自动比对答案并输出对错统计与对应题号。

三、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | ||
| · Estimate | · 估计这个任务需要多少时间 | 20 | 20 |
| Development | 开发 | ||
| · Analysis | · 需求分析 (包括学习新技术) | 35 | 40 |
| · Design Spec | · 生成设计文档 | 10 | 10 |
| · Design Review | · 设计复审 | 20 | 25 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 25 | 30 |
| · Design | · 具体设计 | 50 | 65 |
| · Coding | · 具体编码 | 100 | 120 |
| · Code Review | · 代码复审 | 30 | 25 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 40 | 40 |
| Reporting | 报告 | ||
| · Test Repor | · 测试报告 | 35 | 40 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 20 |
| 合计 | 395 | 445 |
四、使用说明
程序提供图形界面与命令行两种运行方式,可根据使用场景灵活选择。所有生成的文件均默认输出在程序运行的当前目录下。
4.1 图形界面模式
无需记忆参数,可视化操作。
(1)启动程序
双击打包好的 Four basic arithmetic problems.jar 即可启动图形界面。程序主界面分为三个功能区:左侧生成题目区、右侧批改作业区、底部日志与文件预览区。
(2)生成题目
- 在左侧「生成题目」面板中,输入题目数量(对应命令行 ‘-n’ 参数)和数值范围(对应命令行 ‘-r’ 参数);
- 点击「生成题目」按钮,底部日志区会实时显示进度,提示 “生成完成” 即结束;
- 生成成功后,当前目录下会自动生成 Exercises.txt 和 Answers.txt ;
- 点击「查看题目」「查看答案」按钮,可直接在底部预览区查看完整文件内容,自动添加行号方便对照。
![image]()
(3)批改作业
- 在右侧「批改作业」面板中,点击「浏览...」按钮,分别选择待批改的题目文件和学生答案文件;
- 点击「开始批改」按钮,程序自动逐题计算并比对答案;
- 批改完成后生成 Grade.txt,点击「查看 Grade 结果」即可查看对错数量与对应题号。
![image]()
(4)注意事项
- 数值范围为必填项,且必须为正整数,参数非法时会弹出错误提示,不生成无效文件;
- 生成的题目默认满足全部运算约束:单题运算符不超过 3 个、减法无负数、除法结果为真分数、加法乘法自动去重;
- 文件选择器默认定位到程序运行目录,也可手动选择其他位置的题目与答案文件。
4.2 命令行模式
支持参数化调用,适合脚本批量处理、自动化生成场景。
(1)生成题目模式
命令格式:
java -jar ArithmeticGenerator.jar -n 题目数量 -r 数值范围
参数说明:
- -n:指定生成题目的个数,可选参数,默认值为 10;
- -r:指定题目中数值(自然数、真分数分母)的取值范围,必填参数,缺省则程序报错并输出帮助信息。
示例:生成 10 道数值范围在 10 以内的题目
java -jar ArithmeticGenerator.jar -n 10 -r 10
执行完成后,在当前目录生成 Exercises.txt 与 Answers.txt。
(2)作业批改模式
命令格式:
java -jar ArithmeticGenerator.jar -e 题目文件路径 -a 答案文件路径
参数说明:
- -e:指定待批改的题目文件路径;
- -a:指定学生答案文件路径。
示例:批改当前目录下的题目与答案
java -jar ArithmeticGenerator.jar -e Exercises.txt -a Answers.txt
执行完成后,在当前目录生成 Grade.txt 批改结果文件。
4.3 输出文件格式说明
- Exercises.txt(题目文件)
每行一道题目,运算符与等号前后均带空格,支持括号、真分数、带分数,示例:
1、3 + (2 × 1/2) =
2、5 - 2'1/4 =
3、1/2 + 1/3 × 4 =
- Answers.txt(答案文件)
每行对应一道题的答案,自动约分为最简形式,大于 1 的分数以带分数输出,示例:
1、4
2、2'3/4
3、2'1/6
- Grade.txt(批改结果文件)
分别统计正确与错误的题数及对应题号,格式如下:
Correct: 3 (1, 3, 5)
Wrong: 2 (2, 4)
五、设计实现过程
1. 整体架构设计
采用分层设计,核心运算逻辑与界面完全解耦,命令行与图形界面共用同一套核心算法。
| 层级 | 模块 | 职责 |
|---|---|---|
| 核心层 | Fraction | 分数数据结构与精确四则运算 |
| 核心层 | ExpressionGenerator | 递归生成表达式,处理约束与括号 |
| 业务层 | ProblemGenerator | 批量生成、去重、文件输出 |
| 业务层 | AnswerGrader | 表达式解析、作业批改、结果统计 |
| 界面处 | MainFrame | Swing 图形主界面,交互与事件处理 |
| 入口层 | AppMain | 启动入口,模式分发 |
2. 核心算法设计:表达式树递归生成
我们没有采用直接拼接字符串的方式,而是通过递归生成表达式节点(ExprNode),每个节点包含表达式字符串、运算结果值、根运算符三类信息。所有计算、校验、格式化、去重都基于节点结构完成,规则只维护一份,全程一致。
该设计的优势:
- 计算与字符串输出同源,从根源避免 “答案与表达式运算结果不一致”;
- 递归校验所有子表达式约束,而非仅检查最终结果;
- 规范化去重基于树结构,精准匹配交换律与结合性规则。
3. 精确分数运算
所有数值统一用自定义 Fraction 类表示,内部以分子、分母长整型存储,构造时自动约分、统一分母符号。加减乘除全部基于分数通分法则实现,大小比较通过交叉相乘实现,全程不转换浮点数,无精度损失。输出时自动识别自然数、真分数、带分数三种格式。
4. 等价题目去重机制
采用递归规范化键判重:
- 对于 + 和 × 节点,递归生成左右子树的规范化键后,按字典序交换左右顺序;
- 对于 - 和 ÷ 节点,严格保持左右顺序不变;
- 将规范化后的字符串作为唯一键存入 HashSet,新题目生成前先查询集合,命中则丢弃重生成。
该机制保证 (1 + 2) + 3 与 3 + (2 + 1) 判为重复,而 (1 + 2) + 3 与 (3 + 2) + 1 因结合结构不同不判重,完全符合题目要求。
5. 图形界面设计
界面采用左右分栏 + 底部日志的经典布局,整体遵循 Windows 原生风格,操作直观。
(1)界面分区
- 左侧:生成题目面板
- 输入框:题目数量、数值范围
- 功能按钮:生成题目、查看题目、查看答案
- 右侧:批改作业面板
- 文件选择:题目文件、答案文件,支持浏览按钮选择
- 功能按钮:开始批改、查看 Grade 结果
- 底部:运行日志 / 文件预览区
- 实时显示操作状态、错误提示
- 展示读取的文件内容,题目与答案自动带行号
(2)交互设计
- 所有输入均做合法性校验,参数错误弹出友好提示,不崩溃;
- 文件选择器默认定位到程序运行目录,减少操作步骤;
- 长时操作(如万题生成)界面不卡死,日志实时滚动更新;
- 文件不存在时弹出提示,而非静默失败。
(3)模式兼容
程序入口 AppMain 自动判断运行方式:
- 无命令行参数 → 启动图形界面;
- 带 -n/-r/-e/-a 参数 → 执行命令行模式,静默输出文件。
六、关键代码说明
1. 分数运算核心
public Fraction add(Fraction other) {
long num = this.numerator * other.denominator
+ other.numerator * this.denominator;
long den = this.denominator * other.denominator;
return new Fraction(num, den);
}
private void simplify() {
if (numerator == 0) {
denominator = 1;
return;
}
long gcd = BigInteger.valueOf(a).gcd(BigInteger.valueOf(b)).longValue();
numerator /= gcd;
denominator /= gcd;
}
所有运算均通过分子分母整数运算完成,构造即自动约分,保证全程序分数表示唯一,为后续等值比较和去重奠定基础。
2. 括号正确性判断
private boolean needParentheses(char outerOp, char innerOp, boolean isLeft) {
if (innerOp == ' ') return false;
int outerPrio = getPriority(outerOp);
int innerPrio = getPriority(innerOp);
if (innerPrio < outerPrio) return true;
// 同优先级:左结合运算符的右子式必须加括号
if (innerPrio == outerPrio && !isLeft
&& (outerOp == '-' || outerOp == '÷')) {
return true;
}
return false;
}
严格遵循四则运算优先级与左结合规则,嵌套表达式按需自动添加括号,确保输出的字符串表达式与递归计算顺序完全一致,解决了早期版本答案与表达式不符的问题。
3. 图形界面事件处理
private void generateProblems() {
try {
int count = Integer.parseInt(countField.getText().trim());
int range = Integer.parseInt(rangeField.getText().trim());
if (count <= 0 || range <= 0) {
JOptionPane.showMessageDialog(this, "参数必须大于0", "参数错误", JOptionPane.ERROR_MESSAGE);
return;
}
log("开始生成 " + count + " 道题目...");
ProblemGenerator generator = new ProblemGenerator(count, range);
generator.generate();
log("生成完成!题目已保存至 Exercises.txt");
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(this, "请输入有效的数字", "格式错误", JOptionPane.ERROR_MESSAGE);
} catch (Exception e) {
log("生成失败: " + e.getMessage());
JOptionPane.showMessageDialog(this, "生成失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
界面层封装参数校验与异常处理,将用户输入转换为业务层调用,统一捕获异常并给出可视化反馈,保证界面操作的友好性。
4. 通用文件预览方法
private void viewTextFile(String fileName, String title) {
File file = new File(fileName);
if (!file.exists()) {
JOptionPane.showMessageDialog(this, fileName + " 文件不存在", "提示", JOptionPane.INFORMATION_MESSAGE);
return;
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
log("========== " + title + " ==========");
String line;
int lineNum = 1;
while ((line = reader.readLine()) != null) {
log(lineNum + ". " + line);
lineNum++;
}
} catch (IOException e) {
log("读取失败: " + e.getMessage());
}
}
抽取通用的文件读取与显示逻辑,三个文件预览功能复用同一套代码,同时自动添加行号,方便题目与答案对照查看。
七、效能分析
本次性能分析使用 JProfiler 16 剖析工具,采用 CPU 采样模式,以命令行模式运行程序,程序参数为 -n 10000 -r 10,批量生成10000条题目,保证数据准确反映核心算法的真实性能。
7.1 方法级热点分析

该视图按方法总耗时从高到低排序,直观呈现程序的性能瓶颈分布:
- 耗时最高的方法为 main.ExpressionGenerator.generateExpression,总耗时 271 ms,占程序总运行时间的 78%,是程序的核心性能消耗点,对应表达式递归生成、约束校验、括号拼接的全部核心逻辑。
- 第二梯队为分数运算相关方法:Fraction.gcd(最大公约数计算)、Fraction.simplify(自动约分)、Fraction.
(分数对象构造),合计占比约 21%,是精确分数运算的固有开销。 - 文件写入、题目去重、参数解析等辅助逻辑占比均低于 3%,IO 开销极低,程序属于典型的计算密集型。
7.2 类维度耗时分布

按类维度聚合耗时后,可以更清晰地看到模块级的开销分布:
- ExpressionGenerator 类:总耗时占比 78%,承载表达式生成、运算符分配、括号判断全部逻辑。
- Fraction 类:总耗时占比 48%,封装所有分数四则运算、约分、格式化底层能力。
注:类耗时统计的是总调用时间,存在嵌套调用关系,因此占比之和超过 100%。
整体开销高度集中在核心运算类,模块职责与耗时分布完全匹配,架构设计合理,没有明显的不合理开销。
7.3 调用链路分析

调用树视图完整呈现了从程序入口到底层运算的调用层级与每层耗时占比,完整链路为:
AppMain.main (100%)
└─ ProblemGenerator.generate (98%)
└─ ExpressionGenerator.generateExpression (78%)
└─ Fraction 系列运算方法 (48%)
- 整条调用链损耗极低,入口层与业务层几乎没有额外开销,性能全部集中在核心计算逻辑。
- generateExpression 共调用 12637 次,对应每道题的递归生成过程,调用次数与生成规模匹配。
- 分数相关方法总调用量超 11 万次,是递归生成过程中最频繁的底层操作。
7.4 性能结论
程序整体运行高效,单轮批量生成总耗时仅 346 ms,性能瓶颈集中在表达式递归生成与精确分数约分运算,符合计算密集型程序的特征。文件 IO、去重哈希等辅助操作开销极低,万道级题目生成可稳定在秒级完成,完全满足日常出题与批量作业的性能需求。
八、测试运行
1. 测试用例设计
我们设计了覆盖功能、边界、异常、界面、性能五类共 13 项核心测试用例:
| 编号 | 测试场景 | 输入/操作 | 验证点 | 结果 |
| ---- | ---- | ---- | | |
| 1 | 基础自然数生成 | 界面输入n=5,r=10 | 加减乘除正确,减法无复数 | 通过 |
| 2 | 真分数运算生成 | 界面输入 n=5, r=10 | 分数运算精确,格式符合规范 | 通过 |
| 3 | 带分数运算 | 界面输入 n=3, r=10 | 带分数输入输出格式一致 | 通过 |
| 4 | 多运算符与括号 | 界面输入 n=5, r=20 | 运算符≤3 个,括号位置正确 | 通过 |
| 5 | 极小范围边界 | 界面输入 r=1 | 程序稳定运行,无参数异常 | 通过 |
| 6 | 除法结果约束 | 随机抽样 20 题 | 所有除法结果均为真分数 | 通过 |
| 7 | 题目去重规则 | 界面输入 n=100, r=5 | 交换律等价题不重复 | 通过 |
| 8 | 作业批改功能 | 选择自带文件批改 | 对错数量与题号统计准确 | 通过 |
| 9 | 文件预览功能 | 点击查看题目 / 答案 / 结果 | 正确显示文件内容,带行号 | 通过 |
| 10 | 万题大规模生成 | 界面输入 n=10000, r=20 | 生成成功,无重复,界面不卡死 | 通过 |
| 11 | 参数非法输入 | 输入负数、非数字 | 弹出错误提示,不崩溃 | 通过 |
| 12 | 文件不存在 | 批改错误文件名 | 明确提示,不生成异常文件 | 通过 |
| 13 | 命令行兼容模式 | java -jar xxx.jar -n 10 -r 10 | 命令行模式正常工作 | 通过 |
2. 正确性保障说明
我们通过多重交叉验证确保程序正确:
- 精确分数运算:全程整数运算,无浮点精度损失,等值判断绝对准确;
- 全树约束校验:每道题递归校验所有子表达式,而非仅检查最终结果;
- 生成 - 批改闭环验证:将生成的题目与答案用批改模块回读重算,结果完全一致,同时验证了生成、格式化、解析、批改四个环节;
- 界面 - 核心解耦验证:图形界面与命令行调用同一套核心逻辑,界面操作结果与命令行完全一致。
九、项目小结与结对感受
9.1 项目总结
这次结对项目算是完整走了一遍开发全流程,从最开始捋需求、搭架构,到写代码、改 bug,再到最后用 JProfiler 跑性能分析,自己设计测试用例并完成测试,整套流程都走通了。最后不仅把作业要求的命令行生成、批改功能都做出来了,还额外加了图形界面和文件预览,整体效果比预期的好。
开发路上也踩了不少坑,攒了点实打实的经验:
- 规则一定要先讲死、写下来。最开始我俩对 “啥样算重复题”“分数怎么输出” 这种细节没统一口径,各写各的,做到一半才发现生成的格式和批改的格式对不上,返工了一波。后来先把所有规则都整理成文字再开工,后面对接就顺多了。
- 别图省事直接拼字符串。最开始图快直接用字符串拼表达式,结果括号总加不对,经常出现 “答案算出来是对的,但表达式按优先级算出来不一样” 的离谱 bug。后来改成用表达式树递归生成,计算、加括号、去重都用同一套结构,规则只写一遍,这类 bug 直接少了一大半。
- 界面和核心分开写真的香。从一开始就把生成、批改的核心逻辑和界面分开做,后面加图形界面的时候几乎没动核心代码,直接调用就行,加功能特别省心。
9.2 结对开发感受
这次项目我两按模块分工,我主要负责代码开发、算法设计和性能分析方面,队友主要负责测试方面,但是又不是完全负责,而是相互交流着来,每当完成了一部分,我们会一起用测试样例来检查输出。俩人一起做和自己单写代码差别真的挺大的,每件事都能互相 “对一遍”。从需求怎么理解、模块怎么分,到测试用例够不够,都是俩人商量着来,很多自己一个人容易忽略的边边角角,比如数值范围特别小的时候会不会崩、除法结果格式对不对,聊着聊着就想到了。
印象最深的就是查那个括号 bug,我盯着递归逻辑看了半天都没发现哪错了,队友过来问了一句 “输出的字符串和你递归算的顺序真的一样吗”,一下就点醒了,很快就找到是最外层漏加括号的问题。两个人思路不一样,真的能更快跳出死胡同。我们也养成了做完一个小功能就测试一次的习惯,每步都有记录,出问题了也能很快往回找。
9.3 彼此闪光点
姚祥捷的闪光点:
姚祥捷主要负责测试方面,他控得特别稳。我每写完一个功能模块,他都能快速整理好对应的测试用例,陪着我一起逐条核对输出,不光测正常场景,各种边界犄角旮旯都能覆盖到,像极小数值范围的参数异常、表达式括号的隐藏 bug,都是我们一起过用例的时候揪出来的,全程不是各干各的,每推进一段就同步碰一次,沟通很顺畅,没出现过做完了才发现口径对不上的情况。
吴东鹏的闪光点:
吴东鹏主要负责代码开发、算法设计和性能分析方面,他核心代码和算法设计这块很扎实,表达式递归生成、精确分数运算这些核心逻辑写得清晰利落,需求吃得透,出活速度快,基础的逻辑 bug 很少。而且不是写完就甩手,每完成一部分都会主动拉着一起过逻辑、验输出,听得进去测试端的反馈,调整修改都很及时,配合起来很顺畅。


浙公网安备 33010602011771号