这个属于哪个课程 | 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是一个对数学逻辑方面比较敏感的人,我们能够互补进行开发,开发节奏快很多