Architectural Shift: Procedural Scripts to Object-Oriented Parsers
Processing structured text input in Java requires a clear separation between data representation, parsing logic, and business rules. Early implementaitons often rely on monolithic main methods that interleave I/O operations, string manipulation, and conditional scoring. This approach quickly becomes unmaintainable as input formats grow in complexity. Transitioning to an object-oriented architecture involvse encapsulating state within domain models, delegating parsing responsibilities to dedicated components, and applying the Single Responsibility Principle to evaluation logic.
Phase 1: Foundational Parsing and Encapsulation
The initial challenge involves reading question definitions and user responses, aligning them by identifier, and performing basic verification. A procedural implementation typically stores raw arrays or lists and manages alignment through manual index manipulation. An object-oriented refactor introduces dedicated classes for the question bank, response sheet, and grading engine. This eliminates index-coupling and centralizes sorting and retrieval logic.
import java.util.*;
import java.util.stream.Collectors;
public class ExamProcessor {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int questionCount = input.nextInt();
input.nextLine();
QuestionBank bank = new QuestionBank();
bank.loadQuestions(input, questionCount);
ResponseSheet sheet = new ResponseSheet();
sheet.loadResponses(input);
Grader evaluator = new Grader(bank, sheet);
evaluator.displayResults();
evaluator.printVerificationFlags();
}
}
class QuestionBank {
private final List<ExamQuestion> items = new ArrayList<>();
public void loadQuestions(Scanner src, int count) {
for (int i = 0; i < count; i++) {
String raw = src.nextLine();
String[] tokens = raw.split("#N:|\\s*#Q:|\\s*#A:");
if (tokens.length >= 4) {
int id = Integer.parseInt(tokens[1].trim());
String text = tokens[2].trim();
String key = tokens[3].trim();
items.add(new ExamQuestion(id, text, key));
}
}
items.sort(Comparator.comparingInt(ExamQuestion::getId));
}
public List<ExamQuestion> getItems() { return Collections.unmodifiableList(items); }
}
class ExamQuestion {
private final int id;
private final String text;
private final String key;
public ExamQuestion(int id, String text, String key) {
this.id = id; this.text = text; this.key = key;
}
public int getId() { return id; }
public String getText() { return text; }
public String getKey() { return key; }
}
class ResponseSheet {
private final List<String> responses = new ArrayList<>();
public void loadResponses(Scanner src) {
while (src.hasNextLine()) {
String line = src.nextLine();
if ("end".equalsIgnoreCase(line)) break;
String[] parts = line.split("#A:|\\s+");
for (String p : parts) {
if (!p.isEmpty()) responses.add(p);
}
}
}
public List<String> getResponses() { return Collections.unmodifiableList(responses); }
}
class Grader {
private final QuestionBank bank;
private final ResponseSheet sheet;
public Grader(QuestionBank bank, ResponseSheet sheet) {
this.bank = bank; this.sheet = sheet;
}
public void displayResults() {
List<String> answers = sheet.getResponses();
for (ExamQuestion q : bank.getItems()) {
String resp = (q.getId() <= answers.size()) ? answers.get(q.getId() - 1) : "";
System.out.println(q.getText() + "~" + resp);
}
}
public void printVerificationFlags() {
List<String> answers = sheet.getResponses();
List<Boolean> flags = bank.getItems().stream()
.map(q -> (q.getId() <= answers.size()) && answers.get(q.getId() - 1).equals(q.getKey()))
.collect(Collectors.toList());
System.out.println(flags.stream().map(String::valueOf).collect(Collectors.joining(" ")));
}
}
Phase 2: Collection Selection and Scoring Logic
As requirements expand to include weighted scoring and paper configuration, collection choice becomes critical. Using a standard HashMap for question-to-score mapping introduces unpredictable iteration order, which breaks sequential output expectations. Replacing it with LinkedHashMap preserves insertion order, ensuring deterministic grading. Additionally, extracting scoring validation (e.g., verifying a 100-point total) into a service layer prevents business logic from leaking into I/O routines.
import java.util.*;
public class ScoringEngine {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
Map<String, QuestionData> repository = new LinkedHashMap<>();
Map<String, PaperConfig> papers = new LinkedHashMap<>();
List<StudentSubmission> submissions = new ArrayList<>();
while (input.hasNextLine()) {
String line = input.nextLine().trim();
if ("end".equals(line)) break;
if (line.startsWith("#N:")) parseQuestion(line, repository);
else if (line.startsWith("#T:")) parsePaper(line, papers);
else if (line.startsWith("#S:")) parseSubmission(line, submissions);
}
EvaluationService service = new EvaluationService(repository, papers);
for (StudentSubmission sub : submissions) {
service.gradeAndPrint(sub);
}
}
private static void parseQuestion(String raw, Map<String, QuestionData> repo) {
String[] segments = raw.split("\\s+");
String id = segments[0].substring(3);
String content = segments[1].substring(3);
String key = segments[2].substring(3);
repo.put(id, new QuestionData(id, content, key));
}
private static void parsePaper(String raw, Map<String, PaperConfig> papers) {
String[] segments = raw.split("\\s+");
String paperId = segments[0].substring(3);
PaperConfig config = new PaperConfig(paperId);
for (int i = 1; i < segments.length; i++) {
String[] pair = segments[i].split("-");
config.assignWeight(pair[0], Integer.parseInt(pair[1]));
}
papers.put(paperId, config);
}
private static void parseSubmission(String raw, List<StudentSubmission> subs) {
String[] segments = raw.split("\\s+");
String paperId = segments[0].substring(3);
StudentSubmission sub = new StudentSubmission(paperId);
for (int i = 1; i < segments.length; i++) {
sub.recordAnswer(segments[i].substring(3));
}
subs.add(sub);
}
}
class QuestionData {
final String id, content, key;
QuestionData(String id, String content, String key) { this.id = id; this.content = content; this.key = key; }
}
class PaperConfig {
final String id;
final Map<String, Integer> weights = new LinkedHashMap<>();
PaperConfig(String id) { this.id = id; }
void assignWeight(String qId, int pts) { weights.put(qId, pts); }
int getTotalPoints() { return weights.values().stream().mapToInt(Integer::intValue).sum(); }
}
class StudentSubmission {
final String paperId;
final List<String> answers = new ArrayList<>();
StudentSubmission(String paperId) { this.paperId = paperId; }
void recordAnswer(String ans) { answers.add(ans); }
}
class EvaluationService {
private final Map<String, QuestionData> repo;
private final Map<String, PaperConfig> papers;
EvaluationService(Map<String, QuestionData> repo, Map<String, PaperConfig> papers) {
this.repo = repo; this.papers = papers;
}
void gradeAndPrint(StudentSubmission sub) {
PaperConfig paper = papers.get(sub.paperId);
if (paper == null) return;
if (paper.getTotalPoints() != 100) {
System.out.println("alert: full score of test paper " + paper.id + " is not 100 points");
}
int totalEarned = 0;
List<Integer> questionScores = new ArrayList<>();
Iterator<String> qIterator = paper.weights.keySet().iterator();
int idx = 0;
while (qIterator.hasNext()) {
String qId = qIterator.next();
int maxPts = paper.weights.get(qId);
QuestionData q = repo.get(qId);
String userAns = (idx < sub.answers.size()) ? sub.answers.get(idx) : "";
boolean correct = q != null && q.key.equals(userAns);
int earned = correct ? maxPts : 0;
totalEarned += earned;
questionScores.add(earned);
System.out.println((q != null ? q.content : "unknown") + "~" + userAns + "~" + correct);
idx++;
}
if (sub.answers.size() < paper.weights.size()) {
System.out.println("answer is null");
}
System.out.println(questionScores.stream().map(String::valueOf).collect(Collectors.joining(" ")) + "~" + totalEarned);
}
}
Phase 3: Regex Validation, State Management, and Edge Cases
Complex input streams often include conditional rules: revoked questions, student registry lookups, and strict format validation. Relying on String.split() with hardcoded delimiters becomes fragile when whitespace or optional fields vary. Compiling Pattern objects upfront and using Matcher groups provides robust validation. Furthermore, managing cross-references (e.g., linking submissions to student names and filtering deleted questions) requires a centralized context object and defensive null-handling to prevent NullPointerException during traversal.
import java.util.*;
import java.util.regex.*;
import java.util.stream.*;
public class AdvancedExamSystem {
private static final Pattern Q_PATTERN = Pattern.compile("^#N:(\\d+)\\s+#Q:(.+?)\\s+#A:(.+)$");
private static final Pattern T_PATTERN = Pattern.compile("^#T:(\\d+)(?:\\s+(\\d+)-(\\d+))+$");
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
DataContext ctx = new DataContext();
while (sc.hasNextLine()) {
String line = sc.nextLine().trim();
if ("end".equals(line)) break;
RouteParser.dispatch(line, ctx);
}
ReportGenerator.generate(ctx);
}
}
class DataContext {
final Map<Integer, QuestionItem> questions = new HashMap<>();
final Map<Integer, PaperTemplate> templates = new LinkedHashMap<>();
final Map<Integer, List<AnswerRecord>> submissions = new LinkedHashMap<>();
final Map<String, String> studentRegistry = new HashMap<>();
final Set<Integer> revokedQuestions = new HashSet<>();
}
class RouteParser {
static void dispatch(String line, DataContext ctx) {
if (line.startsWith("#N:")) parseQuestion(line, ctx);
else if (line.startsWith("#T:")) parseTemplate(line, ctx);
else if (line.startsWith("#S:")) parseSubmission(line, ctx);
else if (line.startsWith("#X:")) parseStudents(line, ctx);
else if (line.startsWith("#D:")) parseDeletions(line, ctx);
else System.out.println("wrong format:" + line);
}
private static void parseQuestion(String line, DataContext ctx) {
Matcher m = AdvancedExamSystem.Q_PATTERN.matcher(line);
if (m.matches()) {
int id = Integer.parseInt(m.group(1));
ctx.questions.put(id, new QuestionItem(id, m.group(2).trim(), m.group(3).trim()));
} else {
System.out.println("wrong format:" + line);
}
}
private static void parseTemplate(String line, DataContext ctx) {
Matcher m = AdvancedExamSystem.T_PATTERN.matcher(line);
if (m.find()) {
int paperId = Integer.parseInt(m.group(1));
PaperTemplate tpl = new PaperTemplate(paperId);
String[] parts = line.substring(line.indexOf(' ') + 1).split("\\s+");
for (int i = 0; i < parts.length; i += 2) {
tpl.addMapping(Integer.parseInt(parts[i]), Integer.parseInt(parts[i+1]));
}
ctx.templates.put(paperId, tpl);
} else {
System.out.println("wrong format:" + line);
}
}
private static void parseSubmission(String line, DataContext ctx) {
String[] tokens = line.split("#");
String[] header = tokens[1].substring(2).trim().split("\\s+");
int sheetId = Integer.parseInt(header[0]);
String stuId = header[1];
AnswerRecord rec = new AnswerRecord(sheetId, stuId);
for (int i = 2; i < tokens.length; i++) {
String[] pair = tokens[i].substring(2).trim().split("-");
int qNum = Integer.parseInt(pair[0]);
String ans = pair.length > 1 ? pair[1] : "";
rec.putAnswer(qNum, ans);
}
ctx.submissions.computeIfAbsent(sheetId, k -> new ArrayList<>()).add(rec);
}
private static void parseStudents(String line, DataContext ctx) {
String[] entries = line.substring(3).split("-");
for (String e : entries) {
String[] kv = e.trim().split("\\s+");
if (kv.length == 2) ctx.studentRegistry.put(kv[0], kv[1]);
}
}
private static void parseDeletions(String line, DataContext ctx) {
String[] ids = line.substring(3).trim().split("\\s+");
for (String id : ids) {
ctx.revokedQuestions.add(Integer.parseInt(id.split("-")[1]));
}
}
}
class QuestionItem { int id; String text, key; QuestionItem(int i, String t, String k) { id=i; text=t; key=k; } }
class PaperTemplate {
int id; List<int[]> mappings = new ArrayList<>();
PaperTemplate(int id) { this.id = id; }
void addMapping(int qId, int score) { mappings.add(new int[]{qId, score}); }
int calcTotal() { return mappings.stream().mapToInt(a -> a[1]).sum(); }
}
class AnswerRecord {
int sheetId; String studentId; Map<Integer, String> answers = new HashMap<>();
AnswerRecord(int s, String st) { sheetId=s; studentId=st; }
void putAnswer(int q, String a) { answers.put(q, a); }
}
class ReportGenerator {
static void generate(DataContext ctx) {
for (PaperTemplate tpl : ctx.templates.values()) {
if (tpl.calcTotal() != 100) {
System.out.println("alert: full score of test paper" + tpl.id + " is not 100 points");
}
}
for (Map.Entry<Integer, List<AnswerRecord>> entry : ctx.submissions.entrySet()) {
int paperId = entry.getKey();
if (!ctx.templates.containsKey(paperId)) {
System.out.println("The test paper number does not exist");
continue;
}
PaperTemplate tpl = ctx.templates.get(paperId);
for (AnswerRecord rec : entry.getValue()) {
processSingleSubmission(tpl, rec, ctx);
}
}
}
private static void processSingleSubmission(PaperTemplate tpl, AnswerRecord rec, DataContext ctx) {
List<Integer> scores = new ArrayList<>();
List<String> outputs = new ArrayList<>();
for (int[] map : tpl.mappings) {
int qId = map[0];
int pts = map[1];
String userAns = rec.answers.getOrDefault(qId, "answer is null");
if (ctx.revokedQuestions.contains(qId)) {
outputs.add("the question " + qId + " invalid~0");
scores.add(0);
} else if (!ctx.questions.containsKey(qId)) {
outputs.add("non-existent question~0");
scores.add(0);
} else {
QuestionItem q = ctx.questions.get(qId);
boolean match = q.key.equals(userAns);
outputs.add(q.text + "~" + userAns + "~" + match);
scores.add(match ? pts : 0);
}
}
outputs.forEach(System.out::println);
String name = ctx.studentRegistry.get(rec.studentId);
if (name != null) {
System.out.print(rec.studentId + " " + name + ": ");
System.out.println(scores.stream().map(String::valueOf).collect(Collectors.joining(" ")) + "~" + scores.stream().mapToInt(Integer::intValue).sum());
} else {
System.out.println(rec.studentId + " not found");
}
}
}
Technical Best Practices and Common Pitfalls
- Regex Compilation and Validation: Avoid inline pattern compilation inside loops. Precompile
Patternconstants and useMatcher.group()for precise extraction. This prevents fragilesplit()chains and handles variable whitespace gracefully. - Collection Framework Selection: Choose data structures based on access patterns. Use
LinkedHashMapwhen insertion order dictates output sequence. ReserveHashMapfor O(1) lookups where order is irrelevant. Misapplying unordered maps to sequential grading logic causes non-deterministic output. - String Immutability and Manipulation: Java strings are immutable. Repeated concatenation in loops should use
StringBuilderor Stream collectors. Direct assignment modifications on substrings do not alter the original reference and can lead to silent data loss. - Null Safety and Defensive Programming: Always validate map lookups and list bounds before access. Utilize
getOrDefault(),Optional, or explicit boundary checks to preventNullPointerExceptionandIndexOutOfBoundsExceptionduring partial input scenarios. - Encapsulation and Separation of Concerns: Keep I/O, parsing, domain models, and business logic in distinct classes. Exposing internal collections directly breaks encapsulation. Return unmodifiable views or defensive copies to maintain state integrity across the application lifecycle.