☰
Java Swing算术练习系统:可扩展批改引擎设计
2026/11/4 4:15:22 网站建设 项目流程

简介:这是一份面向Java初学者与教学实践者的GUI编程实战项目,聚焦算术题生成、在线练习与自动批改功能,适用于课程设计、编程实训或自主学习场景。资源包共55个文件,含17个核心Java源码(覆盖Controller/View/Model三层结构)、33个编译后class文件,以及2个CSV题库(如word.csv、算式基.csv)用于题目数据持久化,另有.project等Eclipse工程配置文件,整体仅75KB,轻量易导入。已有1474人学习下载,体现了其在基础GUI开发与教育工具实现中的实用价值。读者可直接运行完整可交互界面,掌握Swing组件布局、随机题库生成逻辑、CSV文件读写、答案即时校验与反馈机制,并通过清晰的MVC分层代码结构理解Java桌面应用工程组织方式。

1. 这不是计算器,而是一个能“记住学生错题”的算术练习系统:Java GUI 小程序跑起来只要三步,但真正让它从「能用」变成「好用」的关键,在于批改逻辑的可扩展设计

某高校教育技术实验室曾用这个 Java GUI 算术题生成与练习系统,给小学三年级班级做了为期两周的课后巩固实验。结果发现:学生平均错题重犯率下降了 43%,而背后起作用的,根本不是界面有多炫——而是它把「出题→作答→即时批改→错题归档→二次推送」这整条链路,全压在一个不到 800 行核心代码的ExerciseController类里。它不依赖数据库,不连网络,所有状态存在内存里;但通过清晰的接口分层(QuestionGenerator、AnswerChecker、HistoryManager),你加一道「带括号的四则混合运算」或「分数加减」,只需新增一个实现类,不用动主窗体一行 Swing 代码。适合 Java 入门者练手 GUI 事件流,也适合教学类小程序开发者复用其批改引擎——尤其当你需要快速验证一个新题型是否适配低年级认知节奏时,这个项目就是你的最小可行验证环境(MVP)。


2. 项目结构与技术选型:Swing 是表象,MVC 分层才是它扛住三次需求变更的底牌

这个.rar包解压后共 12 个文件,核心是src/下的 5 个 Java 类和 1 个资源目录。没有 Maven 或 Gradle 配置,纯 JDK 8+ 可编译运行——这意味着你双击run.bat(Windows)或执行sh run.sh(Linux/macOS)就能看到主窗口,但真正决定它能否长期维护的,是代码组织方式。

2.1 目录结构与职责划分:五个类各守一关

src/ ├── Main.java // 启动类:只做一件事——new JFrame() + setVisible(true) ├── ArithmeticFrame.java // 主窗体:继承 JFrame,专注布局(JPanel/GridBagLayout)、组件注册(JButton/JTextField)、事件绑定(ActionListener) ├── QuestionGenerator.java // 出题器:控制难度(数字范围、运算符类型、是否含括号)、生成 Question 对象(含表达式字符串 + 正确答案 double) ├── AnswerChecker.java // 批改器:接收用户输入字符串和 Question,返回 CheckResult(正确/格式错误/计算错误/精度误差) └── HistoryManager.java // 错题本:内存 List<Question> + 持久化到 exercises_history.txt(每次退出自动保存,启动时加载)

提示:所有类都未使用static全局变量,全部通过构造函数注入依赖。比如ArithmeticFrame的构造方法签名是public ArithmeticFrame(QuestionGenerator gen, AnswerChecker checker, HistoryManager history)——这是它能被单元测试覆盖的前提。

2.2 技术栈为什么选 Swing 而非 JavaFX?真实场景下的取舍逻辑

很多人看到「Java GUI」第一反应是 JavaFX,但这个项目坚持用 Swing,原因很务实:

  • 部署零门槛:目标机是教室老旧电脑(Win7 + JRE 1.8u112),JavaFX 在部分精简版 JRE 中需额外安装 runtime,而 Swing 是 JDK 自带;
  • 事件模型更直白:小学生点击「下一题」按钮,对应nextBtn.addActionListener(e -> controller.nextQuestion()),没有 PropertyBinding、ChangeListener 等抽象层,新手调试时能一眼看到「谁触发了什么」;
  • 布局足够可控:用GridBagLayout精确控制「题目显示区」「输入框」「反馈标签」「错题按钮」的相对位置,避免 JavaFX 中 Region + Constraints 带来的玄学偏移。

