软件工程第三次作业-结对项目
队员一:计科3班 冯诗萍 3223004555
队员二:计科3班 吴业鸿 3223004257
github项目地址:https://github.com/fengshiping/second
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience |
| 这个作业要求在哪里| https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13479 |
| 这个作业的目标 | <实现一个自动生成小学四则运算题目的命令行程序> |
一、PSP图
| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 60 | 85 |
| · Estimate | · 估计这个任务需要多少时间 | 60 | 85 |
| Development | 开发 | 780 | 800 |
| · Analysis | · 需求分析(包括学习新技术) | 120 | 140 |
| · Design Spec | · 生成设计文档 | 90 | 100 |
| · Design Review | · 设计复审(和同事审核设计文档) | 60 | 30 |
| · Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 30 | 25 |
| · Design | · 具体设计 | 100 | 135 |
| · Coding | · 具体编码 | 240 | 200 |
| · Code Review | · 代码复审 | 60 | 90 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 60 | 80 |
| Reporting | 报告 | 180 | 170 |
| · Test Report | · 测试报告 | 60 | 60 |
| · Size Measurement | · 计算工作量 | 30 | 20 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 90 | 90 |
| 合计 | 1000 | 1055 |
二、性能分析
使用idea的Java Flight Record性能分析工具进行性能分析
以下是性能分析工具展示的信息:



根据以上数据,可以得到一张性能分析表:
性能分析图表 - 基于实际采样数据
┌────────────────────────────────────────────┬──────────┬──────────┐
│ 方法名称 │ 采样数量 │ 占比 │
├────────────────────────────────────────────┼──────────┼──────────┤
│ ProblemGenerator.generateProblems() │ 6 │ 60.0% │
│ ├─ generateExpression() │ (包含) │ (主要) │
│ ├─ ExpressionNode.evaluate() │ (包含) │ (次要) │
│ └─ toInfixString()/getCanonicalKey() │ (包含) │ (次要) │
├────────────────────────────────────────────┼──────────┼──────────┤
│ JFR监控系统开销 │ 5 │ 33.3% │
│ ├─ PlatformRecorder.periodicTask() │ 1 │ 6.7% │
│ └─ 其他JFR内部方法 │ 4 │ 26.6% │
├────────────────────────────────────────────┼──────────┼──────────┤
│ 其他方法 │ 1 │ 6.7% │
└────────────────────────────────────────────┴──────────┴──────────┘
总采样数: 9 | 有效业务代码占比: 66.7%
具体的性能问题分析:
主要瓶颈:generateProblems() - 60.0%
优化后的性能分析图:

性能优化效果总结
| 指标 | 优化前 | 优化后 | 说明 |
|---|---|---|---|
| 主业务方法可见性 | 不清晰 | 清晰 | 优化后能明确看到 generateProblems 是热点 |
| JFR 内部开销 | 高 | 低 | 优化后 JFR 自身不再成为性能瓶颈 |
| 业务方法占比 | 低(被掩盖) | 高(76.5%) | 优化后业务逻辑成为主要性能点 |
| 可优化点 | 不明确 | 明确 | 现在可以针对 generateValidCachedExpression 进行优化 |
三、设计实现过程
核心类设计:
| 类名 | 职责 | 关键方法 |
|---|---|---|
| Main | 程序入口,流程控制 | main(), execute() |
| ProblemGenerator | 题目生成算法 | generateProblems(), generateValidCachedExpression() |
| ExpressionNode | 表达式树结构 | evaluate(), toInfixString(), getCanonicalKey() |
| Fraction | 分数运算 | 四则运算方法 |
| AnswerChecker | 答案批改 | grade() |
| CommandLineArgs | 参数解析 | 参数验证方法 |
题目生成流程:

