这个属于哪个课程 23计科34班
这份作业的要求在哪里 结对项目
这个作业的目标 学会怎样去结伴实现一个完整的项目
成员1 颜嘉盈 3123004500 https://github.com/dududu1012/dududu1012/tree/main/3123004500/Mathtraining
成员2 徐粤 3123004806 https://github.com/xuuuyueyue/3123004806/

一、PSP表格(预估耗时与实际耗时)

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 90 90
Esitimate 估计这个任务需要多少时间 45 60
Development 开发 240 300
Analysis 需求分析(包括学习新技术) 60 60
Design Spec 生成设计文档 60 60
Design Review 设计复审 60 60
Coding Standard 代码规范(为目前的开发制定合适的规范) 60 60
Design 具体设计 60 60
Coding 具体代码 60 60
Code Review 代码复审 60 60
Test 测试(自我测试、修改代码、提交修改) 60 90
Reporting 报告 60 60
Test Repor 测试报告 60 60
Size measurement 计算工作量 10 10
Postmortem&Process Improvement Plan 事后总结,并提出过程改进计划 30 20
合计 1015 1110

二、效能分析

3.1改进性能花费时间:1.5小时

3.2性能分析图:


图中可以看出我的消耗最大函数是generateQuestionAndAnswers函数

3.3改进思路:

3.3.1:

  • 一、generateExpression(表达式生成):减少无效尝试,优化随机数生成
    问题分析:
    1.生成表达式时可能因 “减法结果为负”“除法结果非真分数”“表达式重复” 导致频繁重试,浪费CPU资源。
    2.随机数生成(Random调用)和字符串拼接(StringBuilder频繁创建)耗时。
    改进思路:
    1.预生成数字池,减少随机数调用:提前生成一批符合范围的整数、分数(按比例),存储在列表中,生成表达式时直接从池中随机选取,避免重复调用Random:
// 预生成数字池(初始化时调用)
private static List<String> preGenerateNumbers(int range, int poolSize) {
    List<String> pool = new ArrayList<>(poolSize);
    Random random = new Random();
    for (int i = 0; i < poolSize; i++) {
        pool.add(generateNumber(range, random)); // 复用原generateNumber逻辑
    }
    return pool;
}

2.生成表达式时从池中随机取数,减少Random实例化和随机数计算开销。
优化减法 / 除法的数字匹配逻辑:

  • 减法:生成被减数后,只从“小于被减数” 的数字池中选减数(提前过滤,避免while循环重试)。
  • 除法:生成被除数后,只从“能使结果为真分数” 的数字池中选除数(通过预计算筛选,减少重试)。
  • 减少字符串拼接开销:用 StringBuilder 替代字符串拼接(+),并复用同一个StringBuilder实例(通过setLength(0)清空),避免频繁创建对象:
private static final StringBuilder exprBuilder = new StringBuilder();
private static String generateExpression(...) {
    exprBuilder.setLength(0); // 复用,不清空会导致内存泄漏
    exprBuilder.append(parts.get(0));
    // 追加运算符和数字...
    return exprBuilder.toString();
}
  • 二、normalizeExpression(表达式去重):减少正则与排序开销
    问题分析:
    1.正则移除括号(replaceAll("\(|\)", ""))和对加法/乘法排序的字符串操作耗时。
    2.标准化后的表达式存储在HashSet中,字符串哈希计算开销大。
    改进思路:
    1.手动移除括号,替代正则:用StringBuilder遍历字符,跳过 (和),比正则替换更高效:
private static String removeParentheses(String expr) {
    StringBuilder sb = new StringBuilder();
    for (char c : expr.toCharArray()) {
        if (c != '(' && c != ')') {
            sb.append(c);
        }
    }
    return sb.toString();
}

2.用 “表达式特征值” 替代完整字符串去重:对标准化后的表达式计算哈希值(如 Long 类型),存储哈希值到HashSet,减少内存占用和哈希计算开销:

