软件工程第三次作业——结对项目
第三次作业——结对项目
| 这个作业属于那个课程 | 计科23级12班 |
|---|---|
| 这个作业的要求在哪里 | 结对项目 |
| 这个作业的目标 | 与搭子完成项目编码,分析记录过程,共同开发项目 |
1.个人信息 :
| 姓名 | 学号 |
|---|---|
| 凌紫君 | 3223004383 |
| 严展桐 | 3223004388 |
github链接: https://github.com/Yzttt0425/yanzhantong.github/tree/main/第三次软工作业
2. PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 15 |
| · Estimate | · 估计这个任务需要多少时间 | 15 |
| Development | 开发 | 335 |
| · Analysis | · 需求分析 (包括学习新技术) | 30 |
| · Design Spec | · 生成设计文档 | 40 |
| · Design Review | · 设计复审 | 15 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 |
| · Design | · 具体设计 | 60 |
| · Coding | · 具体编码 | 120 |
| · Code Review | · 代码复审 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 |
| Reporting | 报告 | 95 |
| · Test Repor | · 测试报告 | 60 |
| · Size Measurement | · 计算工作量 | 15 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 |
| · 合计 | 425 |
3.效能分析
性能分析图:


可看出,消耗最大的函数为normalizeExpression(),内存占用高。
优化思路:
(1)表达式生成逻辑优化
用逆波兰表达式,中缀转后缀加上栈式计算代替递归调用生成表达式,通过栈结构避免递归,降低调用开销。
(2)重复检测算法优化
生成子表达式时提前校验合法性,减少无效表达式的生成;用结构化哈希代替正则替换进行去重,计算哈希值,减少字符串操作开销。
(3)数据结构优化
将存储已生成表达式的HashSet<String>改为HashSet<Long>存储哈希值,降低内存占用;生成随机数时复用Random,避免频繁创建对象。
(4)计算逻辑优化
在Fraction类simplify方法中,对分数化简时只通过一次GCD(最大公约数)完成约分,减少操作步骤。
4.设计实现过程
开发环境:Eclipse IDE for Enterprise Java and Web Developers(4.35.0)
开发语言:Java
系统总体设计
本系统采用单类多内部类的分层架构设计,核心逻辑封装在MathExerciseGenerator主类中,通过内部类实现职责拆分:
1、入口层(MathExerciseGenerator.main)
负责解析命令行参数,区分 “生成题目” 与 “批改答案” 两种模式,控制程序流程,处理异常并输出帮助信息。
2、功能层(主类 + 内部类)
实现系统核心逻辑,包含 4 个核心组件:
Fraction内部类:处理分数与带分数的表示、计算与格式化。
ExpressionResult内部类:封装生成的表达式与对应结果,便于数据传递。
题目生成模块(主类方法):生成不重复、符合约束的四则运算题。
答案批改模块(主类方法):比对题目与答案,生成批改结果文件。
类与接口设计
(一)MathExerciseGenerator 主类
职责:程序主入口,封装题目生成、答案批改、文件读写的核心逻辑,通过内部类拆分细分功能。
| 主要函数 | 功能 |
|---|---|
main(String[] args) |
解析参数,调用生成 / 批改逻辑,处理异常。 |
printUsage() |
输出命令用法(生成题目:-n <数量> -r <范围>;批改:-e <题目文件> -a <答案文件>)。 |
gradeExercises(String exerciseFile, String answerFile) |
读题与答案,校验数量一致性,逐题比对,生成Grade.txt。 |
readLines(String filename) |
逐行读文件,过滤空行,处理 IO 异常。 |
formatGradeLine(String type, List<Integer> numbers) |
格式化批改结果(如Correct: 2 (1,3))。 |
(二)Fraction 内部类
职责:分数数据模型,实现分数(含带分数)的解析、四则运算、约分与格式化,确保计算精确无误差。
(1)构造方法:全参构造,初始化后自动调用simplify()约分;整数构造,分母默认为 1,分子为 0;普通分数构造,整数部分为 0。
(2)核心方法
| 核心方法 | 功能 |
|---|---|
simplify() |
统一分母符号、约分、转换带分数。 |
gcd(int a, int b) |
用欧几里得算法求最大公约数。 |
| 四则运算(add/subtract/multiply/divide) | 转假分数计算,自动约分。 |
parse(String s) |
解析整数、普通分数、带分数格式。 |
greaterOrEqual(Fraction other) |
比较分数大小,确保减法非负。 |
toString() |
按规则格式化输出(如2'1/3)。 |
(三)ExpressionResult 内部类
职责:数据传输载体,封装 “表达式字符串” 与 “对应分数结果”,避免生成表达式时重复计算。
属性与构造
String expression:四则运算表达式(如(1/2 + 3) × 4)。
Fraction result:表达式对应的计算结果。
public ExpressionResult(String expression, Fraction result):全参构造,直接赋值属性。
(四)题目生成核心方法(主类中)
职责:生成指定数量、指定范围、无重复且符合约束(结果非负、除法结果为真分数)的四则运算题。
| 关键方法 | 功能 |
|---|---|
generateExpression(int rangeLimit, int maxOps) |
生成子表达式,随机选运算符,校验减法非负、除法为真分数,处理括号。 |
generateSubExpression(int rangeLimit, int maxOps) |
递归生成子表达式,maxOps=0时生成单个分数。 |
generateNumber(int rangeLimit) |
50% 概率生成整数,50% 概率生成分数(含带分数)。 |
normalizeExpression(String expr) |
移除括号、排序,避免重复表达式。 |
containsAddOrSubtract(String expr) |
判断表达式含+/-,辅助括号处理。 |
(五)表达式计算核心方法(主类中)
职责:解析四则运算表达式,计算结果(用于生成答案和批改答案时校验)。
关键方法
1、calculateExpression(String expr):预处理表达式,调用栈处理方法计算。
2、evaluateExpressionWithStack(String expr):转后缀表达式并计算。
3、infixToPostfix(String expr):用调度场算法转后缀表达式。
4、evaluatePostfix(List<Object> postfix):栈计算后缀表达式,处理格式异常。
关系图

5.代码说明
(1)分数类Fraction
核心作用:封装分数的表示、运算及格式化,支持整数、纯分数、带分数的处理。
设计思路:将所有分数基于假分数进行运算,自动简化结果结并支持多种格式分数的输入输出。
点击查看代码
static class Fraction {
int integer; // 整数部分(如3'1/2中的3)
int numerator; // 分子(如3'1/2中的1)
int denominator;// 分母(如3'1/2中的2)
// 构造方法:支持整数、纯分数、带分数初始化
public Fraction(int integer, int numerator, int denominator) {
this.integer = integer;
this.numerator = numerator;
this.denominator = denominator;
simplify(); // 初始化后自动简化
}
public Fraction(int integer) { this(integer, 0, 1); } // 整数构造
public Fraction(int numerator, int denominator) { this(0, numerator, denominator); } // 纯分数构造
// 核心方法:分数简化(自动约分、转换带分数、处理负号)
private void simplify() {
if (denominator == 0) throw new ArithmeticException("分母不能为0");
// 确保分母为正数(负号转移到分子)
if (denominator < 0) {
numerator *= -1;
denominator *= -1;
}
// 合并整数部分与分子(转为假分数)
int totalNumerator = integer * denominator + numerator;
integer = 0;
numerator = totalNumerator;
// 零分数处理
if (numerator == 0) {
denominator = 1;
return;
}
// 约分(最大公约数)
int gcd = gcd(Math.abs(numerator), Math.abs(denominator));
numerator /= gcd;
denominator /= gcd;
// 转换为带分数(若分子 >= 分母)
if (Math.abs(numerator) >= denominator) {
integer = numerator / denominator;
numerator = Math.abs(numerator % denominator);
}
}
// 加减乘除运算(基于假分数计算,自动简化结果)
public Fraction add(Fraction other) { ... } // 加法:a/b + c/d = (ad+bc)/bd
public Fraction subtract(Fraction other) { ... } // 减法:a/b - c/d = (ad-bc)/bd
public Fraction multiply(Fraction other) { ... } // 乘法:a/b × c/d = ac/bd
public Fraction divide(Fraction other) { ... } // 除法:a/b ÷ c/d = ad/bc(除数不为0)
// 解析字符串为分数(支持"3'1/2"、"5/6"、"4"格式)
public static Fraction parse(String s) { ... }
// 重写equals和toString(用于答案对比和格式化输出)
@Override
public boolean equals(Object obj) { ... }
@Override
public String toString() { ... }
}
(2)表达生成与计算
核心作用:随机生成不重复的表达式,结合运算符号的优先级解析计算。
设计思路:递归生成表达式,实现去重机制,通过栈处理运算符优先级,按顺序计算结果。
点击查看代码
// 生成单个表达式(递归构造,支持多层运算)
private static ExpressionResult generateExpression(int rangeLimit, int maxOps) {
// 随机生成运算符数量(1~maxOps)
int numOps = random.nextInt(maxOps) + 1;
// 拆分左右子表达式的运算符数量
int opCount = random.nextInt(numOps) + 1;
int leftOps = opCount - 1; // 左子表达式运算符数
int rightOps = numOps - opCount; // 右子表达式运算符数
// 递归生成左右子表达式
ExpressionResult left = generateSubExpression(rangeLimit, leftOps);
ExpressionResult right = generateSubExpression(rangeLimit, rightOps);
// 随机选择运算符
String[] ops = {"+", "-", "×", "÷"};
String op = ops[random.nextInt(ops.length)];
// 特殊规则校验(避免负数结果、除法分母为0等)
if (op.equals("-") && !left.result.greaterOrEqual(right.result)) {
return generateExpression(rangeLimit, maxOps); // 减法确保被减数 >= 减数
}
if (op.equals("÷")) {
// 除法确保结果为真分数且除数不为0
...
}
// 计算表达式结果
Fraction result = calculateResult(left.result, right.result, op);
// 处理括号(避免改变运算顺序)
String leftExpr = left.expression;
String rightExpr = right.expression;
if (op.equals("-") && containsAddOrSubtract(right.expression)) {
rightExpr = "(" + rightExpr + ")"; // 减法右表达式含加减时加括号
}
...
return new ExpressionResult(leftExpr + " " + op + " " + rightExpr, result);
}
// 表达式去重(归一化):通过去括号和交换律标准化表达式
public static String normalizeExpression(String expr) {
String normalized = expr.replaceAll("[()]", ""); // 去除括号
// 匹配加法/乘法表达式,交换左右操作数(确保"a+b"和"b+a"视为同一表达式)
Pattern pattern = Pattern.compile("(.+) ([+×]) (.+)");
Matcher matcher = pattern.matcher(normalized);
if (matcher.matches()) {
String left = matcher.group(1);
String op = matcher.group(2);
String right = matcher.group(3);
String normalizedLeft = normalizeExpression(left);
String normalizedRight = normalizeExpression(right);
// 按字典序交换,确保顺序一致
if (normalizedLeft.compareTo(normalizedRight) > 0) {
return normalizedRight + " " + op + " " + normalizedLeft;
}
}
return normalized;
}
public static Fraction calculateExpression(String expr) {
expr = expr.replace("=", "").trim();
// 格式化表达式:在运算符和括号周围加空格(便于分词)
expr = expr.replaceAll("([()+\\-×÷])", " $1 ").replaceAll("\\s+", " ").trim();
return evaluateExpressionWithStack(expr);
}
// 栈式计算:中缀转后缀(逆波兰式)+ 后缀计算
private static Fraction evaluateExpressionWithStack(String expr) {
List<Object> postfix = infixToPostfix(expr); // 中缀转后缀
return evaluatePostfix(postfix); // 计算后缀表达式
}
点击查看代码
private static void gradeExercises(String exerciseFile, String answerFile) {
List<String> exercises = readLines(exerciseFile);
List<String> answers = readLines(answerFile);
List<Integer> correct = new ArrayList<>();
List<Integer> wrong = new ArrayList<>();
for (int i = 0; i < exercises.size(); i++) {
String exercise = exercises.get(i);
String userAnswer = answers.get(i);
try {
// 计算正确答案并与用户答案对比
Fraction correctAnswer = calculateExpression(exercise);
Fraction userAns = Fraction.parse(userAnswer);
if (correctAnswer.equals(userAns)) {
correct.add(i + 1);
} else {
wrong.add(i + 1);
}
} catch (Exception e) {
wrong.add(i + 1); // 解析错误视为答错
}
}
// 保存批改结果到Grade.txt
try (BufferedWriter writer = new BufferedWriter(new FileWriter(GRADE_FILE))) {
writer.write(formatGradeLine("Correct", correct));
writer.write(formatGradeLine("Wrong", wrong));
}
}
6.测试运行
测试用例运行结果

测试用例覆盖率

测试用例
| 测试函数 | 测试目标 |
|---|---|
| testFractionSimplifyWithMixedNumber | 验证假分数(整数 + 分数)的自动简化逻辑,确保分子分母约分并转换为带分数。 |
| testFractionSimplifyWithNegative | 验证负分数的符号处理(分母转为正数,符号转移到整数部分)。 |
| testFractionAddPureFractions | 验证两个纯分数的加法运算,通分后求和并自动简化。 |
| testFractionAddIntegerAndMixed | 验证整数与带分数的加法,将整数转换为分数后通分求和。 |
| testFractionSubtractToPure | 验证带分数减整数的运算,结果为纯分数时的简化逻辑。 |
| testCalculateNestedParentheses | 验证栈式计算对多层括号的优先级处理,从内层到外层逐步计算。 |
| testCalculateMixedOperators | 验证运算符优先级(×、÷ 高于 +)的处理逻辑。 |
| testFractionParseEdgeCases | 验证特殊格式字符串(整数 0、负零、大整数带分数)的解析逻辑。 |
| testFractionWithDenominatorOne | 验证分母为 1 的分数(等价于整数)的加法逻辑,确保与整数运算一致。 |
| testDivideByZeroException | 验证除数为零时是否正确抛出算术异常,避免程序崩溃。 |
| testGradeMultipleExercises | 模拟多题场景,验证正确答案和错误答案(含运算顺序错误)的判断逻辑。 |
7.实际花费时间
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 15 | 20 |
| · Estimate | · 估计这个任务需要多少时间 | 15 | 20 |
| Development | 开发 | 335 | 410 |
| · Analysis | · 需求分析 (包括学习新技术) | 30 | 30 |
| · Design Spec | · 生成设计文档 | 40 | 50 |
| · Design Review | · 设计复审 | 15 | 20 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
| · Design | · 具体设计 | 60 | 60 |
| · Coding | · 具体编码 | 120 | 200 |
| · Code Review | · 代码复审 | 30 | 20 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 | 40 |
| Reporting | 报告 | 95 | 95 |
| · Test Repor | · 测试报告 | 60 | 50 |
| · Size Measurement | · 计算工作量 | 15 | 20 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 25 |
| · 合计 | 425 | 505 |
8.项目小结
1、项目功能
题目生成:根据用户指定的数量(-n)和数值范围(-r),自动生成含整数、普通分数、带分数的四则运算题(支持+ - × ÷),确保结果非负(减法)、除法结果为真分数,且无重复表达式。
答案批改:通过读取题目文件(-e)和用户答案文件(-a),自动计算每道题的正确结果并比对,生成Grade.txt记录正确率及错题详情。
2、技术亮点
精确分数计算:通过Fraction内部类实现分数的解析、四则运算与约分,完全规避浮点数精度误差,支持带分数(如2'1/3)的表示与计算。
表达式去重:采用normalizeExpression方法对表达式归一化(移除括号、排序),结合HashSet确保生成的题目无等价重复(如3+2与2+3)。
健壮的流程控制:通过命令行参数解析区分功能模式,覆盖文件读写异常、参数错误、表达式格式错误等边界场景,输出清晰的提示信息。
3、开发过程
在两人共同解决设计思路与代码设计问题后,我们分别承担了相应的生成设计文稿以及对代码进行测试分析的任务。发现,在项目开始时要明确流程,了解写代码的思路流程,并且在代码写完后要进行测试用例检测以及用相关工具分析项目的性能,进一步优化代码。早点解决代码设计问题能更快推进项目设计。
通过这个结对项目,我们也明白了做项目要分工明确,我们会按模块拆分任务,经常一起沟通进度,遇到卡点像代码设计遇到表达式括号以及运算符优先级的困扰时即时讨论,理清设计思路以及上网学习新技术,团队协作能力有了极大的提升。在完成自己的任务时都需要将对方的任务看一遍,理清思路,看哪些地方可以优化,完善项目内容。

浙公网安备 33010602011771号