一、前言

1.知识点
(1)面向对象编程:这三道题都要求使用题目类、试卷类、答卷类等封装相应的信息。使用类和对象的概念,通过定义类和创建对象来组织和管理代码,这就要求我们在写程序之前设计好各个类和他们之间的关系。

(2)字符串处理:使用字符串操作方法,例如split(), trim() 来解析输入的数据。格式化输出字符,例如formatWithAnswer 方法中拼接题目和答案。通过简单的正则表达式来解析输入的题目和答案,从中提取题目编号、题目内容和标准答案。

(3)输入输出:使用Scanner类从控制台读取输入。使用System.out.println() 输出信息。

(4)数据结构:使用 HashMap 存储题目信息和标准答案,通过题号作为键,题目内容和答案作为值。使用 ArrayList 存储用户的答案,可以动态扩展。

(5)条件判断:使用 if 语句判断题号是否存在,以避免访问不存在的题目内容和答案。使用条件表达式比较标准答案与用户答案,决定判题结果。

2.题量和难度
(1)答题判题程序1题量相对较小,只需创建三个类,然后处理输入的数据,但仍然需要花大量的时间在类的编写和设计上。题目提供了设计建议,帮助我更好的理解题目并完成代码的设计,难度适中。

(2)答题判题程序2在程序1的基础上增加了一些需求,例如试卷信息和答卷信息并且做出某些警示,逻辑上更加复杂,需要一定的逻辑思考能力。

(3)答题判题程序3在程序2的基础上,增加到五种不同的输入信息,加强了对代码的质量要求,难度进一步增加。这对我来说是难度最大的一道,也是我最后没有拿到满分的一道题,但我从这道题目中学会了很多我没有接触过的知识,收获很大。

二、设计与分析

1.答题判题程序1

  在第一次作业中,我设计了三个类,分别是问题类、试卷类和答题类。

问题类

  包含三个属性,题目编号、题目内容和标准答案,并可以对这些信息进行储存和返回。同时定义了判题方法,使用 equals 方法比较 this.standardAnswer 和处理后的 answer 字符串。部分代码如下:

点击查看代码
private int num; // 题目编号
private String question; // 题目内容
private String standardAnswer; // 标准答案

public boolean checkAnswer(String answer) {//对比答案
	return this.standardAnswer.equals(answer.trim());
}
试卷类

  用于封装试卷信息,管理题目的集合。使用 TreeMap 存储题号和对应的题目对象 Question,确保题目按题号顺序排列。可以方便地添加、查询和统计题目。部分代码如下:

点击查看代码
private TreeMap<Integer, Question> questions; // 用于存储题号对应的题目
public TestPaper() {
	questions = new TreeMap<>(); // 使用TreeMap保证题目按题号顺序
}
答卷类

  用于管理学生的答题情况,包括保存答案、判题和输出结果。引用一个 TestPaper 对象,表示关联的试卷。List answers用于存储学生的答案,类型为 String 列表。List results用于存储每个题目的判题结果。设计判题方法遍历试卷中的题目,对于每个题目,从answers列表中获取对应的答案,并判断答案是否正确,并将结果添加到 results列表,最后输出结果。部分代码如下:

点击查看代码
public void checkAnswers() {
        List<Integer> questionNums = testPaper.getAllQuestionNums();
        for (int i = 0; i < questionNums.size(); i++) {
            Question question = testPaper.getQuestion(questionNums.get(i));
            String answer = answers.get(i);
            boolean result = question.checkAnswer(answer);
            results.add(result);
        }
    }
主函数

  主要功能是读取试卷和学生的答案,然后进行判分。先读取题目数量,然后逐行解读。利用分割字符串的方法,将整行字符串以 # 作为分隔符分割成多个部分。这使得每个部分对应不同的信息,对于每个分割后的部分,使用 split(":") 对这个字符串再以:进行分割,提取出具体的信息保存到数组中。最后解析答案,输出结果。部分代码如下:

