(一)
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13479 |
这个作业的目标 | <结对编程项目,实现一个自动生成小学四则运算题目的命令行程序> |
github仓库(master分支) | https://github.com/hypocodeemia/hypocodeemia |
github仓库具体地址 | https://github.com/hypocodeemia/hypocodeemia/tree/master/math_exercise |
成员姓名 | 成员学号 |
---|---|
林嘉俊 | 3123004446 |
王梓涵 | 3123002706 |
(二)PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 45 |
Estimate | 估计这个任务需要多少时间 | 20 | 30 |
Development | 开发 | 650 | 700 |
Analysis | 需求分析 (包括学习新技术) | 60 | 80 |
Design Spec | 生成设计文档 | 40 | 50 |
Design Review | 设计复审 | 20 | 25 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 15 | 20 |
Design | 具体设计 | 60 | 70 |
Coding | 具体编码 | 180 | 200 |
Code Review | 代码复审 | 30 | 35 |
Test | 测试(自我测试,修改代码,提交修改) | 75 | 90 |
Reporting | 报告 | 90 | 110 |
Test Repor | 测试报告 | 30 | 40 |
Size Measurement | 计算工作量 | 15 | 20 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 45 | 50 |
合计 | 880 | 995 |
(三)效能分析
操作类型 | 总耗时(total time/cpu time) |
---|---|
生成10000道题 | 180ms/180ms |
![]() |
|
---------- | --------------------------------------------------------------------- |
检查答案对错 | 84ms/84ms |
![]() |
①生成10000道题
函数 | 耗时占比 | parent |
---|---|---|
ExerciseServiceImpl.generateExercises | 92.85% | 总 |
CommandLineController.writeExercisesToFile | 7.14% | 总 |
---- | ---- | ---- |
ExerciseServiceImpl.generateAndValidateExercise | 100% | ExerciseServiceImpl.generateExercises |
---- | ---- | ---- |
ExerciseServiceImpl.generateSingleExercise | 76.92% | ExerciseServiceImpl.generateAndValidateExercise |
ValidationServiceImpl.isExpressionNonNegative | 23.07% | ExerciseServiceImpl.generateAndValidateExercise |
---- | ---- | ---- |
ExerciseServiceImpl.generateFourOperandExercise | 40% | ExerciseServiceImpl.generateSingleExercise |
ExerciseServiceImpl.generateThreeOperandExercise | 40% | ExerciseServiceImpl.generateSingleExercise |
ExerciseServiceImpl.generateTwoOperandExercise | 20% | ExerciseServiceImpl.generateSingleExercise |
关键发现:
- 分数除法运算是最大的性能瓶颈
- 四操作数题目生成比两操作数题目生成更耗时
- 文件写入操作占据时间比例较小
②检查10000题答案对错
函数 | 耗时占比 | parent |
---|---|---|
ExerciseServiceImpl.checkAnswers | 71.42% | 总 |
CommandLineController.readLinesFromFile | 14.28% | 总 |
CommandLineController.writeGradeToFile | 14.28% | 总 |
---- | ---- | ---- |
ExpressionEvaluator.evaluate | 60% | ExerciseServiceImpl.checkAnswers |
FractionUtil.parseFraction | 20% | ExerciseServiceImpl.checkAnswers |
ExpressionEvaluator.processOperation | 33.33% | ExpressionEvaluator.evaluate |
关键发现: |
- 计算数学表达式的结果是答案检查过程的主要性能瓶颈
- 表达式求值占据了检查逻辑的大部分时间
- 分数解析也有一定的性能开销
③改进思路
整体来看,程序性能表现合格,能够处理大规模题目生成和检查任务。
改进思路:
- 优化核心算法:重点改进分数运算和表达式求值算法
- 减少IO操作:优化文件读写性能
- 缓存优化:对常用计算结果进行缓存
- 并行处理:对可并行操作进行优化
具体改进措施:
-
分数运算优化
优化了Fraction.divide方法的实现,减少中间对象的创建
改进了最大公约数计算算法,使用更高效的欧几里得算法实现 -
表达式求值优化
优化了ExpressionEvaluator的栈操作,减少不必要的对象创建
改进了运算符优先级处理逻辑 -
文件IO优化
使用更大的缓冲区进行文件读写
优化了文件编码处理逻辑
(四)设计实现过程
(1)项目结构
点击查看代码
math_exercise/
├── src/
│ └── main/
│ │ └── java/
│ │ └── com/linjiajun/math_exercise/
│ │ ├── MathExerciseApplication.java # Spring Boot主应用类
│ │ ├── controller/
│ │ │ └── CommandLineController.java # 命令行控制器
│ │ ├── service/
│ │ │ ├── ExerciseService.java # 题目生成服务接口
│ │ │ ├── ValidationService.java # 验证服务接口
│ │ │ ├── impl/
│ │ │ ├── ExerciseServiceImpl.java # 题目生成服务实现类
│ │ │ └── ValidationServiceImpl.java # 验证服务实现类
│ │ │
│ │ │
│ │ ├── bean/
│ │ │ ├── Exercise.java # 题目模型类
│ │ │ └── Fraction.java # 分数模型类
│ │ └── util/
│ │ ├── ExpressionEvaluator.java # 表达式求值器
│ │ └── FractionUtil.java # 分数工具类
│ └── test/
│ └── java/
│ └── com/linjiajun/math_exercise/
│ ├── MathExerciseApplicationTests.java # 测试类
├── target/
│ └── math_exercise-0.0.1-SNAPSHOT.jar # 打包生成的可执行JAR
├── Exercises.txt # 生成的题目文件
├── Answers.txt # 生成的答案文件
├── Grade.txt # 答案检查统计文件
└── pom.xml # Maven项目配置文件
(2)关键流程图
①题目生成
②检查答案对错
(3)简略调用关系
点击查看代码
用户命令行输入↓
CommandLineController↓
ExerciseServiceImpl ←→ ValidationServiceImpl↓
ExpressionEvaluator ←→ FractionUtil↓
Fraction + Exercise
(五)代码说明
(1)关键需求实现
①生成的题目中计算过程不能产生负数
点击查看代码
/*** 验证整个表达式是否满足非负要求 - 核心验证逻辑* 确保所有中间步骤和最终结果都不为负数*/
public boolean isExpressionNonNegative(String expression) {try {String cleanExpression = expression.replace("=", "").replace(" ", "");return evaluateAllSteps(cleanExpression);} catch (Exception e) {log.debug("表达式验证失败: {}", expression, e);return false;}
}/*** 评估表达式的所有计算步骤,确保中间步骤和最终结果都非负*/
private boolean evaluateAllSteps(String expression) {Stack<Fraction> numbers = new Stack<>();Stack<String> operators = new Stack<>();int i = 0;while (i < expression.length()) {char c = expression.charAt(i);// 解析逻辑...if (isOperator(c)) {while (!operators.isEmpty() && precedence(operators.peek()) >= precedence(String.valueOf(c))) {// 关键:每一步运算都检查结果非负if (!processOperationWithCheck(numbers, operators)) {return false;}}operators.push(String.valueOf(c));i++;}// ... 其他解析逻辑}// 处理剩余运算符while (!operators.isEmpty()) {if (!processOperationWithCheck(numbers, operators)) {return false;}}// 最终结果也应该是非负的Fraction finalResult = numbers.pop();return isNonNegative(finalResult);
}/*** 带检查的运算处理 - 核心减法验证*/
private boolean processOperationWithCheck(Stack<Fraction> numbers, Stack<String> operators) {String op = operators.pop();Fraction right = numbers.pop();Fraction left = numbers.pop();Fraction result;switch (op) {case "+":result = left.add(right);break;case "-":result = left.subtract(right);// 关键:检查减法结果是否为负if (!isNonNegative(result)) {return false; // 发现负数,立即返回失败}break;case "×":result = left.multiply(right);break;case "÷":result = left.divide(right);break;default:throw new IllegalArgumentException("未知运算符: " + op);}numbers.push(result);return true;
}
实现思路:
- 逐步骤验证:在表达式求值的每一步都检查结果是否非负
- 立即失败:一旦发现负数结果立即终止验证
- 最终确认:确保最终答案也非负
- 全面覆盖:处理括号、运算符优先级等复杂情况
②生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数
点击查看代码
/*** 验证除法运算是否合法 - 确保结果为真分数*/
@Override
public boolean isValidDivision(Fraction left, Fraction right) {// 1. 检查除数不为零if (right.getNumerator() == 0 && right.getWhole() == 0) {log.debug("除数为零");return false;}// 2. 计算除法结果Fraction result = left.divide(right);// 3. 严格验证:结果必须是真分数boolean isValid = result.isProperFraction();if (!isValid) {log.debug("除法结果不是真分数: {} ÷ {} = {}", left, right, result);}return isValid;
}/*** 真分数判断标准 - 严格定义*/
public boolean isProperFraction() {// 整数部分必须为0if (whole != 0) {return false;}// 分子绝对值必须小于分母if (Math.abs(numerator) >= denominator) {return false;}// 分母不能为1(否则就是整数)if (denominator == 1) {return false;}// 分子不能为0(否则就是0)if (numerator == 0) {return false;}return true;
}
- 定义:真分数必须满足四个条件:
- 整数部分为0
- 分子绝对值小于分母
- 分母不为1
- 分子不为0
- 计算验证:先计算结果再验证,确保准确性
- 立即拒绝:不满足条件立即拒绝该题目
③每道题目中出现的运算符个数不超过3个
点击查看代码
/*** 生成单个题目,根据运算符数量选择不同的生成策略* @param range 数值范围* @param index 题目编号* @return 生成的题目,如果生成失败返回null*/private Exercise generateSingleExercise(int range, int index) {int operatorCount = random.nextInt(3) + 1;try {switch (operatorCount) {case 1:return generateTwoOperandExercise(range, index);case 2:return generateThreeOperandExercise(range, index);case 3:return generateFourOperandExercise(range, index);default:return null;}} catch (Exception e) {log.debug("生成题目失败: {}", e.getMessage());return null;}}
④程序一次运行生成的题目不能重复
点击查看代码
/*** 获取规范化后的表达式* 用于题目去重比较,移除空格并统一运算符表示* @return 规范化后的表达式字符串*/public String getNormalizedExpression() {return expression.replace(" ", "").replace("×", "*").replace("÷", "/");}@Overridepublic List<Exercise> generateExercises(int count, int range) {Set<Exercise> exercises = new HashSet<>();int attempts = 0;// 增加最大尝试次数int maxAttempts = count * 20;while (exercises.size() < count && attempts < maxAttempts) {Exercise exercise = generateAndValidateExercise(range, exercises.size() + 1);if (exercise != null && !exercises.contains(exercise)) {exercises.add(exercise);}attempts++;}if (exercises.size() < count) {log.warn("只成功生成了 {} 道题目,目标数量为 {}", exercises.size(), count);}return new ArrayList<>(exercises);}
(2)关键代码
①表达式求值核心算法
点击查看代码
/*** 计算数学表达式的结果* 使用操作数栈和运算符栈,按照运算符优先级进行计算* @param expression 数学表达式字符串* @return 计算结果(分数形式)* @throws IllegalArgumentException 如果表达式格式错误或包含不支持的操作*/public static Fraction evaluate(String expression) {// 移除空格和等号expression = expression.replace(" ", "").replace("=", "");Stack<Fraction> numbers = new Stack<>();Stack<String> operators = new Stack<>();int i = 0;while (i < expression.length()) {char c = expression.charAt(i);if (c == '(') {operators.push("(");i++;} else if (c == ')') {while (!"(".equals(operators.peek())) {processOperation(numbers, operators);}// 移除 "("operators.pop();i++;} else if (isOperator(c)) {while (!operators.isEmpty() && precedence(operators.peek()) >= precedence(String.valueOf(c))) {processOperation(numbers, operators);}operators.push(String.valueOf(c));i++;} else {// 解析数字或分数StringBuilder sb = new StringBuilder();while (i < expression.length() &&(Character.isDigit(expression.charAt(i)) ||expression.charAt(i) == '/' ||expression.charAt(i) == '\'')) {sb.append(expression.charAt(i));i++;}numbers.push(parseFraction(sb.toString()));}}while (!operators.isEmpty()) {processOperation(numbers, operators);}return numbers.pop();}
设计思路:
- 采用双栈算法
- 操作数栈存储分数对象,运算符栈存储运算符和括号
- 正确处理运算符优先级:乘除优先于加减
- 支持括号改变运算顺序
- 时间复杂度 O(n),空间复杂度 O(n)
②题目生成与验证
点击查看代码
/*** 生成单个题目并确保符合所有约束条件*/
private Exercise generateAndValidateExercise(int range, int index) {int attempts = 0;while (attempts < 50) {Exercise exercise = generateSingleExercise(range, index);if (exercise != null && validationService.isExpressionNonNegative(exercise.getExpression())) {return exercise;}attempts++;}return null;
}/*** 验证运算的合法性 - 多层验证确保题目质量*/
private boolean isOperationValid(Fraction left, Fraction right, String operator, String fullExpression) {// 第一层:基础运算验证boolean basicValid;switch (operator) {case "-":basicValid = validationService.isValidSubtraction(left, right);break;case "÷":basicValid = validationService.isValidDivision(left, right);break;default:basicValid = true;}if (!basicValid) {return false;}// 第二层:完整表达式验证return validationService.isExpressionNonNegative(fullExpression);
}
设计思路:
- 采用多次尝试机制,确保生成符合条件的题目
- 多层验证链:基础验证 → 表达式验证 → 范围验证 → 结果验证
- 确保减法结果非负、除法结果为真分数
- 验证整个表达式的所有中间步骤
③分数运算
点击查看代码
/*** 分数规范化 - 确保分数始终处于最简形式*/
private void normalize() {if (denominator == 0) {throw new IllegalArgumentException("分母不能为零");}// 处理分母为负的情况if (denominator < 0) {numerator = -numerator;denominator = -denominator;}// 约分:求分子分母的最大公约数int gcd = gcd(Math.abs(numerator), denominator);numerator /= gcd;denominator /= gcd;// 处理假分数:转换为带分数if (Math.abs(numerator) >= denominator) {whole += numerator / denominator;numerator = Math.abs(numerator) % denominator;}// 如果分子为0,重置整数部分if (numerator == 0) {whole = 0;}
}/*** 分数除法运算*/
public Fraction divide(Fraction other) {// 转换为假分数进行计算int num1 = this.toImproperFraction().numerator;int den1 = this.toImproperFraction().denominator;int num2 = other.toImproperFraction().numerator;int den2 = other.toImproperFraction().denominator;// 分数除法:乘以倒数int newNum = num1 * den2;int newDen = den1 * num2;return new Fraction(newNum, newDen);
}
设计思路:
- 规范化确保分数处于最简形式
- 支持真分数、带分数、自然数的统一处理
- 使用欧几里得算法计算最大公约数进行约分
- 四则运算都基于假分数进行计算,避免复杂逻辑
(六)测试运行
测试方法均在MathExerciseApplicationTests类中
测试编号 | 测试内容 | 结果 |
---|---|---|
1-testGenerateExercisesDirectly | 调用生成题目方法 | 正常运行,得到对应文件 |
2-testCheckAnswersDirectly | 调用检查答案方法 | 正常运行,得到对应文件 |
3-testParamLost | 测试缺失参数-r的情况 | 不会得到题目和答案文件,系统会打印程序使用帮助信息 |
4-testIllegalParam | 测试不合规参数的情况(-n的参数为负数) | 会提示"题目数量必须大于0,数值范围必须大于1" |
5-testWrongDirectly | 测试文件地址异常 | 会显示文件读取错误:xxx文件 |
6-testNoNegativeResults | 测试负数约束验证 | 正常运行 |
7-testDivisionResultsAreProperFractions | 除法结果真分数验证 | 正常运行 |
8-testOperatorCountLimit | 测试运算符个数限制 | 正常运行 |
9-testExerciseUniqueness | 测试题目去重功能 | 正常运行 |
10-testLargeScalePerformance | 测试大规模性能 | 正常运行 |
通过这些测试用例,我能够:
- 验证功能完整性:所有核心功能都经过测试
- 确保需求符合性:严格验证了所有数学规则约束
- 验证性能表现:确认程序能够高效处理大规模数据
- 检查错误处理:确保程序能够优雅处理各种错误情况
(七)项目小结
项目启示
这个项目不仅是一个技术实践,更是一次完整的软件工程体验。我们深刻体会到:
- 质量源于设计:良好的架构设计是项目成功的基础
- 测试保障质量:完善的测试体系是代码质量的保证
- 协作提升效率:有效的团队协作能产生1+1>2的效果
结对感受
开发者A:林嘉俊
在结对编程过程中,我学会了更好地沟通技术方案,通过代码审查发现了自己忽略的细节问题。合作伙伴对测试的重视让我发现了更多潜在的问题.
对对方的评价:
- 对细节的把握非常到位,发现了多个关键的业务逻辑问题
- 测试用例设计全面,覆盖了各种边界情况
建议:可以更早开始性能测试和相关优化
开发者B:王梓涵
这个项目让我对软件工程的全流程有了更完整的认识,从需求分析、架构设计到测试部署。特别是在验证逻辑设计和异常处理方面收获很大。
与合作伙伴的讨论激发了很多思路,比如表达式规范化去重等。代码复审环节帮助我们发现了很多潜在问题。
对对方的评价:
- 算法实现能力强
- 代码结构清晰,注释完整,便于理解和维护
建议:可以更多进行代码的逻辑测试,异常处理