JAVA 前三次题目集总结

flysusdjd / 2024-11-11 / 原文

在过去的一个月里完成了java的前三次大作业对于JAVA的语法以及面向对象编程还不台上手,接下来说前三次大作业。
前三次大作业要是围绕答题判题系统展开的每次作业都在完善这个程序的功能可以说
1.第一次作业判分功能
在第一次作业阶段,核心任务是建立一个能够接收题目信息和答题信息,并能够进行基本判分的系统。
• 核心功能:
题目处理: 通过 Question 类存储题目内容和正确答案,为判分提供基础。
答卷处理: AnswerSheet 类用于收集学生答案,包括学生ID和他们的答案。
判分逻辑: 对学生提交的答案与标准答案进行对比,判断答题的正确性。
• 技术实现:
使用Java的数据结构如 Map 和 List 来存储题目和答案。
对输入的处理采用简单的字符串解析技术,以提取关键信息。
• 挑战与解决方案:
初始阶段主要挑战是确保所有题目和答案能正确录入并能进行准确判分。
解决方案是通过单元测试确保每部分输入正确处理和输出预期结果。
2.第二次作业管理与总分校验
第二次作业引入试卷的概念,增加了试卷与题目的关联管理以及试卷总分的验证。
• 新增功能:
试卷类的引入: TestPaper 类管理一张试卷中的题目和各题的分值。
总分验证: 检查每张试卷的总分是否为100分,如果不是则发出警告。
• 技术深化:
扩展了对输入的解析,包括处理更复杂的字符串格式和关联数据。
引入逻辑判断试卷总分是否合规。
• 挑战与解决方案:
确保试卷中的题目与题库中题目正确关联,并正确计算总分。
通过逐项添加题目到试卷并实时计算总分来解决问题。
3.第三次作业的错误管理和数据完整性
第三次作业不仅增加了对学生信息的管理,还增强了对错误和数据完整性的管理。
• 扩展功能:
学生信息管理: 增加对学生的信息处理,使得每张答卷都能与具体学生关联。
题目删除功能: 允许从系统中删除题目,即使这些题目已经被试卷引用。
• 健壮性提升:
引入错误处理机制,如输入格式错误、不存在的试卷号或学生号等。
对被删除的题目在输出时提供特别提示,如"题目无效"。
• 挑战与解决方案:
处理输入的复杂性和数据的一致性问题,如确保即使题目被删除,相关的试卷仍能正确处理。
采用数据验证和异常处理策略,增强系统的健壮性和用户的错误反馈。

代码分析
第一次作业:
类图:

顺序图:

解析输入逻辑:parseInput 方法

    try {
        if (line.startsWith("#N:")) {
            // 解析题目信息
            String[] parts = line.split(" #Q:| #A:");
            int questionId = Integer.parseInt(parts[0].substring(3));
            String questionContent = parts[1];
            String answer = parts[2];
            questions.put(questionId, new Question(questionContent, answer));
        } else if (line.startsWith("#T:")) {
            // 解析试卷信息
            String[] parts = line.substring(3).split(" ");
            int paperId = Integer.parseInt(parts[0]);
            Paper paper = new Paper(paperId);
            for (int i = 1; i < parts.length; i++) {
                String[] questionParts = parts[i].split("-");
                int questionId = Integer.parseInt(questionParts[0]);
                int score = Integer.parseInt(questionParts[1]);
                paper.addQuestion(questionId, score);
            }
            papers.put(paperId, paper);
        } else if (line.startsWith("#X:")) {
            // 解析学生信息
            String[] studentInfos = line.substring(3).split("-");
            for (String studentInfo : studentInfos) {
                String[] parts = studentInfo.split(" ");
                String studentId = parts[0];
                String name = parts[1];
                students.put(studentId, new Student(name));
            }
        } else if (line.startsWith("#S:")) {
            // 解析答卷信息
            String[] parts = line.split(" ");
            int paperId = Integer.parseInt(parts[0].substring(3));
            String studentId = parts[1];
            AnswerSheet answerSheet = new AnswerSheet(paperId, studentId);
            for (int i = 2; i < parts.length; i++) {
                if (parts[i].startsWith("#A:")) {
                    String[] answerParts = parts[i].substring(3).split("-");
                    int questionIndex = Integer.parseInt(answerParts[0]);
                    String answer = answerParts[1];
                    answerSheet.addAnswer(questionIndex, answer);
                }
            }
            answerSheets.add(answerSheet);
        } else if (line.startsWith("#D:N-")) {
            // 解析删除题目信息
            int questionId = Integer.parseInt(line.substring(5));
            deletedQuestions.add(questionId);
            if (questions.containsKey(questionId)) {
                questions.get(questionId).setValid(false);
            }
        } else {
            throw new IllegalArgumentException("wrong format:" + line);
        }
    } catch (Exception e) {
        System.out.println("wrong format:" + line);
    }
}