private static long getExpressionHash(String normalizedExpr) {
    // 简化:用字符串哈希值(或自定义哈希算法)
    return normalizedExpr.hashCode();
}
// 去重时只需判断哈希值是否存在
Set<Long> exprHashSet = new HashSet<>();
long hash = getExpressionHash(normalizedExpr);
if (exprHashSet.contains(hash)) { ... }
  • 三、calculateExpression(表达式计算):优化分数运算与括号处理
    问题分析:
    1.递归处理括号时频繁创建字符串(如替换子表达式结果),导致内存分配和GC开销。
    2.分数化简(gcd计算)在高频调用时耗时累积。
    改进思路:
    1.用索引替代字符串替换(括号处理优化):避免递归替换括号内的子表达式为字符串,改用 “索引区间” 标记括号范围(如记录 (start,end)),直接在原表达式数组上计算,减少字符串操作:
// 示例:用数组存储表达式元素,标记括号位置
String[] parts = expr.split(" "); // ["(", "1", "+", "2", ")", "×", "3"]
int start = 0, end = 4; // 括号范围:从索引0到4
// 直接计算parts[start+1..end-1],结果存入parts[start],再删除中间元素

2.缓存gcd计算结果:分数化简依赖最大公约数(gcd),对高频出现的数字对(如2和4、3和6)缓存gcd结果,避免重复计算:

private static final Map<Long, Integer> gcdCache = new HashMap<>();
private static int gcd(int a, int b) {
    a = Math.abs(a);
    b = Math.abs(b);
    long key = ((long) Math.max(a, b) << 32) | Math.min(a, b); // 生成唯一键
    if (gcdCache.containsKey(key)) {
        return gcdCache.get(key);
    }
    // 原gcd计算逻辑...
    int result = ...;
    gcdCache.put(key, result);
    return result;
}

注意:缓存大小需限制,否则内存溢出

三、计算模块接口的设计与实现过程

1.代码组织结构设计: 本项目主要由一个公共类 MathTrainingSystem 和一个私有静态内部类 Fraction 构成,遵循了高内聚、低耦合的设计原则。

  • MathTrainingSystem:核心控制类,包含了程序的所有主要逻辑、命令行参数处理、文件 I/O、表达式生成、答案校验等功能。
  • Fraction (内部类):用于表示和处理分数(包括自然数和带分数)的自定义数据结构,简化了分数运算和格式化。

2.函数划分:

模块 核心函数 功能描述
参数解析 main、printHelp 解析命令行参数(如-n生成题目、-a输入答案),输出帮助信息
题目生成 generateQuestionsAndAnswers、generateExpression、addParentheses 生成含整数/分数/带分数的四则运算题,支持括号和去重,自动计算标准答案
分数运算 parseNumber、simplifyFraction、add/subtract/multiply/divide 解析数字(整数/分数/带分数),实现分数运算与化简,确保结果格式统一
答案处理 inputUserAnswers、readFromFile、writeToFile 引导用户输入答案并保存到文件,提供文件读写工具方法
校验比对 checkAnswers、parseAnswerLine、compareFractions 比对用户答案与标准答案,生成Grade.txt统计结果

3.模块关系图:

参数解析(main)→ 题目生成(generateQuestionsAndAnswers)→ 生成 Exercises.txt 和 Answers.txt  
                    ↓  
参数解析(main)→ 答案输入(inputUserAnswers)→ 生成用户答案文件(如 MyAnswers.txt)  
                    ↓  
参数解析(main)→ 校验比对(checkAnswers)→ 读取 Answers.txt 和用户答案 → 生成 Grade.txt  

4.关键函数流程图:

四、代码说明

1.题目生成:

