软件工程第三次作业

软件工程第三次作业

项目 内容
这个作业属于哪个课程 软件工程
这个作业要求在哪里 作业要求
这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序,并能检验题目答案正确性

基本信息

代码仓库: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 性能瓶颈与优化思路

  1. 初始问题:生成大量题目时,由于频繁的表达式判重和结果计算,导致性能下降,生成10000道题需要约8秒。

  2. 优化措施

    • 改进Expression类的标准化算法,减少递归调用次数
    • ProblemGenerator中的随机生成逻辑进行调整,减少无效重试
    • Fraction类中缓存计算结果,避免重复约分操作
  3. 优化效果

    • 生成10000道题的时间从8秒减少到3.2秒,性能提升60%
    • 内存占用降低约35%

2.2 性能分析图

2.2.1 CPU时间

  • 火焰图
    image-20251021205222889

  • 调用树
    image-20251021204709781

2.2.2 内存分配

  • 火焰图
    image-20251021205122863

  • 调用树
    image-20251021205328299

程序中消耗最大的函数是ProblemGenerator.generateExpression(),主要原因是需要频繁生成表达式并验证其有效性,尤其是在处理括号和去重逻辑时开销较大。

2.2.3 概览

image-20251021210157705

3、设计实现过程

3.1 代码组织结构

本项目采用模块化设计,主要包含以下几个核心类:

  1. Main:程序入口,解析命令行参数,驱动生成或验证流程
  2. ProblemGenerator:负责生成符合要求的四则运算题目
  3. Expression:封装表达式及其标准化形式,用于判重
  4. ExpressionEvaluator:解析并计算表达式的值
  5. Fraction:处理分数的表示和运算
  6. AnswerValidator:验证题目答案的正确性并生成评分

image-20251021210343470

类之间的关系如下:

mermaid-diagram-2025-10-21-222706

3.2 关键函数流程图

1. 表达式生成流程(generateExpression

mermaid-diagram-2025-10-21-211629

2. 表达式标准化流程(normalize

mermaid-diagram-2025-10-21-211714

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/52'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)转为后缀表达式(逆波兰式),并计算结果。

计算流程

  1. 中缀转后缀:
    • 用栈处理运算符优先级(×÷优先级高于+-)。
    • 处理括号:左括号入栈,右括号弹出栈顶运算符直到左括号。
    • 示例:(3+5)×2 转为 3 5 + 2 ×
  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,结果必为真分数(分子绝对值 < 分母)。
  • 去重逻辑:通过ExpressionnormalizedString判断,确保加法 / 乘法交换律下的表达式视为重复(如3+55+3)。

生成流程

  1. 生成操作数:50% 概率生成自然数,50% 概率生成真分数(含带分数,如2'3/8)。
  2. 拼接表达式:按运算符数量拼接操作数与运算符,实时验证减法 / 除法约束。
  3. 随机加括号:30% 概率不加括号,其余情况随机选择子表达式添加括号(如(3+5)×2)。
  4. 去重检查:通过Expressionequals方法(基于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. 读取文件:
    • 题目文件:提取表达式(移除题号和=,如1. 3+5=提取为3+5)。
    • 答案文件:提取答案(移除题号,如1. 8提取为8)。
  2. 比对答案:
    • 计算题目表达式的正确结果(通过ExpressionEvaluator)。
    • 解析用户答案为Fraction,与正确结果比较。
  3. 生成结果:按格式写入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.txtAnswers.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如下,结果符合预期

image-20251021212453401

  • 将生成答案中的第3、7道答案改错,得到判题结果如下,符合预期结果

image-20251021212602253

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());
    }
}

测试结果与测试覆盖率:

测试结果如下:测试函数正确运行通过,对 FractionExpressionEvaluator类的测试覆盖率达到100%

image-20251021215022582

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%

image-20251021213420926

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%

image-20251021213710749

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%

image-20251021213917043

通过以上测试用例,能够全面验证程序的核心功能,包括分数运算、表达式处理、题目生成、去重和答案验证等,确保程序的正确性。

6、项目小结

成功之处

  1. 模块化设计:将程序分为生成、计算、验证等模块,每个类职责单一,便于维护和扩展。

  2. 完善的分数处理:实现了完整的分数运算逻辑,支持带分数的表示和转换,符合小学算术的要求。

  3. 有效的去重机制:通过表达式标准化处理,成功解决了加法和乘法交换律导致的题目重复问题。

  4. 健壮的命令行处理:支持生成和验证两种模式,参数检查完善,错误提示友好。

不足之处

  1. 括号生成逻辑简单:目前的括号生成算法比较基础,可能无法生成所有可能的合理括号组合。

  2. 性能仍有优化空间:生成大量题目(如10000道)时,虽然经过优化,但耗时仍有改进余地。

  3. 错误处理可以更细致:对于无效表达式的处理可以提供更具体的错误信息。

经验教训

  1. 前期设计很重要:在开始编码前,充分设计类结构和交互关系,能避免后期大量重构。

  2. 测试驱动开发:先编写测试用例再实现功能,能更早发现问题,提高代码质量。

  3. 性能优化需有数据支撑:使用性能分析工具找到真正的瓶颈,避免盲目优化。

  4. 用户体验细节:命令行参数的提示、文件格式的兼容性等细节处理,能显著提升用户体验。

通过这个项目,我不仅巩固了Java编程技能,还学会了如何分析问题、设计解决方案,并通过测试和性能分析不断改进程序。未来可以考虑增加图形界面、支持更多运算类型等功能,进一步提升程序的实用性。

posted @ 2025-10-21 22:30  fz6668  阅读(8)  评论(0)    收藏  举报