点击查看代码
TestPaper testPaper = new TestPaper();

        //读取题目信息
        for (int i = 0; i < questionCount; i++) {
            String line = scanner.nextLine().trim();
            // 解析题目信息 #N:题号 #Q:题目内容 #A:标准答案
            String[] parts = line.split("#");
            int num = Integer.parseInt(parts[1].split(":")[1].trim());
            String questionContent = parts[2].split(":")[1].trim();
            String standardAnswer = parts[3].split(":")[1].trim();
            testPaper.addQuestion(num, questionContent, standardAnswer);
        }

        //创建答卷
        AnswerSheet answerSheet = new AnswerSheet(testPaper);

        //读取答题信息,直到遇到 "end"
        String answerLine = scanner.nextLine().trim();
        if (!answerLine.equals("end")) {
            String[] answerParts = answerLine.split(" ");
            for (String answerPart : answerParts) {
                // 解析答案 #A:答案
                String answer = answerPart.split(":")[1].trim();
                answerSheet.addAnswer(answer);
            }
        }

  一开始接触类的设计,我还没有完全掌握类与类之间的关系,在设计类图时不能正确的找到对应的关系,后来我在网上查找相关资料,发现依赖关系是:1.类A中某个方法的形参是类B类型;2.类A中某个方法的返回类型是类B类型;3.类A中某个方法中的局部变量是类B类型。而关联关系是B类中某个成员变量的类型是A类。最后我明白了这些类之间的关系,设计出了答题判题程序1的类图:

image

2.答题判题程序2

  本道题相较于答题判题程序1,增加了以下需求,程序信息输入由一种变为三种,并且三种信息可能打乱混合输入,新增输入的试卷信息(以“#T”开头的字符串)和答案信息以“#S”开头的字符串),并增加了相应的提示信息。这时之前创建的三个类已经不能解决问题,于是我新增了一个类Test,对新增的试卷信息例如试卷编号,试卷总分等进行相应的操作。

问题类

  我添加了若输入的答案信息少于试卷的题目数量时的判断,没有答案信息的题目输出"answer is null" ,部分代码如下:

点击查看代码
public String formatWithAnswer(String answer, boolean isCorrect) {
        return question + "~" + (answer == null ? "answer is null" : answer) + "~" + (isCorrect ? "true" : "false");
    }

Test 类

  用于管理试卷的基本信息,包括试卷编号、题目及其分数,以及计算总分。它包含了试卷编号、试卷的总分数和一个映射,用于存储题号与对应分数的关系。

点击查看代码
private int testNum; // 试卷编号
private int total; // 试卷总分
private Map<Integer, Integer> questionPoints; // 题号与分数对应
public void addQuestion(int questionNum, int points) {
        questionPoints.put(questionNum, points);
        total += points;
    }
试卷类

  仅将方法addQuestion和getQuestion方法保留。

答卷类

  在保留原来属性的基础上,我还添加了一个 Test对象,代表试卷的具体内容,一个List scores存储每道题的得分,得分对应于用户的答案是否正确。boolean isTestExists标记试卷编号是否存在,默认为 true,表示试卷存在。在原来的判题中,添加打分功能,正确则加上相应的分数,错误则分数为0。部分代码如下:

点击查看代码
public void checkAnswers() {
List<Integer> questionNums = new ArrayList<>(test.getQuestionPoints().keySet());
        for (int i = 0; i < questionNums.size(); i++) {
            Question question = testPaper.getQuestion(questionNums.get(i));
            String answer = i < answers.size() ? answers.get(i) : null;
            boolean result = answer != null && question != null && question.checkAnswer(answer);
            results.add(result);
            if (result) {
                scores.add(test.getQuestionPoints().get(questionNums.get(i)));
            } else {
                scores.add(0);
            }
        }
    }
主函数

  由于新增了输入,在主函数中需要增加判断,首先解析以#N: 开头的行,并将其添加到 testPaper 中。接着解析以#T: 开头的行,提取测试编号和题目信息,创建 Test 对象并存入 tests。同时检查该测试的总分是否为100,并在不符合时添加警告信息。最后解析以#S: 开头的行,提取测试编号并查找对应的 Test。如果找不到,则创建 AnswerSheet 并标记试卷不存在。否则,解析用户的答案并进行检查,最后将答卷添加到 answerSheets 列表中。部分代码如下:

点击查看代码
			// 处理题目信息
            if (line.startsWith("#N:")) {
                String[] parts = line.split("#");
                int num = Integer.parseInt(parts[1].split(":")[1].trim());
                String questionContent = parts[2].split(":")[1].trim();
                String standardAnswer = parts[3].split(":")[1].trim();
                testPaper.addQuestion(num, questionContent, standardAnswer);

            // 处理测试信息
            } else if (line.startsWith("#T:")) {
                String[] parts = line.split(" ");
                int testNum = Integer.parseInt(parts[0].split(":")[1].trim());
                Test test = new Test(testNum);
                
                for (int i = 1; i < parts.length; i++) {
                    String[] questionAndPoints = parts[i].split("-");
                    int questionNum = Integer.parseInt(questionAndPoints[0].trim());
                    int points = Integer.parseInt(questionAndPoints[1].trim());
                    test.addQuestion(questionNum, points);
                }
                
                tests.put(testNum, test);
                if (test.getTotalScore() != 100) {
                    alerts.add("alert: full score of test paper" + testNum + " is not 100 points");
                }

            // 处理答卷信息
            } else if (line.startsWith("#S:")) {
                String[] parts = line.split(" ");
                int testNum = Integer.parseInt(parts[0].split(":")[1].trim());

                Test test = tests.get(testNum);
                if (test == null) {
                    // 如果试卷编号不存在,创建一个新的 AnswerSheet 并标记
                    AnswerSheet answerSheet = new AnswerSheet(testPaper, test);
                    answerSheet.setTestExists(false); // 标记试卷不存在
                    answerSheets.add(answerSheet);
                    continue; // 跳过处理该答卷
                }

                AnswerSheet answerSheet = new AnswerSheet(testPaper, test);
                for (int i = 1; i < parts.length; i++) {
                    if (parts[i].startsWith("#A:")) {
                        String answer = parts[i].split(":")[1].trim();
                        answerSheet.addAnswer(answer);
                    }
                }
                answerSheet.checkAnswers();
                answerSheets.add(answerSheet);
            }
        }

  在第二次设计类图的时候,我便能较快的完成,对类与类之间的关系有了进一步的认识。