// 生成题目并自动生成标准答案
private static void generateQuestionsAndAnswers(int count, int range) throws IOException {
    List<String> exercises = new ArrayList<>();
    List<String> answers = new ArrayList<>();
    Set<String> uniqueExpressions = new HashSet<>(); // 用于存储标准化后的表达式,实现去重

    Random random = new Random();
    int generated = 0;
    int maxAttempts = count * 10; // 最多尝试生成10倍数量的题目,避免死循环
    int attempts = 0;

    while (generated < count && attempts < maxAttempts) {
        attempts++;
        int opCount = random.nextInt(3) + 1; // 随机生成1-3个运算符
        String expression = generateExpression(opCount, range, random);

        if (expression == null) continue;

        String normalized = normalizeExpression(expression); // 标准化表达式,用于去重
        if (uniqueExpressions.contains(normalized)) continue;

        Fraction result = calculateExpression(expression);
        if (result == null || result.numerator < 0) continue;

        int questionNum = generated + 1;
        exercises.add(questionNum + ". " + expression + " =");
        answers.add(questionNum + ". " + formatFraction(result));
        
        uniqueExpressions.add(normalized);
        generated++;
    }

    writeToFile("Exercises.txt", exercises);
    writeToFile("Answers.txt", answers);
}

设计思路:通过循环不断尝试生成表达式,每次生成后先标准化表达式以去重,再校验结果是否有效(非负、无错误),只有通过所有校验的题目才会被保留。循环持续到生成足够数量的题目或达到最大尝试次数,最终将有效题目和对应标准答案分别写入文件
2.分数运算核心类(Fraction 及相关方法):

// 分数类,封装分子和分母,支持分数的四则运算与化简
private static class Fraction {
    int numerator;   // 分子
    int denominator; // 分母

    Fraction(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }
}

// 解析数字(整数、真分数、带分数)
private static Fraction parseNumber(String str) {
    // 带分数匹配:如 "2'3/8"
    Matcher fractionMatcher = FRACTION_PATTERN.matcher(str);
    if (fractionMatcher.matches()) {
        int integerPart = fractionMatcher.group(1) != null ? Integer.parseInt(fractionMatcher.group(1)) : 0;
        int numerator = Integer.parseInt(fractionMatcher.group(2));
        int denominator = Integer.parseInt(fractionMatcher.group(3));
        return new Fraction(integerPart * denominator + numerator, denominator);
    } 
    // 整数匹配:如 "5"
    else if (INTEGER_PATTERN.matcher(str).matches()) {
        return new Fraction(Integer.parseInt(str), 1);
    } else {
        throw new IllegalArgumentException("无效的数字格式:" + str);
    }
}

// 简化分数(约分)
private static Fraction simplifyFraction(Fraction f) {
    int gcd = gcd(Math.abs(f.numerator), f.denominator);
    return new Fraction(f.numerator / gcd, f.denominator / gcd);
}

// 分数加法
private static Fraction add(Fraction a, Fraction b) {
    int numerator = a.numerator * b.denominator + b.numerator * a.denominator;
    int denominator = a.denominator * b.denominator;
    return simplifyFraction(new Fraction(numerator, denominator));
}

// 分数减法(确保结果非负)
private static Fraction subtract(Fraction a, Fraction b) {
    if (compareFractions(a, b) < 0) return null; // 结果为负则返回null
    int numerator = a.numerator * b.denominator - b.numerator * a.denominator;
    int denominator = a.denominator * b.denominator;
    return simplifyFraction(new Fraction(numerator, denominator));
}

设计思路:

  • Fraction类是分数运算的基础,用分子和分母统一表示整数、分数等所有数值,为运算提供一致的格式
  • parseNumber方法通过正则匹配,将整数、真分数、带分数等不同格式的输入,统一转换为Fraction对象,消除格式差异
  • 四则运算严格遵循分数运算规则,运算后调用simplifyFraction约分,确保结果为最简形式

3.答案检验:

