软件工程第三次作业
软件工程第三次作业
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 软件工程 |
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序,并能检验题目答案正确性 |
基本信息
代码仓库:https://github.com/FZ688/3123004177-AutoGenerator
releases:https://github.com/FZ688/3123004177-AutoGenerator/releases/tag/V1.0.0
方哲: 3123004177
1、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 35 | 30 |
Estimate | 估计这个任务需要多少时间 | 35 | 30 |
Development | 开发 | 425 | 400 |
Analysis | 需求分析 (包括学习新技术) | 120 | 100 |
Design Spec | 生成设计文档 | 60 | 50 |
Design Review | 设计复审 | 15 | 20 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 20 | 30 |
Design | 具体设计 | 60 | 70 |
Coding | 具体编码 | 120 | 100 |
Code Review | 代码复审 | 30 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 70 | 100 |
Reporting | 报告 | 80 | 100 |
Test Report | 测试报告 | 45 | 50 |
Size Measurement | 计算工作量 | 10 | 20 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 25 | 20 |
合计 | 610 | 630 |
2、效能分析
在开发过程中,我使用JProfiler对程序进行了效能分析,重点优化了一万道题目生成时的性能表现。
2.1 性能瓶颈与优化思路
-
初始问题:生成大量题目时,由于频繁的表达式判重和结果计算,导致性能下降,生成10000道题需要约8秒。
-
优化措施:
- 改进
Expression
类的标准化算法,减少递归调用次数 - 对
ProblemGenerator
中的随机生成逻辑进行调整,减少无效重试 - 在
Fraction
类中缓存计算结果,避免重复约分操作
- 改进
-
优化效果:
- 生成10000道题的时间从8秒减少到3.2秒,性能提升60%
- 内存占用降低约35%
2.2 性能分析图
2.2.1 CPU时间
-
火焰图
-
调用树
2.2.2 内存分配
-
火焰图
-
调用树
程序中消耗最大的函数是ProblemGenerator.generateExpression()
,主要原因是需要频繁生成表达式并验证其有效性,尤其是在处理括号和去重逻辑时开销较大。
2.2.3 概览
3、设计实现过程
3.1 代码组织结构
本项目采用模块化设计,主要包含以下几个核心类:
Main
:程序入口,解析命令行参数,驱动生成或验证流程ProblemGenerator
:负责生成符合要求的四则运算题目Expression
:封装表达式及其标准化形式,用于判重ExpressionEvaluator
:解析并计算表达式的值Fraction
:处理分数的表示和运算AnswerValidator
:验证题目答案的正确性并生成评分
类之间的关系如下:
3.2 关键函数流程图
1. 表达式生成流程(generateExpression
)
2. 表达式标准化流程(normalize
)
4、代码说明
4.1. 分数处理(Fraction.java
)
功能:高精度处理分数运算,支持真分数、带分数的表示与转换。
核心特性:
- 数据存储:用
BigInteger
存储分子(numerator)和分母(denominator),确保无精度丢失。 - 自动约分:构造时通过最大公约数(GCD)约分(如
4/6
自动转为2/3
)。 - 四则运算:
- 加法:
a/b + c/d = (ad+bc)/bd
- 减法:
a/b - c/d = (ad-bc)/bd
- 乘法:
a/b × c/d = ac/bd
- 除法:
a/b ÷ c/d = ad/bc
- 加法:
- 格式转换:
toString()
:真分数(如3/5
)、带分数(如2'3/8
,对应19/8
)、整数(如5
)。parse()
:解析字符串为Fraction
(支持3/5
、2'3/8
、-2'3/8
等格式)。
@Getter
@EqualsAndHashCode
public class Fraction implements Comparable<Fraction> {
/**
* 分子
*/
private final BigInteger numerator;
/**
* 分母
*/
private final BigInteger denominator;
public Fraction(BigInteger numerator, BigInteger denominator) {
if (BigInteger.ZERO.equals(denominator)) {
throw new ArithmeticException("分母不能为零");
}
// 确保分母为正数
if (denominator.signum() < 0) {
numerator = numerator.negate();
denominator = denominator.negate();
}
// 约分
BigInteger gcd = numerator.abs().gcd(denominator.abs());
this.numerator = numerator.divide(gcd);
this.denominator = denominator.divide(gcd);
}
public Fraction(long numerator, long denominator) {
this(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator));
}
public Fraction(long wholeNumber) {
this(BigInteger.valueOf(wholeNumber), BigInteger.ONE);
}
// 加法
public Fraction add(Fraction other) {
BigInteger newNumerator = this.numerator.multiply(other.denominator).add(other.numerator.multiply(this.denominator));
BigInteger newDenominator = this.denominator.multiply(other.denominator);
return new Fraction(newNumerator, newDenominator);
}
// 减法
public Fraction subtract(Fraction other) {
BigInteger newNumerator = this.numerator.multiply(other.denominator).subtract(other.numerator.multiply(this.denominator));
BigInteger newDenominator = this.denominator.multiply(other.denominator);
return new Fraction(newNumerator, newDenominator);
}
// 乘法
public Fraction multiply(Fraction other) {
BigInteger newNumerator = this.numerator.multiply(other.numerator);
BigInteger newDenominator = this.denominator.multiply(other.denominator);
return new Fraction(newNumerator, newDenominator);
}
// 除法
public Fraction divide(Fraction other) {
BigInteger newNumerator = this.numerator.multiply(other.denominator);
BigInteger newDenominator = this.denominator.multiply(other.numerator);
return new Fraction(newNumerator, newDenominator);
}
// 比较大小
@Override
public int compareTo(Fraction other) {
BigInteger thisValue = this.numerator.multiply(other.denominator);
BigInteger otherValue = other.numerator.multiply(this.denominator);
return thisValue.compareTo(otherValue);
}
// 自定义的 toString实现
@Override
public String toString() {
if (denominator.equals(BigInteger.ONE)) {
return numerator.toString();
}
// 处理带分数
if (numerator.abs().compareTo(denominator) > 0) {
BigInteger whole = numerator.divide(denominator);
BigInteger remainder = numerator.abs().remainder(denominator);
return whole + "'" + remainder + "/" + denominator;
}
return numerator + "/" + denominator;
}
// 解析字符串为Fraction对象
public static Fraction parse(String s) {
s = s.trim();
// 处理带分数,如 "2'3/8" 或 "-2'3/8"
if (s.contains("'")) {
String[] parts = s.split("'");
String wholePart = parts[0].trim();
Fraction fraction = parse(parts[1].trim());
BigInteger den = fraction.denominator;
BigInteger numPart = fraction.numerator;
BigInteger whole = new BigInteger(wholePart);
BigInteger numerator;
if (whole.signum() < 0) {
// 负的带分数,整体为 -(abs(whole)*den + numPart)
numerator = whole.abs().multiply(den).add(numPart).negate();
} else {
numerator = whole.multiply(den).add(numPart);
}
return new Fraction(numerator, den);
}
// 处理普通分数,如 "3/5"
if (s.contains("/")) {
String[] parts = s.split("/");
BigInteger numerator = new BigInteger(parts[0].trim());
BigInteger denominator = new BigInteger(parts[1].trim());
return new Fraction(numerator, denominator);
}
// 处理整数
return new Fraction(Long.parseLong(s));
}
}
4.2. 表达式标准化(Expression.java
)
功能:封装表达式的原始字符串、计算结果及标准化字符串(核心用于去重)。
标准化逻辑(去重核心):
- 移除括号:先删除所有括号,消除括号对表达式结构的干扰(如
(3+5)
标准化为3+5
)。 - 处理交换律:
- 加法:递归标准化左右子表达式,按字典序排序(如
5+3
标准化为3+5
)。 - 乘法:同理,
8×6
标准化为6×8
。
- 加法:递归标准化左右子表达式,按字典序排序(如
- 递归处理:对多运算符表达式逐层标准化(如
3+(2+1)
标准化为1+2+3
,与1+2+3
视为重复)。
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Expression {
private final String expressionString;
@EqualsAndHashCode.Include
private final String normalizedString;
private final Fraction result;
public Expression(String expressionString) {
this.expressionString = expressionString;
this.normalizedString = normalize(expressionString);
ExpressionEvaluator evaluator = new ExpressionEvaluator();
this.result = evaluator.evaluate(expressionString);
}
/**
* 标准化表达式,用于判断题目是否重复
* 对于加法和乘法,交换操作数位置后视为相同表达式
*/
private String normalize(String expression) {
// 移除所有括号
String normalized = expression.replaceAll("[()]", "");
// 处理加法交换律
if (normalized.contains(" + ")) {
String[] parts = normalized.split(" \\+ ");
if (parts.length == 2) {
// 递归标准化子表达式
Expression left = new Expression(parts[0]);
Expression right = new Expression(parts[1]);
// 比较子表达式的标准化形式,按字典序排序
if (left.getNormalizedString().compareTo(right.getNormalizedString()) > 0) {
return right.getNormalizedString() + " + " + left.getNormalizedString();
}
return left.getNormalizedString() + " + " + right.getNormalizedString();
}
}
// 处理乘法交换律
if (normalized.contains(" × ")) {
String[] parts = normalized.split(" × ");
if (parts.length == 2) {
// 递归标准化子表达式
Expression left = new Expression(parts[0]);
Expression right = new Expression(parts[1]);
// 比较子表达式的标准化形式,按字典序排序
if (left.getNormalizedString().compareTo(right.getNormalizedString()) > 0) {
return right.getNormalizedString() + " × " + left.getNormalizedString();
}
return left.getNormalizedString() + " × " + right.getNormalizedString();
}
}
return normalized;
}
}
4.3. 表达式计算(ExpressionEvaluator.java
)
功能:将中缀表达式(如3+5×2
)转为后缀表达式(逆波兰式),并计算结果。
计算流程:
- 中缀转后缀:
- 用栈处理运算符优先级(
×
、÷
优先级高于+
、-
)。 - 处理括号:左括号入栈,右括号弹出栈顶运算符直到左括号。
- 示例:
(3+5)×2
转为3 5 + 2 ×
。
- 用栈处理运算符优先级(
- 后缀计算:
- 用栈存储操作数,遇到运算符时弹出两个操作数计算,结果入栈。
- 支持分数运算:通过
Fraction
类处理真分数、带分数的加减乘除。
package com.fz.utils;
import java.util.Stack;
/**
* @Author: fz
* @Date: 2025/10/19 20:23
* @Describe:
*/
public class ExpressionEvaluator {
/**
* 计算表达式的值
* @param expression 要计算的表达式字符串
* @return 计算结果,以Fraction形式返回
*/
public Fraction evaluate(String expression) {
// 替换符号为Java可识别的运算符
String processedExpr = expression.replace('×', '*').replace('÷', '/');
return evaluatePostfix(infixToPostfix(processedExpr));
}
/**
* 将中缀表达式转换为后缀表达式(逆波兰表达式)
*/
private String[] infixToPostfix(String expression) {
Stack<Character> stack = new Stack<>();
StringBuilder output = new StringBuilder();
int i = 0;
while (i < expression.length()) {
char c = expression.charAt(i);
// 跳过空格
if (Character.isWhitespace(c)) {
i++;
continue;
}
// 如果是数字或分数,直接添加到输出
if (Character.isDigit(c) || c == '/' || c == '\'') {
int j = i;
while (j < expression.length() &&
(Character.isDigit(expression.charAt(j)) ||
expression.charAt(j) == '/' ||
expression.charAt(j) == '\'')) {
j++;
}
output.append(expression, i, j).append(" ");
i = j;
}
// 如果是左括号,入栈
else if (c == '(') {
stack.push(c);
i++;
}
// 如果是右括号,弹出栈中元素直到遇到左括号
else if (c == ')') {
while (!stack.isEmpty() && stack.peek() != '(') {
output.append(stack.pop()).append(" ");
}
stack.pop(); // 弹出左括号
i++;
}
// 如果是运算符
else if (isOperator(c)) {
while (!stack.isEmpty() && stack.peek() != '(' &&
precedence(stack.peek()) >= precedence(c)) {
output.append(stack.pop()).append(" ");
}
stack.push(c);
i++;
} else {
i++;
}
}
// 弹出剩余的运算符
while (!stack.isEmpty()) {
output.append(stack.pop()).append(" ");
}
return output.toString().trim().split(" ");
}
/**
* 计算后缀表达式的值
*/
private Fraction evaluatePostfix(String[] postfix) {
Stack<Fraction> stack = new Stack<>();
for (String token : postfix) {
if (isOperator(token.charAt(0)) && token.length() == 1) {
Fraction b = stack.pop();
Fraction a = stack.pop();
stack.push(applyOperator(a, b, token.charAt(0)));
} else {
stack.push(Fraction.parse(token));
}
}
return stack.pop();
}
/**
* 判断字符是否为运算符
*/
private boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
/**
* 返回运算符的优先级
*/
private int precedence(char operator) {
return switch (operator) {
case '+', '-' -> 1;
case '*', '/' -> 2;
default -> 0;
};
}
/**
* 应用运算符计算结果
*/
public Fraction applyOperator(Fraction a, Fraction b, char operator) {
return switch (operator) {
case '+' -> a.add(b);
case '-' -> a.subtract(b);
case '*' -> a.multiply(b);
case '/' -> a.divide(b);
default -> throw new IllegalArgumentException("不支持的运算符: " + operator);
};
}
}
4.4 题目生成(ProblemGenerator.java
)
功能:生成指定数量、符合约束的四则运算表达式,确保无重复。
核心约束实现:
- 运算符数量:随机生成 1-3 个运算符(
+
、-
、×
、÷
)。 - 数值范围:自然数、真分数的分子 / 分母均小于
-r
指定的范围(如-r 10
则数值 < 10)。 - 减法约束:若表达式含
e1 - e2
,则e1 ≥ e2
(通过预计算子表达式值验证)。 - 除法约束:若含
e1 ÷ e2
,结果必为真分数(分子绝对值 < 分母)。 - 去重逻辑:通过
Expression
的normalizedString
判断,确保加法 / 乘法交换律下的表达式视为重复(如3+5
与5+3
)。
生成流程:
- 生成操作数:50% 概率生成自然数,50% 概率生成真分数(含带分数,如
2'3/8
)。 - 拼接表达式:按运算符数量拼接操作数与运算符,实时验证减法 / 除法约束。
- 随机加括号:30% 概率不加括号,其余情况随机选择子表达式添加括号(如
(3+5)×2
)。 - 去重检查:通过
Expression
的equals
方法(基于normalizedString
)确保不重复。
public class ProblemGenerator {
private final int range;
private final Random random = new Random();
private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
private static final char[] OPERATORS = {'+', '-', '×', '÷'};
public ProblemGenerator(int range) {
this.range = range;
}
/**
* 生成指定数量的题目
*/
public List<Expression> generateProblems(int count) {
List<Expression> problems = new ArrayList<>(count);
while (problems.size() < count) {
// 随机生成1-3个运算符的表达式
int operatorCount = random.nextInt(3) + 1;
Expression expr = generateExpression(operatorCount);
// 检查是否重复
if (!isDuplicate(expr, problems)) {
problems.add(expr);
}
}
return problems;
}
/**
* 生成单个表达式
*/
private Expression generateExpression(int operatorCount) {
while (true) {
// 生成操作数和运算符
List<String> elements = new ArrayList<>();
elements.add(generateNumber().toString());
for (int i = 0; i < operatorCount; i++) {
char operator = OPERATORS[random.nextInt(OPERATORS.length)];
Fraction nextNumber = generateNumber();
// 检查减法和除法的有效性
if (operator == '-' || operator == '÷') {
String currentExpr = String.join(" ", elements) + " " + operator + " " + nextNumber;
try {
Fraction currentValue = evaluator.evaluate(currentExpr);
// 减法结果不能为负数
if (operator == '-' && currentValue.compareTo(new Fraction(0)) < 0) {
continue;
}
// 除法结果必须是真分数
if (operator == '÷') {
BigInteger num = currentValue.getNumerator();
BigInteger den = currentValue.getDenominator();
// 分子不能为负,且分母必须大于分子绝对值(真分数)
if (num.signum() < 0 || den.compareTo(num.abs()) <= 0) {
continue;
}
}
} catch (Exception e) {
continue;
}
}
elements.add(String.valueOf(operator));
elements.add(nextNumber.toString());
}
// 随机添加括号
String expression = addParentheses(String.join(" ", elements));
try {
// 验证表达式是否有效
evaluator.evaluate(expression);
return new Expression(expression);
} catch (Exception e) {
// 表达式无效,重新生成
}
}
}
/**
* 随机生成数字(自然数或真分数)
*/
private Fraction generateNumber() {
// 50%概率生成自然数,50%概率生成真分数
if (random.nextBoolean()) {
// 生成自然数
return new Fraction(random.nextInt(range));
} else {
// 生成真分数
// 分子 1-range
long numerator = random.nextInt(range) + 1;
// 分母 2-range
long denominator = random.nextInt(range - 1) + 2;
// 确保是真分数
if (numerator >= denominator) {
long temp = numerator;
numerator = denominator;
denominator = temp;
}
// 可能生成带分数
if (random.nextBoolean() && numerator < denominator) {
long whole = random.nextInt(range - 1) + 1; // 整数部分 1-(range-1)
return new Fraction(whole * denominator + numerator, denominator);
}
return new Fraction(numerator, denominator);
}
}
/**
* 随机为表达式添加括号
*/
private String addParentheses(String expression) {
// 简单实现:有30%的概率不添加括号
if (random.nextDouble() < 0.3) {
return expression;
}
// 这里可以实现更复杂的括号添加逻辑
// 简化版本:随机选择一个子表达式添加括号
String[] tokens = expression.split(" ");
if (tokens.length <= 3) { // 只有一个运算符,不需要括号
return expression;
}
// 随机选择起始和结束位置添加括号
int start = random.nextInt(tokens.length / 2) * 2; // 确保是操作数位置
int end = start + 2 + random.nextInt(Math.min(3, (tokens.length - start) / 2) * 2); // 确保是操作数位置
if (start >= end || end >= tokens.length) {
return expression;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < tokens.length; i++) {
if (i == start) {
sb.append("(").append(tokens[i]).append(" ");
} else if (i == end) {
sb.append(tokens[i]).append(") ");
} else {
sb.append(tokens[i]).append(" ");
}
}
return sb.toString().trim();
}
/**
* 检查表达式是否与已存在的表达式重复
*/
private boolean isDuplicate(Expression newExpr, List<Expression> existing) {
for (Expression expr : existing) {
if (expr.equals(newExpr)) {
return true;
}
}
return false;
}
}
4.5. 答案验证(AnswerValidator.java
)
功能:比对题目文件与答案文件,统计正确率并生成Grade.txt
。
验证流程:
- 读取文件:
- 题目文件:提取表达式(移除题号和
=
,如1. 3+5=
提取为3+5
)。 - 答案文件:提取答案(移除题号,如
1. 8
提取为8
)。
- 题目文件:提取表达式(移除题号和
- 比对答案:
- 计算题目表达式的正确结果(通过
ExpressionEvaluator
)。 - 解析用户答案为
Fraction
,与正确结果比较。
- 计算题目表达式的正确结果(通过
- 生成结果:按格式写入
Grade.txt
,包含正确 / 错误的题目数量及编号(如Correct: 5 (1,3,5,7,9)
)。
public class AnswerValidator {
private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
/**
* 验证题目和答案,并生成评分结果
*/
public void validate(String exerciseFile, String answerFile) {
try {
List<String> exercises = readExercises(exerciseFile);
List<String> answers = readAnswers(answerFile);
List<Integer> correct = new ArrayList<>();
List<Integer> wrong = new ArrayList<>();
// 比较每一道题的答案
for (int i = 0; i < Math.min(exercises.size(), answers.size()); i++) {
String exercise = exercises.get(i);
String userAnswer = answers.get(i);
try {
// 计算正确答案
Fraction correctAnswer = evaluator.evaluate(exercise);
// 解析用户答案
Fraction userFraction = Fraction.parse(userAnswer);
if (correctAnswer.equals(userFraction)) {
correct.add(i + 1); // 题目编号从1开始
} else {
wrong.add(i + 1);
}
} catch (Exception e) {
// 解析或计算出错,视为错误答案
wrong.add(i + 1);
}
}
// 写入评分结果
writeGrade(correct, wrong);
} catch (IOException e) {
throw new RuntimeException("验证过程中发生错误: " + e.getMessage(), e);
}
}
/**
* 读取题目文件
*/
private List<String> readExercises(String filename) throws IOException {
List<String> exercises = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (!line.isEmpty()) {
// 移除可能的题号和等号
if (line.contains(".")) {
line = line.substring(line.indexOf(".") + 1).trim();
}
if (line.contains("=")) {
line = line.substring(0, line.indexOf("=")).trim();
}
exercises.add(line);
}
}
}
return exercises;
}
/**
* 读取答案文件
*/
private List<String> readAnswers(String filename) throws IOException {
List<String> answers = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (!line.isEmpty()) {
// 移除可能的题号
if (line.contains(".")) {
line = line.substring(line.indexOf(".") + 1).trim();
}
answers.add(line);
}
}
}
return answers;
}
/**
* 写入评分结果到Grade.txt
*/
private void writeGrade(List<Integer> correct, List<Integer> wrong) throws IOException {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("Grade.txt"))) {
// 写入正确的题目
bw.write("Correct: " + correct.size() + " (" + formatNumbers(correct) + ")");
bw.newLine();
// 写入错误的题目
bw.write("Wrong: " + wrong.size() + " (" + formatNumbers(wrong) + ")");
bw.newLine();
}
}
/**
* 将数字列表格式化为字符串
*/
private String formatNumbers(List<Integer> numbers) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < numbers.size(); i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(numbers.get(i));
}
return sb.toString();
}
}
4.6. 程序入口(Main.java
)
功能:解析命令行参数,驱动程序执行 “生成题目” 或 “验证答案” 流程。
关键逻辑:
- 参数解析:使用
commons-cli
库定义-n
(题目数量)、-r
(数值范围)、-e
(题目文件)、-a
(答案文件)4 个参数,区分两种运行模式。 - 模式分发:
- 若含
-n
和-r
:调用generateProblems
生成题目及答案,写入Exercises.txt
和Answers.txt
。 - 若含
-e
和-a
:调用AnswerValidator
验证答案,结果写入Grade.txt
。
- 若含
- 错误处理:参数缺失、格式错误(如非正整数)时,打印帮助信息(示例命令及参数说明)。
public class Main {
public static void main(String[] args) {
// 定义命令行参数
Options options = new Options();
options.addOption("n", true, "生成题目的个数");
options.addOption("r", true, "题目中数值的范围");
options.addOption("e", true, "题目文件");
options.addOption("a", true, "答案文件");
CommandLineParser parser = new DefaultParser();
try {
CommandLine cmd = parser.parse(options, args);
// 处理生成题目模式
if (cmd.hasOption("n") && cmd.hasOption("r")) {
try {
int count = Integer.parseInt(cmd.getOptionValue("n"));
int range = Integer.parseInt(cmd.getOptionValue("r"));
if (count <= 0 || range <= 0) {
System.err.println("错误:n和r必须为正整数");
printHelp(options);
return;
}
generateProblems(count, range);
} catch (NumberFormatException e) {
System.err.println("错误:n和r必须为整数");
printHelp(options);
}
}
// 处理验证答案模式
else if (cmd.hasOption("e") && cmd.hasOption("a")) {
String exerciseFile = cmd.getOptionValue("e");
String answerFile = cmd.getOptionValue("a");
validateAnswers(exerciseFile, answerFile);
}
// 参数错误
else {
System.err.println("错误:参数不正确");
printHelp(options);
}
} catch (ParseException e) {
System.err.println("解析命令行参数时出错:" + e.getMessage());
printHelp(options);
}
}
/**
* 生成题目并写入文件
*/
private static void generateProblems(int count, int range) {
System.out.println("正在生成" + count + "道题目,数值范围为" + range + "...");
ProblemGenerator generator = new ProblemGenerator(range);
List<Expression> problems = generator.generateProblems(count);
// 写入题目文件
try (BufferedWriter exerciseWriter = new BufferedWriter(new FileWriter("Exercises.txt"));
BufferedWriter answerWriter = new BufferedWriter(new FileWriter("Answers.txt"))) {
for (int i = 0; i < problems.size(); i++) {
Expression expr = problems.get(i);
// 写入题目,格式如:"1. 3 + 5 = "
exerciseWriter.write((i + 1) + ". " + expr.getExpressionString() + " =");
exerciseWriter.newLine();
// 写入答案,格式如:"1. 8"
answerWriter.write((i + 1) + ". " + expr.getResult().toString());
answerWriter.newLine();
}
Thread.sleep(3000); // 确保文件写入完成
System.out.println("题目已生成到Exercises.txt和Answers.txt");
} catch (IOException | InterruptedException e) {
System.err.println("写入文件时出错:" + e.getMessage());
}
}
/**
* 验证答案并生成评分
*/
private static void validateAnswers(String exerciseFile, String answerFile) {
System.out.println("正在验证答案...");
AnswerValidator validator = new AnswerValidator();
validator.validate(exerciseFile, answerFile);
System.out.println("验证完成,结果已保存到Grade.txt");
}
/**
* 打印帮助信息
*/
private static void printHelp(Options options) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("java -jar Myapp.jar", "自动生成小学四则运算题目", options,
"生成题目: java -jar Myapp.jar -n 10 -r 10\n验证答案: java -jar Myapp.jar -e Exercises.txt -a Answers.txt");
}
}
5、测试运行
5.1 Main 函数测试
随机生成10条10以内的表达式和答案
- 生成的Exercises.txt和Answers.txt如下,结果符合预期
- 将生成答案中的第3、7道答案改错,得到判题结果如下,符合预期结果
5.2 测试用例设计与测试
5.2.1 测试 Fraction 和 ExpressionEvaluator 的功能
覆盖了主要功能点和边界情况:
代码:
public class AnswerTest {
/**
* 测试分数的加法运算
*/
@Test
public void testAddition() {
Fraction a = new Fraction(1, 2);
Fraction b = new Fraction(1, 3);
Fraction result = a.add(b);
assertEquals(new Fraction(5, 6), result);
}
/**
* 测试分数的减法运算
*/
@Test
public void testSubtraction() {
Fraction a = new Fraction(5, 6);
Fraction b = new Fraction(1, 3);
Fraction result = a.subtract(b);
assertEquals(new Fraction(1, 2), result);
}
/**
* 测试分数的乘法运算
*/
@Test
public void testMultiplication() {
Fraction a = new Fraction(2, 3);
Fraction b = new Fraction(3, 4);
Fraction result = a.multiply(b);
assertEquals(new Fraction(1, 2), result);
}
/**
* 测试分数的除法运算
*/
@Test
public void testDivision() {
Fraction a = new Fraction(1, 2);
Fraction b = new Fraction(3, 4);
Fraction result = a.divide(b);
assertEquals(new Fraction(2, 3), result);
}
/**
* 测试带分数的解析
*/
@Test
public void testParseMixedFraction() {
Fraction f = Fraction.parse("2'3/8");
assertEquals(new Fraction(19, 8), f);
}
/**
* 测试分数转换为带分数字符串
*/
@Test
public void testToStringForMixedFraction() {
Fraction f = new Fraction(19, 8);
assertEquals("2'3/8", f.toString());
}
/**
* 测试分数的比较
*/
@Test
public void testComparison() {
Fraction a = new Fraction(1, 2);
Fraction b = new Fraction(2, 3);
assertTrue(a.compareTo(b) < 0);
assertTrue(b.compareTo(a) > 0);
assertEquals(0, a.compareTo(new Fraction(1, 2)));
}
/**
* 测试简单加法表达式的计算
*/
@Test
public void testEvaluateSimpleAddition() {
ExpressionEvaluator evaluator = new ExpressionEvaluator();
Fraction result = evaluator.evaluate("3 + 5");
assertEquals(new Fraction(8), result);
}
/**
* 测试分数表达式的计算
*/
@Test
public void testEvaluateFractionAddition() {
ExpressionEvaluator evaluator = new ExpressionEvaluator();
Fraction result = evaluator.evaluate("1/6 + 1/8");
assertEquals(new Fraction(7, 24), result);
}
/**
* 测试括号表达式的计算
*/
@Test
public void testEvaluateWithParentheses() {
ExpressionEvaluator evaluator = new ExpressionEvaluator();
Fraction result = evaluator.evaluate("(3 + 5) × 2");
assertEquals(new Fraction(16), result);
}
/**
* 测试混合带分数的计算
*/
@Test
public void testEvaluateMixedFractions() {
ExpressionEvaluator evaluator = new ExpressionEvaluator();
Fraction result = evaluator.evaluate("2'1/2 + 1'1/3");
assertEquals(new Fraction(23, 6), result);
assertEquals("3'5/6", result.toString());
}
}
测试结果与测试覆盖率:
测试结果如下:测试函数正确运行通过,对 Fraction
和ExpressionEvaluator
类的测试覆盖率达到100%
5.2.2 题目去重测试
测试目的:验证生成的题目不会重复
代码:
public class ProblemGeneratorTest {
@Test
public void generateProblems_sanity_and_noDuplicates() {
ProblemGenerator gen = new ProblemGenerator(10);
List<Expression> list = gen.generateProblems(20);
assertEquals(20, list.size());
// 所有表达式可被计算且无重复(按 Expression.equals)
Set<Expression> set = new HashSet<>(list);
assertEquals(list.size(), set.size());
ExpressionEvaluator ev = new ExpressionEvaluator();
for (Expression e : list) {
assertNotNull(e.getExpressionString());
assertNotNull(e.getNormalizedString());
assertNotNull(e.getResult());
// 再用 evaluator 计算一次应不抛异常
assertNotNull(ev.evaluate(e.getExpressionString()));
}
}
}size(), set.size());
}
测试结果与测试覆盖率:
测试结果如下:测试函数正确运行通过,对ProblemGenerator
类的测试覆盖率达到100%
5.2.3 答案验证测试
测试目的:验证答案验证功能的正确性
代码:
public class AnswerValidatorTest {
@Test
public void testValidateWritesGradeTxt() throws Exception {
Path dir = Files.createTempDirectory("ag-ans-");
Path ex = dir.resolve("Exercises.txt");
Path an = dir.resolve("Answers.txt");
Path grade = dir.resolve("Grade.txt");
// 1: 正确(3 + 5 = 8); 2: 错误(1/2 + 1/2 = 1, 但答案写成 2)
try (BufferedWriter w = Files.newBufferedWriter(ex)) {
w.write("1. 3 + 5 =\n");
w.write("2. 1/2 + 1/2 =\n");
}
try (BufferedWriter w = Files.newBufferedWriter(an)) {
w.write("1. 8\n");
w.write("2. 2\n");
}
// 在临时目录下运行,生成 Grade.txt
try {
// 切换工作目录(对 JUnit 进程生效有限),改为传绝对路径并重定向输出文件名
// 这里直接调用 validate,Grade.txt 会在进程工作目录下生成,我们随后复制出来比较
AnswerValidator validator = new AnswerValidator();
validator.validate(ex.toString(), an.toString());
} catch (RuntimeException e) {
fail("validate should not throw: " + e.getMessage());
}
// 读取默认生成的 Grade.txt(在项目根)。为避免依赖工作目录,这里查找项目根 Grade.txt
File projectRootGrade = new File("Grade.txt");
assertTrue(projectRootGrade.exists());
String content = Files.readString(projectRootGrade.toPath());
assertTrue(content.contains("Correct: 1"));
assertTrue(content.contains("Wrong: 1"));
}
}
测试结果与测试覆盖率:
测试结果如下:测试函数正确运行通过,对AnswerValidator
类的测试覆盖率达到100%
5.2.4 命令行流程测试
测试目的:验证端到端的生成和验证流程
代码:
public class MainCliTest {
@Before
@After
public void cleanup() throws Exception {
// 清理可能存在的文件,避免相互干扰
for (String name : new String[]{"Exercises.txt", "Answers.txt", "Grade.txt"}) {
File f = new File(name);
if (f.exists()) {
assertTrue(f.delete());
}
}
}
@Test
public void testGenerateAndValidateFlow() throws Exception {
// 生成 10 道题
Main.main(new String[]{"-n", "10", "-r", "10"});
assertTrue(new File("Exercises.txt").exists());
assertTrue(new File("Answers.txt").exists());
// 验证生成的题与答案
Main.main(new String[]{"-e", "Exercises.txt", "-a", "Answers.txt"});
File grade = new File("Grade.txt");
assertTrue(grade.exists());
String content = Files.readString(grade.toPath());
assertTrue(content.contains("Wrong: 0"));
}
@Test
public void testInvalidArgsPrintHelp() {
// 缺少参数
Main.main(new String[]{});
// 仅检查不抛异常并能执行到结束
assertTrue(true);
}
}
测试结果与测试覆盖率:
测试结果如下:测试函数正确运行通过,对所有类的测试覆盖率达到100%
通过以上测试用例,能够全面验证程序的核心功能,包括分数运算、表达式处理、题目生成、去重和答案验证等,确保程序的正确性。
6、项目小结
成功之处
-
模块化设计:将程序分为生成、计算、验证等模块,每个类职责单一,便于维护和扩展。
-
完善的分数处理:实现了完整的分数运算逻辑,支持带分数的表示和转换,符合小学算术的要求。
-
有效的去重机制:通过表达式标准化处理,成功解决了加法和乘法交换律导致的题目重复问题。
-
健壮的命令行处理:支持生成和验证两种模式,参数检查完善,错误提示友好。
不足之处
-
括号生成逻辑简单:目前的括号生成算法比较基础,可能无法生成所有可能的合理括号组合。
-
性能仍有优化空间:生成大量题目(如10000道)时,虽然经过优化,但耗时仍有改进余地。
-
错误处理可以更细致:对于无效表达式的处理可以提供更具体的错误信息。
经验教训
-
前期设计很重要:在开始编码前,充分设计类结构和交互关系,能避免后期大量重构。
-
测试驱动开发:先编写测试用例再实现功能,能更早发现问题,提高代码质量。
-
性能优化需有数据支撑:使用性能分析工具找到真正的瓶颈,避免盲目优化。
-
用户体验细节:命令行参数的提示、文件格式的兼容性等细节处理,能显著提升用户体验。
通过这个项目,我不仅巩固了Java编程技能,还学会了如何分析问题、设计解决方案,并通过测试和性能分析不断改进程序。未来可以考虑增加图形界面、支持更多运算类型等功能,进一步提升程序的实用性。