image

3.答题判题程序3

  本次题目难度明显增大,添加了更多需求,新增输入的学生信息(以“#X”开头的字符串),答卷信息的格式有所改变,新增删除题目信息,判分信息的格式有所改变,新增各种提示信息,在代码上较前一次有大幅度的修改。而且在输入判断是增加了正则表达式来规定格式。

试卷类

  我添加了用于储存被删除的题目的集合,可以方便地检查某个题目是否已被删除。这个 TestPaper类提供了管理考试题目的基本功能,包括添加、获取、删除题目以及检查题目是否已被删除。

点击查看代码
private Set<Integer> deletedQuestions; // 用于存储删除的题目
public void deleteQuestion(int num) {
		deletedQuestions.add(num);
		questions.remove(num);
	}

	public boolean isDeleted(int num) {
		return deletedQuestions.contains(num);
	}
答卷类

  添加了学生的信息,在打分时添加一个判断即当题目被删除时,也被判为0分,在输出结果时,增加学生的相应信息。这次的每一条信息都要严格按照题目中提到的格式,不符合时要输出“wrong format+输入信息”,所以要熟练的运用正则表达按式。

点击查看代码
public void printAnswers() {
		List<Integer> questionNums = new ArrayList<>(test.getQuestionPoints().keySet());
		for (int i = 0; i < questionNums.size(); i++) {
			Integer questionNum = questionNums.get(i);
			Question question = testPaper.getQuestion(questionNum);
			String answer = (i < answers.size()) ? answers.get(i) : null;
			boolean result = (i < results.size()) ? results.get(i) : false;
			if (answer == null) {
				System.out.println("answer is null");
				continue;
			} else if (testPaper.isDeleted(questionNum)) {
				System.out.println("the question " + questionNum + " invalid~0"); // 输出已删除的题目
				continue;
			} else if (question == null) {
				System.out.println("non-existent question~0"); // 输出不存在的题目
				continue;
			}
			System.out.println(question.formatWithAnswer(answer, result));
		}
	}
主函数

  在第二个作业的基础上,首先判断被删除的题目,然后再处理其他语句。添加或修改问题 (#N:): 使用正则表达式匹配问题内容和标准答案。如果格式正确,则提取信息并调用 addQuestion 方法添加问题。添加试卷 (#T:): 解析试卷信息,包括试卷编号和题目编号及其分值。检查总分是否为 100,并存入 tests 映射中。添加学生信息 (#X:): 解析学生的学号和姓名,并存储在 studentMap 中,以便后续引用。处理答卷 (#S:): 根据输入的试卷编号和学生 ID 创建 AnswerSheet 对象,并将答案记录到对应的答卷中。

  第三次类图的设计与第二次类似。

image

三、踩坑心得

1.在答题判题程序1中,在输出格式方面我犯了许多错误,这是由于我不够细心导致的,最后在仔细对比了我的输出结果与正确输出结果后才得以改正。
image
改正:再说输出结果时,判断是否为最后一个元素,如果是则不要打空格,这样就可以解决输出格式错误的问题了。

点击查看代码
public void printResult() {
        for (int i = 0; i < results.size(); i++) {
            System.out.print(results.get(i) ? "true" : "false");
            // 如果不是最后一个元素,打印空格
            if (i < results.size() - 1) {
                System.out.print(" ");
            }
        }
    }

image

2.在答题判题程序2中,在输入分值不足100分的两份试卷时,在我一开始设计的代码中,我的试卷总分警示与预期的不一样,是在检测到试卷后才输出,这导致了输出不及时。

image

改正:我在主函数中创建了一个列表专门用于存储 alert 信息,最后在输出其他结果之前,先把alert 信息输出。
image

3.在答题判题程序3中出现的问题就多了,导致我最后没能拿到满分。首先我没有考虑到题目,试卷,答卷列表为空的情况,导致出现一连串的非零返回。
image

由于没有很好的测试样例,我不断修改代码却还是无法通过这些测试点。

还有就是,一开始我并没有考虑错误格式的信息输入的情况,导致了很多错误,这时候我就立马想到了正则表达式,并将他运用在代码中,避免了很多错误的发生。
比如说:

点击查看代码
if (line.startsWith("#N:")) {
    // 修改正则表达式,使标准答案部分可选
    String regex = "^#N:\\s*(\\d+)\\s+#Q:\\s*(.*?)\\s+#A:\\s*(.*?)\\s*$|^#N:\\s*(\\d+)\\s+#Q:\\s*(.*?)\\s+$";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(line);
    if (matcher.matches()) {
        try {
            int num = Integer.parseInt(matcher.group(1) != null ? matcher.group(1) : matcher.group(4).trim()); // 获取问题编号
            String questionContent = matcher.group(2) != null ? matcher.group(2).trim() : matcher.group(5).trim(); // 获取问题内容
            String standardAnswer = matcher.group(3) != null ? matcher.group(3).trim() : null; // 获取标准答案,若不存在则为null
            testPaper.addQuestion(num, questionContent, standardAnswer);
        } catch (NumberFormatException e) {
            System.out.println("Invalid number format in line: " + line);
        }
    } else {
        System.out.println("wrong format:" + line);
    }
}

还有乱序输入的问题,这次的答卷中答题的顺序可以打乱,但判题仍是按照试卷中的题目顺序,这个在最后判题并输出时需要注意顺序问题。这个问题也困扰我很久。

四、改进建议

1.输入时数据时对字符串进行更加严格的格式判断。确保输入的内容符合预期格式,避免因输入内容格式错误导致的异常或错误情况的发生。我经常因为输入格式的错误而浪费了很多时间来修正,所以应该在一开始的时候就严格判断格式。

2.我的代码出现的一个比较严重的问题就是,没有很好地进行类的设计。在主函数中放入大量输入的字符串的解析方法,这位违背了面向对象程序设计的单一职责原则。我也尝试将这个方法封装到一个类中,但当时时间已经来不及了。所以在一开始设计的时候,我就应该意识到这一点,将主函数的一大坨处理操作放到各个类里面去,尽量不要在主函数里进行过多的不是输入输出的操作。

3.更加熟练度运用正则表达式,这样可以提示我的代码的效率,所以我要更加学习好正则表达式。

4.在代码中多增加批注,防止出现过了几天自己也不熟悉代码内容的情况。

五、总结

  本次三个程序的主要目标是设计和实现一个小型的答题判题系统,模拟了从题目录入、试卷组卷、学生答题、判题、以及删除题目信息的全过程。通过这几次的编程,我不仅要掌握复杂的数据结构和逻辑关系处理,还需要提升对程序的设计编码、调试能力。
  在本次实验中,我接触到了多种信息输入类型(如题目、试卷、学生、答卷等),这要求我合理设计数据结构,以便高效存储和快速检索。这一过程帮助我理解了如何在程序中设计逻辑结构以应对复杂的业务需求。实验过程中,我通过多种异常情况的设计与测试(如题目编号缺失、引用错误、格式错误等),进一步强化了对异常处理机制的理解。这对于提升程序的健壮性非常关键。由于本实验要求处理多种格式化输入信息,我深入学习了正则表达式的应用,掌握了如何使用它进行格式匹配和数据解析。而且这三次程序输出格式要求非常严格,稍有不慎就会导致输出不符合题目要求。需要通过多次测试和修改,逐步完善了程序的输出格式。实现过程中,由于功能模块多且逻辑复杂,我的代码可能存在一定的冗余。在后续学习中,我觉得我应该尝试将判题、计分等功能模块化,提高代码的可读性和可维护性。
  我认为在接下来的学习过程中,我更应该落到实处,不仅要听懂怎么写代码,更要自己动手,去实践,只有这样我才是真正理解了。这三次的大作业对我来说还是存在一定的难道,尤其是第三次,我没能拿到满分,许多测试点不知道怎么修改,并且在代码设计上有许多需要改进的地方。
  通过这次反思,希望我能更好地完成以后的每一次大作业。