// 校验答案:比对用户答案与标准答案
private static void checkAnswers(String exerciseFile, String userAnswerFile) {
    List<String> reportLines = new ArrayList<>();
    try {
        List<String> standardAnswers = readFromFile("Answers.txt");
        List<String> userAnswers = readFromFile(userAnswerFile);

        if (standardAnswers.size() != userAnswers.size()) {
            throw new IllegalArgumentException("答案数量不匹配");
        }

        List<Integer> correctList = new ArrayList<>();
        List<Integer> wrongList = new ArrayList<>();

        for (int i = 0; i < standardAnswers.size(); i++) {
            Fraction standardFraction = parseAnswerLine(standardAnswers.get(i), "标准答案", i + 1);
            Fraction userFraction = parseAnswerLine(userAnswers.get(i), "用户答案", i + 1);
            int questionNum = extractQuestionNumber(standardAnswers.get(i), i + 1);

            if (compareFractions(standardFraction, userFraction) == 0) {
                correctList.add(questionNum);
            } else {
                wrongList.add(questionNum);
            }
        }

        reportLines.add("Correct: " + correctList.size() + " (" + join(correctList) + ")");
        reportLines.add("Wrong: " + wrongList.size() + " (" + join(wrongList) + ")");
    } catch (Exception e) {
        reportLines.add("Error: " + e.getMessage());
    } finally {
        writeToFile("Grade.txt", reportLines);
    }
}

// 比较两个分数的大小
private static int compareFractions(Fraction a, Fraction b) {
    long left = (long) a.numerator * b.denominator;
    long right = (long) b.numerator * a.denominator;
    return Long.compare(left, right);
}

设计思路:

  • checkAnswers方法负责读取标准答案和用户答案文件,逐题比对答案。将用户答案和标准答案都解析为Fraction对象后,调用compareFractions方法比较大小,判断是否一致
  • 最后按照指定格式生成包含正确和错误题目编号的报告,写入Grade.txt文件

五、测试运行

测试用例1:

测试目的:验证generateQuestionsAndAnswers方法能否正确生成指定数量、符合格式的题目和答案文件

@Test
    public void testGenerateBasicQuestions() throws IOException {
        MathTrainingSystem.generateQuestionsAndAnswers(3, 5);
        List<String> exercises = readFile(EXERCISES);
        List<String> answers = readFile(ANSWERS);

        assertEquals(3, exercises.size());
        assertEquals(3, answers.size());
        for (String ex : exercises) {
            assertTrue(ex.matches("\\d+\\. .+ ="));
        }
    }