设计亮点:
- 缓存优化设计
// 双重缓存机制
private final Map<String, CachedExpression> expressionCache = new HashMap<>();
private final SetinvalidExpressions = new HashSet<>();
// 规范化键生成 - 确保表达式语义等价去重
public String getCanonicalKey() {
if (operator == Operator.ADD || operator == Operator.MULTIPLY) {
// 可交换运算符排序
if (leftKey.compareTo(rightKey) > 0) {
return operator.name() + "(" + rightKey + "," + leftKey + ")";
}
}
}
2. 表达式树设计
// 多态节点设计
public class ExpressionNode {
public enum NodeType { NUMBER, OPERATOR }
public enum Operator { ADD, SUBTRACT, MULTIPLY, DIVIDE }
// 递归求值
public Fraction evaluate() {
if (type == NodeType.NUMBER) return value;
Fraction leftVal = left.evaluate();
Fraction rightVal = right.evaluate();
// ... 运算符处理
}
}
四、代码说明
项目结构

核心代码分析
- 分数计算核心 - Fraction.java
package com.wyh;
import java.util.Objects;
public class Fraction implements Comparable
private final int numerator;
private final int denominator;
public Fraction(int numerator, int denominator) {
if (denominator == 0) {
throw new IllegalArgumentException("分母不能为零");
}
// 规范化符号
if (denominator < 0) {
numerator = -numerator;
denominator = -denominator;
}
int gcd = gcd(Math.abs(numerator), denominator);
this.numerator = numerator / gcd;
this.denominator = denominator / gcd;
}
public Fraction(int wholeNumber) {
this.numerator = wholeNumber;
this.denominator = 1;
}
private int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
// 四则运算
public Fraction add(Fraction other) {
int newNum = this.numerator * other.denominator + other.numerator * this.denominator;
int newDen = this.denominator * other.denominator;
return new Fraction(newNum, newDen);
}
public Fraction subtract(Fraction other) {
int newNum = this.numerator * other.denominator - other.numerator * this.denominator;
int newDen = this.denominator * other.denominator;
return new Fraction(newNum, newDen);
}
public Fraction multiply(Fraction other) {
int newNum = this.numerator * other.numerator;
int newDen = this.denominator * other.denominator;
return new Fraction(newNum, newDen);
}
public Fraction divide(Fraction other) {
if (other.numerator == 0) {
throw new ArithmeticException("除零错误");
}
int newNum = this.numerator * other.denominator;
int newDen = this.denominator * other.numerator;
return new Fraction(newNum, newDen);
}
// 比较方法
public boolean isZero() {
return numerator == 0;
}
public boolean isProper() {
return Math.abs(numerator) < denominator;
}
@Override
public int compareTo(Fraction other) {
long left = (long) this.numerator * other.denominator;
long right = (long) other.numerator * this.denominator;
return Long.compare(left, right);
}
@Override
public String toString() {
if (denominator == 1) {
return String.valueOf(numerator);
}
if (Math.abs(numerator) < denominator) {
return numerator + "/" + denominator;
}
int whole = numerator / denominator;
int remainder = Math.abs(numerator % denominator);
if (remainder == 0) {
return String.valueOf(whole);
}
return whole + "'" + remainder + "/" + denominator;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Fraction other = (Fraction) obj;
return numerator == other.numerator && denominator == other.denominator;
}
@Override
public int hashCode() {
return Objects.hash(numerator, denominator);
}
public int getNumerator() { return numerator; }
public int getDenominator() { return denominator; }
}
- 表达式树 - ExpressionNode.java
package com.wyh;
import java.util.ArrayList;
import java.util.List;
public class ExpressionNode {
public enum NodeType { NUMBER, OPERATOR }
public enum Operator { ADD, SUBTRACT, MULTIPLY, DIVIDE }
private NodeType type;
private Fraction value;
private Operator operator;
private ExpressionNode left;
private ExpressionNode right;
// 叶节点构造函数
public ExpressionNode(Fraction value) {
this.type = NodeType.NUMBER;
this.value = value;
}
// 运算符节点构造函数
public ExpressionNode(Operator operator, ExpressionNode left, ExpressionNode right) {
this.type = NodeType.OPERATOR;
this.operator = operator;
this.left = left;
this.right = right;
}
public Fraction evaluate() {
if (type == NodeType.NUMBER) {
return value;
}
Fraction leftVal = left.evaluate();
Fraction rightVal = right.evaluate();
switch (operator) {
case ADD: return leftVal.add(rightVal);
case SUBTRACT: return leftVal.subtract(rightVal);
case MULTIPLY: return leftVal.multiply(rightVal);
case DIVIDE: return leftVal.divide(rightVal);
default: throw new IllegalStateException("未知运算符");
}
}
public String toInfixString() {
if (type == NodeType.NUMBER) {
return value.toString();
}
String leftStr = left.toInfixString();
String rightStr = right.toInfixString();
// 根据优先级决定是否加括号
if (needsParentheses(left, false)) {
leftStr = "(" + leftStr + ")";
}
if (needsParentheses(right, true)) {
rightStr = "(" + rightStr + ")";
}
String opStr = getOperatorSymbol();
return leftStr + " " + opStr + " " + rightStr;
}
private boolean needsParentheses(ExpressionNode child, boolean isRight) {
if (child.type != NodeType.OPERATOR) {
return false;
}
int parentPrecedence = getPrecedence(this.operator);
int childPrecedence = getPrecedence(child.operator);
if (childPrecedence < parentPrecedence) {
return true;
}
// 处理结合性
if (childPrecedence == parentPrecedence) {
if (isRight && (operator == Operator.SUBTRACT || operator == Operator.DIVIDE)) {
return true;
}
}
return false;
}
private int getPrecedence(Operator op) {
switch (op) {
case ADD: case SUBTRACT: return 1;
case MULTIPLY: case DIVIDE: return 2;
default: return 0;
}
}
private String getOperatorSymbol() {
switch (operator) {
case ADD: return "+";
case SUBTRACT: return "-";
case MULTIPLY: return "×";
case DIVIDE: return "÷";
default: return "?";
}
}
// 生成规范化键用于去重
public String getCanonicalKey() {
if (type == NodeType.NUMBER) {
return value.toString();
}
String leftKey = left.getCanonicalKey();
String rightKey = right.getCanonicalKey();
// 对可交换运算符进行排序
if (operator == Operator.ADD || operator == Operator.MULTIPLY) {
if (leftKey.compareTo(rightKey) > 0) {
return operator.name() + "(" + rightKey + "," + leftKey + ")";
}
}
return operator.name() + "(" + leftKey + "," + rightKey + ")";
}
public int getOperatorCount() {
if (type == NodeType.NUMBER) {
return 0;
}
return 1 + left.getOperatorCount() + right.getOperatorCount();
}
// Getters
public NodeType getType() { return type; }
public Fraction getValue() { return value; }
public Operator getOperator() { return operator; }
public ExpressionNode getLeft() { return left; }
public ExpressionNode getRight() { return right; }
}
3.题目生成器核心 - ProblemGenerator.java
package com.wyh;
import java.util.*;
public class ProblemGenerator {
private final int range;
private final Random random;
// 添加缓存机制
private final Map<String, CachedExpression> expressionCache = new HashMap<>();
private final Set<String> invalidExpressions = new HashSet<>();
// 缓存内部类
private static class CachedExpression {
final String infixString;
final String answer;
final String canonicalKey;
final int operatorCount;
CachedExpression(ExpressionNode expr) {
this.infixString = expr.toInfixString();
this.answer = expr.evaluate().toString();
this.canonicalKey = expr.getCanonicalKey();
this.operatorCount = expr.getOperatorCount();
}
}
public ProblemGenerator(int range) {
if (range <= 0) {
throw new BusinessException(ErrorCode.INVALID_RANGE_PARAMETER.getCode(),
"数值范围必须为正整数");
}
this.range = range;
this.random = new Random();
}
public List<Problem> generateProblems(int count) {
if (count <= 0) {
throw new BusinessException(ErrorCode.INVALID_COUNT_PARAMETER.getCode(),
"题目数量必须为正整数");
}
Set<String> seenKeys = new HashSet<>();
List<Problem> problems = new ArrayList<>();
int attempts = 0;
int maxAttempts = count * 100; // 减少尝试次数
while (problems.size() < count && attempts < maxAttempts) {
attempts++;
CachedExpression cached = generateValidCachedExpression();
if (cached == null || !seenKeys.add(cached.canonicalKey)) {
continue;
}
problems.add(new Problem(cached.infixString, cached.answer));
}
if (problems.size() < count) {
throw new BusinessException(ErrorCode.INSUFFICIENT_UNIQUE_PROBLEMS.getCode(),
String.format("无法在合理尝试次数内生成足够的不重复题目(已生成 %d/%d)。请增大范围参数 -r 或减少题目数量 -n",
problems.size(), count));
}
return problems;
}
private CachedExpression generateValidCachedExpression() {
// 快速尝试3次
for (int quickAttempt = 0; quickAttempt < 3; quickAttempt++) {
ExpressionNode expression = generateOptimizedExpression(1 + random.nextInt(3));
String key = expression.getCanonicalKey();
// 检查已知无效表达式
if (invalidExpressions.contains(key)) {
continue;
}
// 检查缓存
CachedExpression cached = expressionCache.get(key);
if (cached != null) {
if (isValidCachedExpression(cached)) {
return cached;
} else {
invalidExpressions.add(key);
continue;
}
}
// 新表达式,验证并缓存
try {
// 快速验证运算符数量
if (expression.getOperatorCount() > 3) {
invalidExpressions.add(key);
continue;
}
// 验证表达式有效性
Fraction result = expression.evaluate();
if (isValidFraction(result)) {
CachedExpression newCached = new CachedExpression(expression);
expressionCache.put(key, newCached);
return newCached;
} else {
invalidExpressions.add(key);
}
} catch (Exception e) {
invalidExpressions.add(key);
}
}
return null;
}
private ExpressionNode generateOptimizedExpression(int operatorCount) {
return generateExpression(operatorCount);
}
// 优化原有的 generateExpression 方法
private ExpressionNode generateExpression(int operatorCount) {
if (operatorCount == 0) {
return new ExpressionNode(generateRandomFraction());
}
ExpressionNode.Operator op = randomOperator();
int leftOps = random.nextInt(operatorCount);
int rightOps = operatorCount - 1 - leftOps;
ExpressionNode left = generateExpression(leftOps);
ExpressionNode right = generateExpression(rightOps);
return applyOptimizedConstraints(op, left, right);
}
// 优化约束应用逻辑
private ExpressionNode applyOptimizedConstraints(ExpressionNode.Operator op,
ExpressionNode left, ExpressionNode right) {
// 对于减法和除法,进行快速检查
if (op == ExpressionNode.Operator.SUBTRACT || op == ExpressionNode.Operator.DIVIDE) {
try {
Fraction leftVal = left.evaluate();
Fraction rightVal = right.evaluate();
switch (op) {
case SUBTRACT:
if (leftVal.compareTo(rightVal) < 0) {
return new ExpressionNode(op, right, left);
}
break;
case DIVIDE:
if (rightVal.isZero()) {
// 重新生成右节点,但限制次数
right = generateSimpleExpression();
}
// 检查除法结果是否为真分数
Fraction divisionResult = leftVal.divide(rightVal);
if (!divisionResult.isProper() && leftVal.compareTo(rightVal) >= 0) {
return new ExpressionNode(op, right, left);
}
break;
}
} catch (Exception e) {
// 如果计算出错,交换节点重试
return new ExpressionNode(op, right, left);
}
}
return new ExpressionNode(op, left, right);
}
// 生成简单表达式(避免深度递归)
private ExpressionNode generateSimpleExpression() {
if (random.nextDouble() < 0.5) {
return new ExpressionNode(generateRandomFraction());
} else {
ExpressionNode.Operator op = randomOperator();
// 避免除法和减法以减少复杂度
while (op == ExpressionNode.Operator.DIVIDE || op == ExpressionNode.Operator.SUBTRACT) {
op = randomOperator();
}
return new ExpressionNode(op,
new ExpressionNode(generateRandomFraction()),
new ExpressionNode(generateRandomFraction()));
}
}
private boolean isValidCachedExpression(CachedExpression cached) {
return cached.operatorCount <= 3;
}
private boolean isValidFraction(Fraction fraction) {
try {
return fraction.compareTo(new Fraction(0)) >= 0; // 非负数
} catch (Exception e) {
return false;
}
}
// 保留原有验证方法(用于兼容性)
private boolean validateExpression(ExpressionNode expr) {
try {
Fraction result = expr.evaluate();
return expr.getOperatorCount() <= 3;
} catch (Exception e) {
return false;
}
}
// 优化分数生成
private Fraction generateRandomFraction() {
// 增加整数比例,减少分数运算
if (random.nextDouble() < 0.8) { // 从0.7提高到0.8
return new Fraction(random.nextInt(range - 1) + 1);
} else {
int denominator = random.nextInt(range - 2) + 2;
int numerator = random.nextInt(denominator - 1) + 1;
return new Fraction(numerator, denominator);
}
}
private ExpressionNode.Operator randomOperator() {
ExpressionNode.Operator[] operators = ExpressionNode.Operator.values();
// 调整运算符概率,减少除法和减法
if (random.nextDouble() < 0.3) {
// 30% 概率选择加法或乘法
return random.nextBoolean() ? ExpressionNode.Operator.ADD : ExpressionNode.Operator.MULTIPLY;
}
return operators[random.nextInt(operators.length)];
}
// 添加清理方法(可选)
public void clearCache() {
expressionCache.clear();
invalidExpressions.clear();
}
}
五、测试运行
- 基础功能测试用例
| 测试用例 | 输入命令 | 预期结果 | 测试目的 |
|---|---|---|---|
| TC01 - 基础生成 | -r 10 -n 5 | 生成5道范围10以内的题目 | 验证基本生成功能 |
| TC02 - 默认数量 | -r 10 | 生成10道题目(默认值) | 验证默认参数处理 |
| TC03 - 大量题目 | -r 10 -n 1000 | 生成1000道不重复题目 | 验证去重算法性能 |
| TC04 - 小范围测试 | -r 3 -n 10 | 生成10道题目,可能提示范围小 | 验证边界情况处理 |
| TC05 - 判分模式 | -e Exercises.txt -a Answers.txt | 输出判分结果 | 验证判分功能 |
- 异常处理测试用例
| 测试用例 | 输入命令 | 预期结果 | 测试目的 |
|---|---|---|---|
| TC06 - 无效参数 | -x 10 | 显示"未知参数"错误 | 验证参数验证 |
| TC07 - 缺少范围 | -n 5 | 显示"缺少-r参数"错误 | 验证必要参数检查 |
| TC08 - 范围为零 | -r 0 -n 5 | 显示"范围必须为正整数" | 验证参数有效性 |
| TC09 - 文件不存在 | -e nofile.txt -a nofile.txt | 显示"文件不存在"错误 | 验证文件处理 |
| TC10 - 数量过大 | -r 5 -n 10000 | 可能提示无法生成足够题目 | 验证合理性检查 |
具体测试情况:
1.




















六、总结和反思
-
需求理解偏差
问题:初期对"真分数"约束理解不一致
解决:通过具体例子澄清,建立术语词典避免歧义。 -
版本控制冲突
教训:某次同时修改ExpressionNode导致合并冲突
改进:建立更细粒度的代码所有权和提交规范。
感想:
吴:
"结对编程让我明白,最好的代码不是完美无瑕的代码,而是两个人都能理解、都愿意维护的代码。技术可以学习,但找到一个能激发你最佳状态的搭档需要缘分。"
冯:
"这次经历让我从'写代码的人'成长为'解决问题的人'。感谢我的搭档,不仅教会我技术,更让我看到了工程师应有的专业和热情。"

浙公网安备 33010602011771号