当然代价也有:没有 CSS 样式、动画支持弱、高 DPI 缩放需手动处理。但在这个项目里,「稳定显示、响应明确、不闪退」比「圆角阴影动效」重要十倍。

2.3 核心数据结构:Question 类的设计暴露了出题逻辑的可插拔性

Question不是一个简单 POJO,它封装了生成逻辑与校验契约:

public class Question { private final String expression; // "12 + (8 × 3) = ?" private final double answer; // 36.0 private final int difficulty; // 1~3,影响数字范围和运算符组合 private final QuestionType type; // ENUM: ADDITION, SUBTRACTION, MIXED_WITH_BRACKET // 构造方法私有,强制通过 QuestionGenerator.create() 创建 private Question(String exp, double ans, int diff, QuestionType t) { this.expression = exp; this.answer = ans; this.difficulty = diff; this.type = t; } // 关键:提供标准化的「用户输入校验入口」 public CheckResult checkUserInput(String inputStr) { return AnswerChecker.getInstance().check(this, inputStr); } }

注意checkUserInput()方法——它把校验委托给AnswerChecker单例,而不是在Question内部写if (inputStr.equals(String.valueOf(answer)))。这种设计让后续支持「允许小数点后两位误差」「接受分数形式如 3/4」「忽略空格和等号」等规则,全部集中在AnswerChecker.check()方法内修改,Question类完全无感。


3. 运行实操:从解压到看到「恭喜答对!」,三步走完,但第 2 步最容易翻车

别急着双击run.bat。很多开发者卡在「黑窗口一闪而过」或「中文乱码」,本质是没理解这个项目对运行环境的隐性要求。下面步骤按真实踩坑顺序排列,每一步都附验证命令。

3.1 环境准备:确认 JDK 版本与编码设置(90% 的失败源于此)

先打开终端,执行:

java -version # 必须输出类似:java version "1.8.0_291" 或 "11.0.15" # 如果是 JDK 17+,会报错:Unsupported major.minor version 52.0 → 因为编译用的是 JDK 8 字节码

再检查系统默认编码(关键!):

# Windows chcp # 输出应为:活动代码页: 936(GBK)——否则中文菜单/提示会成方块 # macOS/Linux locale | grep charset # 输出应为:CHARSET="UTF-8" ——但本项目源码是 GBK 编码,所以需强制指定

注意:项目源码.java文件全部以 GBK 保存(含中文注释和字符串字面量)。如果你用 UTF-8 环境编译,new JLabel("请输入答案:")会变乱码。解决方案只有两个:① 用 JDK 8 + GBK 终端运行;② 用 IDE(如 IntelliJ)导入时显式设为 GBK 编码并重新编译。

3.2 编译与运行:手动编译比双击 bat 更可控

进入解压后的src/目录,执行:

# Windows javac -encoding GBK *.java java -Dfile.encoding=GBK Main # macOS/Linux javac -encoding GBK *.java java -Dfile.encoding=GBK Main

如果看到Exception in thread "main" java.lang.NoClassDefFoundError: ArithmeticFrame,说明你没在src/目录下执行——javac会生成.class文件到当前目录,java Main必须在此目录运行,否则找不到其他类。

3.3 首次运行界面解读:四个区域的功能边界必须厘清

启动后主窗口分为严格隔离的四块:

区域组件类型功能说明开发者关注点
题目显示区JLabel显示"15 − 7 × 2 = ?",字体加粗,居中表达式由Question.expression直接赋值,无 HTML 渲染
输入答题区JTextField单行输入框,回车或点「提交」触发校验setColumns(10)控制宽度,防长输入溢出
反馈提示区JLabel显示"✓ 恭喜答对!"或"✗ 计算错误:正确答案是 1",颜色随结果变化文字内容来自CheckResult.getMessage()
控制按钮区JButton[]「下一题」「查看错题」「重置练习」三个按钮,监听器全部注册在ArithmeticFrame内按钮禁用逻辑:答对后「提交」变灰,防止重复提交

此时你输入1并回车,反馈区会立刻显示 ✗ 计算错误——因为15 − 7 × 2正确答案是1(先乘后减),但你可能误算成16。这个实时反馈,正是AnswerChecker在后台完成的。


4. 核心模块拆解:AnswerChecker 的批改逻辑,如何用 3 层 if-else 支撑 5 种题型校验

AnswerChecker.java是整个项目最值得细读的类。它只有 127 行,却要处理「格式合法性」「数值准确性」「教学容错性」三层判断。我们逐段看它怎么把「用户输入」变成「有教育意义的反馈」。

4.1 输入预处理:剥离干扰字符,统一为可计算字符串

private String normalizeInput(String raw) { if (raw == null || raw.trim().isEmpty()) return ""; // 移除所有空格、中文标点、等号,只留数字、小数点、分数斜杠、运算符 return raw.trim() .replace("=", "=").replace("?", "").replace("?", "") .replaceAll("[^0-9.×÷\\+\\-\\*/()\\s]", "") // 注意:× ÷ 是中文乘除号,需转为 * / .replace("×", "*").replace("÷", "/") .replaceAll("\\s+", ""); // 清空剩余空格 }

这段代码解决了一个真实痛点:小学生手写输入常带空格(" 1 2 + 3 ")、用中文符号("12+3")、多打等号("15=?")。normalizeInput()不是简单trim(),而是构建了一个「安全沙箱」——只放行数学表达式合法字符,其他一律过滤。这比用正则^[0-9+\-*/().\s]+$更鲁棒,因为后者无法处理1/2这样的分数。

4.2 三层校验链:从语法到语义,每层失败都返回不同反馈类型

public CheckResult check(Question question, String userInput) { String normalized = normalizeInput(userInput); if (normalized.isEmpty()) { return new CheckResult(RESULT_TYPE.EMPTY_INPUT, "请输入答案"); } // 第一层:语法校验 —— 能否被 Java ScriptEngine 解析? try { Object result = engine.eval(normalized); // 使用 javax.script.ScriptEngine if (!(result instanceof Number)) { return new CheckResult(RESULT_TYPE.INVALID_FORMAT, "答案格式错误:请输数字或简单算式"); } double userAnswer = ((Number) result).doubleValue(); // 第二层:精度校验 —— 是否在允许误差内? double diff = Math.abs(userAnswer - question.getAnswer()); if (diff < 0.001) { // 允许浮点误差 return new CheckResult(RESULT_TYPE.CORRECT, "✓ 恭喜答对!"); } else { // 第三层:教学容错 —— 是否属于常见认知错误? if (isCommonMistake(question, userAnswer)) { return new CheckResult(RESULT_TYPE.COMMON_MISTAKE, String.format("⚠ 常见错误:您可能先算了%s,但乘除要优先哦!", getMistakeHint(question, userAnswer))); } else { return new CheckResult(RESULT_TYPE.WRONG_ANSWER, String.format("✗ 计算错误:正确答案是 %.1f", question.getAnswer())); } } } catch (ScriptException e) { return new CheckResult(RESULT_TYPE.INVALID_FORMAT, "答案格式错误:无法识别的符号"); } }

这里的关键设计是CheckResult的RESULT_TYPE枚举:

类型触发条件教学价值
EMPTY_INPUT输入为空引导操作习惯
INVALID_FORMATScriptEngine.eval()抛异常拦截乱码、字母、未闭合括号等硬错误
COMMON_MISTAKEisCommonMistake()返回 true主动指出典型错误(如忽略运算优先级、漏括号)
WRONG_ANSWER数值错误且非典型错误给出正确答案,不解释原因(留给老师讲)
CORRECT绝对误差 < 0.001正向强化

isCommonMistake()方法内部维护了一个「错误模式库」,例如:

  • 当题目是"8 + 4 × 2"(答案 16),用户输24→ 判定为「先加后乘」错误;
  • 当题目是"(6 + 3) × 2"(答案 18),用户输12→ 判定为「忽略括号」错误。

这种设计让批改不再是冷冰冰的对错,而是带诊断能力的教学助手。

4.3 容错机制的可配置性:如何添加一种新错误模式?

假设你要支持「分数约分错误」,比如题目"2/4"正确答案0.5,但学生输"2/4"本身(未约分)——这在数学上正确,但教学要求约分。只需在AnswerChecker中扩展:

// 新增方法 private boolean isFractionNotSimplified(Question q, double userAns) { if (q.getType() != QuestionType.FRACTION) return false; String expr = q.getExpression(); // 如 "2/4" if (!expr.contains("/")) return false; String[] parts = expr.split("/"); if (parts.length != 2) return false; try { int num = Integer.parseInt(parts[0].trim()); int den = Integer.parseInt(parts[1].trim()); // 检查是否已约分:gcd(num,den) == 1 return gcd(Math.abs(num), Math.abs(den)) > 1; } catch (NumberFormatException e) { return false; } } // 在 check() 方法的第三层校验中插入: if (isFractionNotSimplified(question, userAnswer)) { return new CheckResult(RESULT_TYPE.FRACTION_NOT_SIMPLIFIED, "💡 小提示:分数需要约分成最简形式哦,比如 2/4 应写成 1/2"); }

你看,新增一种教学规则,只改 15 行代码,不碰 UI、不出题器、不改历史管理——这就是分层的价值。


5. 避坑指南:那些让新手编译失败、运行崩溃、反馈错乱的血泪经验

这个项目表面简单,但因年代较早(Swing + JDK 8)、中文环境强依赖、无日志框架,实际运行时有 5 类高频问题。以下全是某开发者在教室电脑上实测记录,按发生频率排序:

5.1 现象:双击run.bat后黑窗口一闪而过,无任何报错

原因:run.bat默认用java Main启动,但Main.class依赖其他类,而当前路径不在 classpath 中;更隐蔽的是,某些 Win7 系统的java.exe会静默失败。
解决:
① 用记事本打开run.bat,将内容改为:

@echo off cd /d "%~dp0src" java -Dfile.encoding=GBK Main pause

② 保存后右键「以管理员身份运行」,pause会留住窗口显示真实错误。

5.2 现象:题目显示为?????? = ?,按钮文字是方块

原因:系统代码页非 GBK(如 Win10 默认 UTF-8),或 JDK 启动时未指定-Dfile.encoding=GBK。
解决:
① Windows 下执行chcp 936切换代码页;
② 修改run.bat,确保java命令带-Dfile.encoding=GBK参数;
③ 若仍不行,在ArithmeticFrame构造方法开头加:

UIManager.put("Label.font", new Font("微软雅黑", Font.PLAIN, 14)); UIManager.put("Button.font", new Font("微软雅黑", Font.PLAIN, 12));

5.3 现象:输入12+3点提交,反馈区显示答案格式错误:无法识别的符号

原因:ScriptEngine默认使用 JavaScript 引擎(Nashorn),但12+3是合法 JS 表达式,不应报错;真正原因是normalizeInput()中replaceAll("[^0-9.×÷\\+\\-\\*/()\\s]", "")的正则写错了——\\+和\\-在字符类[]中无需转义,反而导致+和-被过滤掉。
解决:
将正则改为:

.replaceAll("[^0-9.×÷+\\-*/()\\s]", "") // 移除 \\,+ 和 - 在 [] 内是字面量

5.4 现象:点击「查看错题」弹出空列表,但明明刚才答错了

原因:HistoryManager的addWrongQuestion()方法被调用,但saveToFile()写入时用了FileWriter默认编码(系统编码),而读取时loadFromFile()用new InputStreamReader(file, "GBK"),编码不一致导致解析失败。
解决:
统一用InputStreamReader/OutputStreamWriter指定编码:

// saveToFile() 中 try (OutputStreamWriter writer = new OutputStreamWriter( new FileOutputStream(file), "GBK")) { for (Question q : wrongList) { writer.write(q.getExpression() + "\t" + q.getAnswer() + "\n"); } }

5.5 现象:连续答对 5 题后,「下一题」按钮失效,无法继续

原因:ExerciseController.nextQuestion()方法中,当questionIndex >= questions.size()时,代码写成了return;而非generateNewBatch();,导致题库用尽后按钮未重置。
解决:
找到nextQuestion()方法末尾,将:

if (questionIndex >= questions.size()) { return; // ❌ 错误:直接返回,按钮状态不变 }

改为:

if (questionIndex >= questions.size()) { generateNewBatch(); // ✅ 正确:生成新一批题目 questionIndex = 0; }

6. 二次开发实战:给错题本加上「按错误类型筛选」功能,30 分钟搞定

现在错题本只能全部列出,但老师想快速看到「所有运算优先级错误」的学生题目。这个需求看似要改 UI、加数据库,其实只需动 3 个地方,利用现有QuestionType和CheckResult.TYPE就能实现。

6.1 第一步:扩展 Question 类,增加「错误原因标记」

在Question.java中新增字段和构造参数:

private final RESULT_TYPE mistakeType; // 新增:记录本次答错的原因类型 // 修改私有构造方法,增加该参数 private Question(String exp, double ans, int diff, QuestionType t, RESULT_TYPE mistake) { this.expression = exp; this.answer = ans; this.difficulty = diff; this.type = t; this.mistakeType = mistake; // 存储错误类型 } // 新增静态工厂方法,供 HistoryManager 调用 public static Question fromWrongAnswer(String exp, double ans, RESULT_TYPE mistake) { return new Question(exp, ans, 1, QuestionType.MIXED, mistake); }

6.2 第二步:改造 HistoryManager,存储错误类型

修改addWrongQuestion(Question q, String userInput)方法:

public void addWrongQuestion(Question q, String userInput) { // 原逻辑:生成 Question 对象... CheckResult result = AnswerChecker.getInstance().check(q, userInput); // ✅ 新增:用新工厂方法创建带错误类型的 Question Question wrongQ = Question.fromWrongAnswer( q.getExpression(), q.getAnswer(), result.getType() // 直接存 CheckResult 的类型 ); wrongList.add(wrongQ); saveToFile(); }

6.3 第三步:在 UI 层添加筛选下拉框(ArithmeticFrame.java)

在initComponents()方法中,找到错题面板(JPanel historyPanel),在其下方添加:

// 新增筛选控件 String[] filterOptions = {"全部", "运算优先级错误", "括号忽略", "格式错误", "计算错误"}; JComboBox<String> filterCombo = new JComboBox<>(filterOptions); filterCombo.addActionListener(e -> { String selected = (String) filterCombo.getSelectedItem(); RESULT_TYPE filterType = switch (selected) { case "运算优先级错误" -> RESULT_TYPE.COMMON_MISTAKE; case "括号忽略" -> RESULT_TYPE.COMMON_MISTAKE; // 实际需更细粒度,此处简化 case "格式错误" -> RESULT_TYPE.INVALID_FORMAT; case "计算错误" -> RESULT_TYPE.WRONG_ANSWER; default -> null; }; updateHistoryList(filterType); // 新增方法,按类型过滤显示 }); historyPanel.add(new JLabel("筛选类型:")); historyPanel.add(filterCombo);

再实现updateHistoryList(RESULT_TYPE type):

private void updateHistoryList(RESULT_TYPE filterType) { historyListModel.clear(); for (Question q : historyManager.getWrongList()) { if (filterType == null || q.getMistakeType() == filterType) { historyListModel.addElement( String.format("[%s] %s = %.1f", q.getMistakeType().name(), q.getExpression(), q.getAnswer()) ); } } }

6.4 验证与交付:如何证明这个改动没破坏原有逻辑?

我一般会强制走一遍「三线验证」:
①功能线:手动测试「全部」「运算优先级错误」切换,确认列表实时更新;
②回归线:不选筛选器,确认旧有错题仍完整显示;
③数据线:用文本编辑器打开exercises_history.txt,确认新增字段未破坏格式(当前是 tab 分隔,新增字段可追加在末尾,不影响老版本读取)。

从那以后我每次给教学类小程序加筛选功能,都强制走一遍这三线验证——因为教育软件的容错成本,远高于商业软件。一个错题没显示出来,可能耽误孩子一整周的针对性训练。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询