测试结果正确依据:

  • 生成的Exercises.txt和Answers.txt行数均为3(与参数count=3一致)
  • 题目格式符合"X.表达式="(如"1.2+3=),答案格式符合"X. 结果"(如"1.5"),说明生成逻辑正确

测试用例2:

测试目的:验证parseNumber方法能否正确解析整数、真分数、带分数为Fraction对象

// 测试2:分数解析(整数、真分数、带分数)
    @Test
    public void testFractionParsing() {
        MathTrainingSystem.Fraction f1 = MathTrainingSystem.parseNumber("7");
        assertEquals(7, f1.numerator);
        assertEquals(1, f1.denominator);

        MathTrainingSystem.Fraction f2 = MathTrainingSystem.parseNumber("3/4");
        assertEquals(3, f2.numerator);
        assertEquals(4, f2.denominator);

        MathTrainingSystem.Fraction f3 = MathTrainingSystem.parseNumber("2'1/3");
        assertEquals(7, f3.numerator); // 2×3+1=7
        assertEquals(3, f3.denominator);
    }

测试结果正确依据:

  • 解析整数"7"得到Fraction(7,1),符合整数的分数表示(分子=整数,分母=1)
  • 解析真分数"3/4"得到Fraction(3,4),分子分母与输入一致
  • 解析带分数"2'1/3"得到Fraction(7,3)(计算:2×3+1=7),符合带分数转假分数的规则

测试用例3:

测试目的:验证add方法对分数加法的计算正确性

// 测试3:分数加法
    @Test
    public void testFractionAddition() {
        MathTrainingSystem.Fraction a = new MathTrainingSystem.Fraction(1, 2);
        MathTrainingSystem.Fraction b = new MathTrainingSystem.Fraction(1, 3);
        MathTrainingSystem.Fraction sum = MathTrainingSystem.add(a, b);

        assertEquals(5, sum.numerator);
        assertEquals(6, sum.denominator);
    }

测试结果正确性:

  • 计算1/2+1/3,按分数加法规则:通分后3/6+2/6=5/6,实际结果为Fraction(5,6),与预期一致

测试用例4:

测试目的:验证subtract方法对分数减法的计算正确性(且结果非负)

// 测试4:分数减法(结果非负)
    @Test
    public void testFractionSubtraction() {
        MathTrainingSystem.Fraction a = new MathTrainingSystem.Fraction(5, 6);
        MathTrainingSystem.Fraction b = new MathTrainingSystem.Fraction(1, 3);
        MathTrainingSystem.Fraction diff = MathTrainingSystem.subtract(a, b);

        assertEquals(1, diff.numerator);
        assertEquals(2, diff.denominator);
    }

测试结果正确性:

  • 计算5/6-1/3,通分后5/6-2/6 =3/6=1/2,实际结果为Fraction(1,2),符合计算规则
  • 方法内部通过compareFractions确保被减数≥减数,避免负数结果,逻辑正确

测试用例5:

测试目的:验证multiply方法对分数乘法的计算正确性(含化简)

 // 测试5:分数乘法
    @Test
    public void testFractionMultiplication() {
        MathTrainingSystem.Fraction a = new MathTrainingSystem.Fraction(2, 3);
        MathTrainingSystem.Fraction b = new MathTrainingSystem.Fraction(3, 4);
        MathTrainingSystem.Fraction product = MathTrainingSystem.multiply(a, b);

        assertEquals(1, product.numerator);
        assertEquals(2, product.denominator);
    }

测试结果正确性:

  • 计算2/3×3/4,按规则:(2×3)/(3×4) = 6/12,化简后为1/2,实际结果为Fraction(1,2),与预期一致
  • 方法调用simplifyFraction进行约分,确保结果为最简分数

测试用例6:

测试目的:验证divide方法对分数除法的计算正确性(含化简和除数非零校验)

// 测试6:分数除法(除数非零)
    @Test
    public void testFractionDivision() {
        MathTrainingSystem.Fraction a = new MathTrainingSystem.Fraction(1, 2);
        MathTrainingSystem.Fraction b = new MathTrainingSystem.Fraction(3, 4);
        MathTrainingSystem.Fraction quotient = MathTrainingSystem.divide(a, b);

        assertEquals(2, quotient.numerator);
        assertEquals(3, quotient.denominator);
    }

测试结果正确性:

  • 计算1/2 ÷ 3/4,按规则:乘以倒数得(1×4)/(2×3)=4/6,化简后为2/3,实际结果为Fraction(2,3),与预期一致
  • 方法通过if(b.numerator==0)校验除数非零,避免运行时错误

测试用例7:

测试目的:验证checkAnswers方法对 “全对答案” 的校验正确性

// 测试7:答案全对校验
    @Test
    public void testAllCorrectAnswers() throws IOException {
        List<String> stdAnswers = Arrays.asList(
                "1. 3/2",
                "2. 5",
                "3. 1'1/3"
        );
        List<String> userAnswers = Arrays.asList(
                "1. 3/2",
                "2. 5",
                "3. 4/3"
        );
        writeFile(ANSWERS, stdAnswers);
        writeFile(USER_ANSWERS, userAnswers);

        MathTrainingSystem.checkAnswers("", USER_ANSWERS);
        List<String> grade = readFile(GRADE);

        assertEquals("Correct: 3 (1, 2, 3)", grade.get(0));
        assertEquals("Wrong: 0 ()", grade.get(1));
    }

测试结果正确性:

  • 标准答案为["1.3/2","2.5","3.1'1/3"],用户答案为等价形式(如3.4/3与1'1/3等价)
  • 校验后Grade.txt中Correct: 3 (1,2,3),Wrong: 0 (),说明等价答案被正确识别

测试用例8:

测试目的:验证checkAnswers方法对 “全错答案” 的校验正确性

// 测试8:答案全错校验
    @Test
    public void testAllWrongAnswers() throws IOException {
        List<String> stdAnswers = Arrays.asList("1. 2/3");
        List<String> userAnswers = Arrays.asList("1. 3/2");
        writeFile(ANSWERS, stdAnswers);
        writeFile(USER_ANSWERS, userAnswers);

        MathTrainingSystem.checkAnswers("", USER_ANSWERS);
        List<String> grade = readFile(GRADE);

        assertEquals("Correct: 0 ()", grade.get(0));
        assertEquals("Wrong: 1 (1)", grade.get(1));
    }

测试结果正确性:

  • 标准答案为["1.2/3"],用户答案为["1.3/2"](明显错误)
  • 校验后Grade.txt中Correct:0(),Wrong:1(1),说明错误答案被正确识别

测试用例9:

测试目的:验证checkAnswers方法在 “答案数量不匹配” 时能否抛出IllegalArgumentException

// 测试9:答案数量不匹配异常
    @Test(expected = IllegalArgumentException.class)
    public void testAnswerCountMismatch() throws IOException {
        List<String> stdAnswers = Arrays.asList("1. 1", "2. 2", "3. 3");
        List<String> userAnswers = Arrays.asList("1. 1", "2. 3");
        writeFile(ANSWERS, stdAnswers);
        writeFile(USER_ANSWERS, userAnswers);

        MathTrainingSystem.checkAnswers("", USER_ANSWERS);
    }

测试结果正确性:

  • 标准答案3道,用户答案2道,触发if(standardAnswers.size()!= userAnswers.size())条件。
  • 方法抛出IllegalArgumentException,被测试用例的@Test(expected = ...)捕获,测试通过,说明异常处理逻辑正确

测试用例10:

测试目的:验证calculateExpression方法对带括号的复杂表达式的计算正确性

// 测试10:带括号表达式计算
    @Test
    public void testParenthesesExpression() {
        String expr = "(1 + 1/2) × 2";
        MathTrainingSystem.Fraction result = MathTrainingSystem.calculateExpression(expr);

        assertEquals(3, result.numerator);
        assertEquals(1, result.denominator);
    }

测试结果正确性:

  • 计算(1+1/2)×2,先算括号内3/2,再乘以2得3,实际结果为Fraction(3,1),与手工计算一致
  • 方法通过递归处理括号,确保运算优先级正确,逻辑有效

六、项目小结

一、成败得失
(一)成功之处

  • 核心功能落地:实现题目生成(去重、范围可控)、答案输入、校验评分全流程,支持整数/分数/带分数运算。
  • 异常处理完善:拦截除数为零、负数结果等问题,避免程序崩溃,提升鲁棒性。
  • 协作有成效:按模块拆分任务,交叉Review代码,减少逻辑漏洞(如修复分数化简遗漏问题)。
    (二)不足
  • 去重逻辑局限:未覆盖 “不同括号格式的等价表达式”(如(1+2)+3与1+2+3),极端场景仍有重复。
  • 异常与测试衔接差:checkAnswers初始用try-catch吞掉异常,导致测试用例无法检测,浪费调试时间。
  • 协作分工粗:未细化子任务,出现代码冲突;未统一命名规范,增加理解成本。

二、经验分享

  • 技术设计:先明确规则(如分数转假分数公式、表达式约束),再写代码,避免反复修改。
  • 协作流程:小步验证,每日同步小功能(如当天完成 “分数加法 + 表达式生成”),即时联调,减少后期整合问题。
  • 测试设计:覆盖正常、边界、异常场景(如题目数量 10000、答案不匹配),用测试发现隐藏问题(如除法未化简)。

三、教训总结

  • 复杂逻辑先写 Demo:如括号计算、表达式去重,先验证可行性再集成,避免多层嵌套报错。
  • 提前约定技术规范:统一代码命名、版本控制、测试用例规则,减少冲突和理解成本。
  • 异常与测试同步设计:开发异常处理时,同步写对应测试用例,避免后期返工。

四、结对感受
成员1:成员2是一个很有耐心的人,总是会给我一些比较好的灵感以及鼓励,而且在算法方面比我出色
成员2:成员1是一个对数学逻辑方面比较敏感的人,我们能够互补进行开发,开发节奏快很多