优点:
分支清晰:根据不同的输入前缀 (#N:, #T:, #X:, #S:, #D:N-),程序通过 if-else 结构对不同类型的输入进行处理,逻辑简单明了,便于理解。
数据解析合理:对于题目、试卷、学生信息、答卷和删除题目,使用字符串拆分 (split) 的方式,从输入中提取数据,结合具体的标识符提取相关字段,能够有效处理复杂格式。
良好的容错性:每个分支都使用 try-catch 捕获异常,如果遇到格式错误的输入,程序可以避免崩溃,并输出提示,继续处理后续输入。
可扩展性强:基于输入前缀的模式(如 #N:, #T:),可以很容易添加新的输入处理逻辑,增加系统功能。
缺点:
输入格式依赖性高:代码对于输入格式的假设比较严格,输入格式的稍微变化(如少一个空格或格式不一致)可能会导致解析失败并输出 "wrong format" 提示,但不能提供详细的错误信息帮助调试。
代码冗余:多个 if-else 分支都有类似的字符串解析操作,比如分割字符串、取子串等,虽然功能不同,但某些部分可以进一步重构和优化,减少重复代码。
错误处理简单:catch 中仅简单地输出错误提示,对于不同的异常类型(如空输入、非法格式等)没有详细的区分,也没有额外提示如何修复输入错误。
硬编码解析逻辑:解析逻辑通过直接字符串切割和硬编码索引来提取数据,虽然简单但不灵活,特别是当输入格式发生变化时(如题目信息不止两部分)会变得难以维护和扩展。

成绩处理逻辑:processResults 方法

static void processResults() {
    // 计算每张试卷的总分并生成警示信息
    for (Map.Entry<Integer, Paper> entry : papers.entrySet()) {
        Paper paper = entry.getValue();
        int totalScore = paper.getQuestions().values().stream().mapToInt(Integer::intValue).sum();
        if (totalScore != 100) {
            System.out.println("alert: full score of test paper" + entry.getKey() + " is not 100 points");
        }
    }

    // 处理每个学生的答卷
    for (AnswerSheet answerSheet : answerSheets) {
        Paper paper = papers.get(answerSheet.getPaperId());
        if (paper == null) {
            System.out.println("The test paper number does not exist");
            continue;
        }

        Student student = students.get(answerSheet.getStudentId());
        if (student == null) {
            System.out.println(answerSheet.getStudentId() + " not found");
            continue;
        }

        // 记录每个题目的结果和得分
        List<String> questionResults = new ArrayList<>();
        List<String> scoreResults = new ArrayList<>();
        int totalScore = 0;

        for (Map.Entry<Integer, Integer> questionEntry : paper.getQuestions().entrySet()) {
            Integer questionId = questionEntry.getKey();
            Integer score = questionEntry.getValue();

            // 检查题目是否存在于题库中
            Question question = questions.get(questionId);
            if (question == null) {
                // 题目不存在的情况
                questionResults.add("non-existent question~0");
                scoreResults.add("0");
            } else if (!question.isValid()) {
                // 题目无效(已删除)的情况
                questionResults.add("the question " + questionId + " invalid~0");
                scoreResults.add("0");
            } else {
                // 题目有效,继续检查学生作答情况
                String studentAnswer = answerSheet.getAnswers().getOrDefault(questionId, "answer is null").trim();
                
                if (studentAnswer.equals("answer is null")) {
                    // 学生未作答该题目
                    questionResults.add("answer is null");
                    scoreResults.add("0");
                } else {
                    // 学生作答,判断是否正确
                    boolean correct = studentAnswer.equals(question.getAnswer());
                    int obtainedScore = correct ? score : 0;
                    totalScore += obtainedScore;
                    questionResults.add(question.getContent() + "~" + studentAnswer + "~" + (correct ? "true" : "false"));
                    scoreResults.add(String.valueOf(obtainedScore));
                }
            }
        }
        
        // 输出题目的结果
        for (String result : questionResults) {
            System.out.println(result);
        }

        // 输出学生总成绩及题目得分,格式为: 学号 姓名: 每题得分 总分~总分
        String scoreString = String.join(" ", scoreResults);  // 将每题得分用空格连接
        System.out.println(answerSheet.getStudentId() + " " + student.getName() + ": " + scoreString + "~" + totalScore);
    }
}

优点:
试卷总分验证:系统首先计算每张试卷的总分并检查是否为 100 分,提供合理的警示机制,如果分值设置有误,可以及时发现问题。
答卷处理细致:处理每个学生的答卷时,针对不同情况提供详细判断,包括题目不存在、题目被删除、学生未作答、学生答题错误等。这种细化的判断使得程序对各种答题场景都有较好的处理能力。
实时输出结果:程序在处理每个答卷时,会实时输出每道题的答题情况以及每个学生的成绩,这有助于快速获取结果,便于调试。
答案与分数分离:将学生的每题答题情况与得分分别存储在两个不同的列表 (questionResults 和 scoreResults),使得代码逻辑清晰,输出时可以灵活组合展示信息。
缺点:
复杂性增加:为了处理各种可能的输入情况(题目不存在、题目无效、学生未作答等),代码中存在多层嵌套 if-else,导致代码的复杂度较高,后续维护难度可能较大。
硬编码逻辑:对于题目的正确性判断和分数计算,都采用了硬编码的方式,没有提供灵活的评分机制。如果以后需要扩展,例如引入部分正确、题目权重等,修改现有代码的成本会比较大。
缺少统一的错误处理机制:每种错误(如无效题目、未作答)都在分支内单独处理,虽然能应对各种情况,但没有统一的错误处理机制。如果增加更多种类的错误类型,代码将变得更难维护。
代码冗余:对于题目结果和分数的记录与输出,虽然分离了答题结果和得分,但代码仍然存在一定冗余,例如每个条件分支都需要分别处理 questionResults 和 scoreResults,可以考虑进一步简化。

在提交源码的过程中,遇到了几个常见的问题:
字符串解析问题:在解析输入数据时,由于输入格式稍有差异或空格不规范,导致 split() 的结果不如预期。例如,输入中的多余空格或输入的某些部分缺少必要的标志符,导致解析异常。
解决方案:通过使用 trim() 方法清除多余的空格,并添加更多的格式校验和异常处理。
数据丢失问题:在解析学生答卷时,由于某些题目ID没有正确映射到题库,导致后续评分时出现空指针异常。
解决方案:增加对题目ID的合法性检查,并在答卷处理时进行更严格的数据验证。
边界情况处理不足:例如,有些学生未作答某些题目时,没有给出明确的处理逻辑,导致程序输出出现不一致。
解决方案:完善了默认值的处理,对于未作答的题目,给予零分处理,并在输出时增加了相应提示。

第二次作业:
第二次作业相比于第一次作业简化了类设计、集中化了逻辑、灵活的输入处理和对异常情况的处理。
类图:

顺序图:

evaluatePapers方法
功能:遍历所有答卷,根据对应试卷中的题目和标准答案进行判分,并输出判题信息和总分。

检查试卷的满分是否为100分,不满足则给出警告信息。
判断学生作答是否正确,并根据试卷的分值进行累加,输出每道题的判题结果及总分。
解释:

public static void evaluatePapers() {
    // 检查每张试卷的总分是否为100分
    for (TestPaper paper : testPaperMap.values()) {
        if (!paper.isFullScoreValid()) {
            System.out.println("alert: full score of test paper" + paper.id + " is not 100 points");
        }
    }

    // 判分处理
    for (AnswerSheet sheet : answerSheets) {
        if (!testPaperMap.containsKey(sheet.paperId)) {
            System.out.println("The test paper number does not exist");
            continue;
        }

        TestPaper paper = testPaperMap.get(sheet.paperId);
        int totalScore = 0;
        int answerIndex = 0;
        StringBuilder scoreOutput = new StringBuilder();

        for (int questionId : paper.questionPoints.keySet()) {
            Question question = questionMap.get(questionId);
            int points = paper.questionPoints.get(questionId);

            // 判断是否有作答
            if (answerIndex < sheet.answers.size()) {
                String studentAnswer = sheet.answers.get(answerIndex);
                boolean isCorrect = question.correctAnswer.equals(studentAnswer);
                System.out.println(question.question + "~" + studentAnswer + "~" + isCorrect);
                totalScore += isCorrect ? points : 0;
                scoreOutput.append(isCorrect ? points : 0).append(" ");
            } else {
                // 如果没有答案,输出 null
                System.out.println("answer is null");
                scoreOutput.append(0).append(" ");
            }

            answerIndex++;
        }

        // 输出各题得分及总分
        if (scoreOutput.length() > 0) {
            scoreOutput.setLength(scoreOutput.length() - 1);
        }
        System.out.println(scoreOutput + "~" + totalScore);
    }
}

优点:
清晰的逻辑结构:
函数的逻辑结构非常清晰,分为两个步骤:首先检查试卷的有效性,然后对每张答卷进行评分。这样的方法逻辑划分使得代码易于阅读和理解。
输入输出管理得当:
通过 System.out.println,方法及时反馈了试卷的状态、判题信息和最终得分。这有助于调试和输出结果。
动态处理学生答卷:
针对学生答卷灵活处理,即便学生未作答(题目缺失),程序也能够识别并输出 "answer is null",避免出错。
模块化设计:
questionMap、testPaperMap 和 answerSheets 分别存储题目、试卷和答卷信息。每个数据结构都很好地承担了其功能,这种分离使得数据的管理更加清晰。
灵活的得分系统:
该方法通过逐题判断学生的作答情况,并根据试卷中题目的分值动态调整总分,灵活应对不同试卷题目数量和分值分配。
缺点和改进建议:
错误处理不够完善:
当学生的答卷中题目数量少于试卷中的题目时,只简单输出 "answer is null"。在实际应用中,可能需要更多的错误处理(例如,给出明确的错误提示或计入扣分等)。
可扩展性不足:
目前该方法只能处理固定格式的输入,例如:试卷的满分必须是100分。如果在实际应用中需要更复杂的规则(如允许不同试卷有不同满分),此方法的扩展性较差。建议引入更加灵活的校验机制,如允许不同试卷有不同的总分,或允许自定义满分标准。
性能优化空间:
每次都遍历整个 testPaperMap 和 answerSheets 进行评分,这种方式在试卷和答卷规模较大时可能会导致性能问题。可以考虑引入索引或其他数据结构优化评分流程。
代码重复性:
判题逻辑中,多个地方对答案的正确性进行判断和操作,代码有些冗余。可以提取一个独立的判分方法,减少代码重复,提升代码的可维护性和可读性。
未处理异常情况:
如果学生提交的答卷中的题目数超过试卷中的题目数,当前方法不会处理额外的题目。这种情况下,应该输出明确的警告信息,避免潜在的问题。
改进建议:
错误和异常处理:
针对未作答和多答等异常情况,建议给出更详细的提示或定义更细致的规则(如是否允许部分题目未作答,如何处理多答等)。
动态满分机制:
允许不同试卷有不同的满分。例如,可以在 TestPaper 类中增加一个 maxScore 属性,并在 isFullScoreValid 方法中动态检查满分。
提高代码复用性:
提取重复的判题逻辑作为独立方法,减少冗余代码。这样不仅提高了代码的复用性,也便于日后维护和扩展。
性能优化:
如果题目数量和答卷数量巨大,可以通过使用并发机制(如多线程)来提升判分速度。此外,可以考虑缓存某些中间结果,避免重复计算。
遇到的问题(其中还有非零返回没有处理)
问题1:处理输入格式时出现解析错误:
问题描述:在处理输入行时,有时字符串切割不正确,导致题目、试卷、答卷信息解析错误。
解决方案:使用更健壮的字符串处理方式,如 split() 函数后检查数组长度是否正确,并使用正则表达式更加灵活地解析输入。
问题2:评分逻辑中的空答案处理:
问题描述:当学生没有作答时,程序仍然尝试去比较答案,导致空指针异常。
解决方案:在评分时增加对空答案的判断,确保没有答案时直接记录得分为0。
问题3:试卷分值验证时的警告信息:
问题描述:试卷分值不等于100分时程序只是输出警告,未进行进一步的处理,可能导致用户忽视这一重要信息。
解决方案:在输出警告信息后,直接跳过该试卷的评分,避免后续错误的评分操作。

第三次作业:
更加全面,处理更加灵活,能够处理题目删除、不同顺序作答等情况。设计上更考虑到多种情况,例如题目无效、学生未作答等边界条件。
类图:

顺序图:

计算每张试卷的总分并生成警示信息
首先,代码遍历 papers(代表试卷的集合),对每张试卷计算其总分并判断是否符合满分 100 分的要求。如果某张试卷的总分不等于 100,系统会输出警示信息。

for (Map.Entry<Integer, Paper> entry : papers.entrySet()) {
    Paper paper = entry.getValue();
    int totalScore = paper.getQuestions().values().stream().mapToInt(Integer::intValue).sum();
    if (totalScore != 100) {
        System.out.println("alert: full score of test paper" + entry.getKey() + " is not 100 points");
    }
}

分析:
papers 是一个映射 (Map),键为试卷编号,值为 Paper 对象。
Paper 对象包含试卷中所有题目及每个题目的分值。
通过 stream() 和 mapToInt() 方法对每张试卷的所有题目分值进行求和。
如果某张试卷的总分不等于 100,则输出警告信息。
问题心得:
处理每个学生的答卷
在处理学生答卷的功能中,曾遇到学生未作答导致的异常情况。具体表现为,当某题学生未作答时,answerSheet.getAnswers() 返回 null 或空字符串,而后续的 trim() 操作抛出 NullPointerException。在实际测试中,部分学生答卷不完整,导致系统崩溃并抛出空指针异常。为了解决这个问题,我们需要在处理输入时更加严谨,不能假设输入总是正确的。改进建议是,在获取学生作答数据后先进行 null 检查,而不是直接调用方法。通过这样的改进,能够避免异常并提升代码的健壮性。

接下来,代码遍历所有的学生答卷 answerSheets,对每个答卷进行评估。

for (AnswerSheet answerSheet : answerSheets) {
    Paper paper = papers.get(answerSheet.getPaperId());
    if (paper == null) {
        System.out.println("The test paper number does not exist");
        continue;
    }

    Student student = students.get(answerSheet.getStudentId());
    if (student == null) {
        System.out.println(answerSheet.getStudentId() + " not found");
        continue;
    }

    // 记录每个题目的结果和得分
    List<String> questionResults = new ArrayList<>();
    List<String> scoreResults = new ArrayList<>();
    int totalScore = 0;

分析:
answerSheets 是包含所有学生答卷的集合,每个答卷通过 getPaperId() 找到对应的试卷。
如果试卷编号不在 papers 中,表示试卷不存在,输出 "The test paper number does not exist"。
类似地,通过 getStudentId() 找到学生,如果该学生不存在,输出学生未找到的错误信息。
为了记录题目的评估结果,代码使用了两个列表 questionResults(记录每个题目的详情结果)和 scoreResults(记录每题的得分情况)。totalScore 记录学生的总得分。
处理每张答卷中的题目
问题心得:
在处理试卷总分时,代码原本使用整数计算试卷总分,并判断是否为 100 分。然而在某些情况下,试卷的分数并非整数,而是浮动分值(如 99.5 或 100.0),这导致了系统错误地输出警告信息,尽管试卷的实际总分是正确的。通过修改为支持浮点数运算,并在判断时允许一定的精度误差,可以避免因浮点精度问题导致的误报。这样的改进使得系统在处理带有浮动分值的试卷时更加灵活,并减少误判。
此外,系统在匹配学生和试卷数据时,曾因学生提交了错误的试卷编号而提示试卷不存在。在模拟测试中,系统多次输出 "test paper does not exist",尽管问题实际是由于学生输入错误或试卷编号配置错误引起的。这说明外部输入数据的验证不够严格,系统没有足够的机制去应对错误输入。为了解决这一问题,改进建议是增加更详细的错误提示,指出是哪位学生提交了无效试卷编号,并给出相应的调试信息。这一改进有助于管理员快速定位问题,并减少用户误操作带来的负面影响。

for (Map.Entry<Integer, Integer> questionEntry : paper.getQuestions().entrySet()) {
    Integer questionId = questionEntry.getKey();
    Integer score = questionEntry.getValue();

    // 检查题目是否存在于题库中
    Question question = questions.get(questionId);
    if (question == null) {
        questionResults.add("non-existent question~0");
        scoreResults.add("0");
    } else if (!question.isValid()) {
        questionResults.add("the question " + questionId + " invalid~0");
        scoreResults.add("0");
    } else {
        String studentAnswer = answerSheet.getAnswers().getOrDefault(questionId, "answer is null").trim();
        
        if (studentAnswer.equals("answer is null")) {
            questionResults.add("answer is null");
            scoreResults.add("0");
        } else {
            boolean correct = studentAnswer.equals(question.getAnswer());
            int obtainedScore = correct ? score : 0;
            totalScore += obtainedScore;
            questionResults.add(question.getContent() + "~" + studentAnswer + "~" + (correct ? "true" : "false"));
            scoreResults.add(String.valueOf(obtainedScore));
        }
    }
}

分析:
paper.getQuestions() 返回该试卷中所有题目,键为题目编号,值为分数。
代码首先检查题目是否在题库中存在(questions.get(questionId)),如果不存在,记录为 "non-existent question~0"。
如果题目存在但无效(如题目已被删除或禁用),记录为 "invalid"。
如果题目有效,代码获取学生对该题目的作答。如果答案缺失,记录为 "answer is null",否则进行答案对比:
若学生答案正确,给分,否则不给分。
totalScore 逐题累计学生的总分。
问题心得:
系统在处理无效题目和学生未作答的情况时,处理逻辑不够清晰,导致这两种情况的处理方式过于相似,难以区分。在系统输出中,无法清楚辨别哪些题目是由于题目无效而无法评分,哪些题目是学生未作答。为此,改进建议是对无效题目和未作答的情况设置不同的标识,并在输出中提供详细的提示信息,确保在后续的统计和调试中能够准确追踪每种情况。这不仅提高了系统的可维护性,还能更好地进行问题排查。
通过以上几项改进,系统在处理答卷时的稳定性、健壮性和可维护性得到了大幅提升。这些改进措施基于实际的测试结果和对源码的深度分析,确保系统能够更好地应对各种边缘情况和异常输入。

总结:
在过去的三次Java大作业中,逐步掌握了Java编程的基础语法以及面向对象编程的核心理念。这一过程中,我逐步解决了功能实现、代码优化和异常处理等问题,提升了编程能力和问题解决的综合能力。
首先,通过第一次作业的基础判分功能,我初步理解了Java的数据结构及面向对象编程的基本概念,如类的设计和方法的实现。通过建立 Question 类和 AnswerSheet 类,成功处理题目和答卷信息,并通过字符串解析实现了简单的判分逻辑。在此过程中,我遇到了输入格式的解析问题,但通过单元测试和容错机制保证了系统的稳定性。
在第二次作业中,系统引入了 TestPaper 类,实现了试卷与题目的关联管理以及总分校验功能。这不仅加深了我对类设计的理解,还通过更复杂的字符串处理和逻辑判断,提升了代码的灵活性和健壮性。尽管遇到了试卷分值校验和答卷异常处理方面的挑战,但通过数据验证和实时计算的方式,确保了系统的准确性和易用性。
第三次作业在前两次基础上进一步完善,增加了学生信息管理、题目删除功能以及健壮的错误处理机制。通过完善数据一致性管理和异常处理策略,我进一步掌握了如何处理复杂的数据关系和提高系统健壮性。这使得系统不仅能够应对更多的异常输入,还能在题目删除等特殊场景下保持正确的输出,增强了系统的鲁棒性。
通过三次作业,我深刻理解了如何将面向对象的设计原则应用于项目开发中。虽然项目中仍存在代码冗余、错误处理不够细致等问题,但通过不断改进和优化,我逐步掌握了如何构建灵活、可扩展且健壮的系统。这些实践经历为我今后继续深入学习Java编程和面向对象设计打下了坚实的基础。