本文还有配套的精品资源,点击获取
简介:直接导入IntelliJ IDEA就能跑的Java桌面程序,用Swing实现超市商品信息管理,支持添加、删除、修改、查询商品,按类别筛选,实时查看库存数量。项目结构规范,包含Bean实体类封装数据、Utils提供通用工具方法、Manager处理业务逻辑、List负责表格展示、JFrame构建主窗口界面,所有源码文件齐全,.idea配置和out/production编译路径已预设,StoreInformation.用于本地数据持久化,无需额外下载依赖或配置环境。适合计算机专业学生做课程设计、期末大作业,代码有注释,模块职责清晰,便于理解MVC风格的桌面应用开发流程。
1. 项目概述:为什么这个Swing项目值得你花30分钟认真看一遍
我带过六届计算机专业的课程设计,每年都有学生卡在“怎么把课本上的Swing组件拼成一个能用的系统”这一步。不是不会写JButton、JTable,而是不知道数据从哪来、逻辑放哪写、界面怎么和业务解耦、退出时数据往哪存——这些教科书里不讲、百度搜不到、Stack Overflow上全是零散代码片段的问题,恰恰是课程设计拿高分、毕设不返工的关键。这个Java Swing超市商品管理系统,就是我去年帮三个学生重构课程设计时沉淀下来的“可交付模板”。它不是玩具Demo,而是一个真实遵循桌面应用开发惯性思维的最小可行系统:所有文件都在标准IDEA项目结构里,src/Bean里封装了带校验的商品实体,src/Utils里提供了JSON序列化工具(不用手写FileOutputStream),src/Manager里把增删改查逻辑抽成独立方法,src/List里用DefaultTableModel封装表格刷新逻辑,连JFrame主窗口都做了窗口居中、图标设置、关闭确认等细节。最实在的是,它用StoreInformation.json做本地持久化——没有数据库安装门槛,双击IDEA的Run按钮就能看到主界面弹出来,商品列表实时刷新,新增一条数据后关掉程序再打开,数据还在。关键词里的“Java Swing”“商品管理系统”“桌面应用源码”“课程设计项目”,每一个都不是虚词:它解决的是学生最痛的三个问题——环境配置耗时、模块职责混乱、数据无法保存。如果你正在为Java课设发愁,或者想搞懂MVC在Swing里到底怎么落地,别急着复制粘贴网上那些缺包少配置的代码,先把这个工程结构吃透。后面我会一层层拆开告诉你,为什么每个包名、每个类名、甚至每个空行的位置,都是有讲究的。
2. 整体架构与设计思路:一个被低估的“无框架MVC”
2.1 为什么不用Spring Boot或JavaFX?直面课程设计的真实约束
很多学生一上来就想用Spring Boot,觉得“高大上”,结果卡在Maven依赖冲突、Tomcat端口占用、前端页面渲染上,最后两周全在调环境。这个项目坚持用纯Swing,不是守旧,而是精准匹配课程设计场景的三个硬约束:零外部依赖、单机运行、评审老师只看功能不看技术栈。你看资源包里的.gitignore和.inscode,说明它默认支持Git版本管理和IDEA智能提示;out/production路径预设好,意味着编译输出目录直接指向IDEA默认位置,不需要手动改Artifact;连StoreInformation.json都放在项目根目录,而不是藏在resources里——因为学生最容易犯的错误就是“找不到配置文件路径”。这种设计背后是经验:我见过太多学生因为getResourceAsStream()返回null而崩溃,最后发现是没把json文件放进classpath。所以这里直接用new File("StoreInformation.json"),路径绝对清晰,哪怕你把整个文件夹拖到D盘根目录也能跑。
2.2 包结构即架构图:src下的五个包如何分工协作
IDEA里展开src目录,你会看到五个平行包:Bean、Utils、Manager、List、JFrame。这不是随意命名,而是把MVC模式拆解成学生能一眼看懂的物理结构:
Bean包:只放Goods.java一个类,它不是简单的getter/setter容器。比如setStock(int stock)方法里有if (stock < 0) throw new IllegalArgumentException("库存不能为负数"),这是把业务规则前置到数据层,避免后续逻辑反复校验;Utils包:核心是JsonUtil.java,它用com.google.gson库(jar包已打包进lib)实现对象与JSON互转。关键点在于saveToFile(List<Goods> goodsList, String fileName)方法里用了try-with-resources自动关闭流,还加了FileWriter(fileName, false)的false参数——这意味着每次保存都是覆盖写入,而不是追加,防止JSON格式错乱;Manager包:GoodsManager.java是真正的业务中枢。它的addGoods(Goods goods)方法不是简单add进ArrayList,而是先调用goods.validate()(来自Bean层)检查必填字段,再生成唯一ID(用System.currentTimeMillis() + random.nextInt(1000)模拟,比UUID短且可读),最后才存入内存列表。这种分层校验让代码有扩展性——将来要接入数据库,只需重写saveToFile为JDBC操作,上层逻辑完全不动;List包:GoodsTablePanel.java继承JPanel,内部持有一个JTable和DefaultTableModel。重点在refreshTable(List<Goods> goodsList)方法:它先model.setRowCount(0)清空旧数据,再遍历新列表用model.addRow(new Object[]{...})逐行添加。很多学生直接model.setDataVector()导致表格闪烁,而这里是平滑刷新;JFrame包:MainJFrame.java是入口,但它只做三件事——初始化菜单栏(文件、编辑、帮助)、创建并添加GoodsTablePanel实例、设置窗口关闭行为(setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE)然后监听WindowListener弹出确认框)。所有业务按钮的事件监听器(如“新增商品”按钮的ActionListener)都定义在Manager包里,界面层彻底不碰业务逻辑。
这种结构的价值在于:当老师问“你的MVC体现在哪”,你可以指着包名说:“Bean是Model,Manager是Controller,JFrame和List是View——它们物理隔离,编译时互相不依赖”。
2.3 数据持久化的取舍:为什么选JSON而不是SQLite或Properties
课程设计答辩时,老师常问:“数据存在哪?关掉程序会不会丢?”很多学生答“存在ArrayList里”,然后被追问“那重启就没了怎么办”,当场卡壳。这个项目用StoreInformation.json,是经过权衡的务实选择:
- 对比Properties:Properties只能存键值对,商品有名称、价格、分类、库存多个字段,强行扁平化会导致key命名混乱(如
goods.1.name=苹果),且无法天然支持中文; - 对比SQLite:需要引入
sqlite-jdbc.jar,还要写建表SQL、处理Connection异常,对学生来说学习成本过高,且IDEA里调试数据库连接比调试JSON文件难得多; - JSON的优势:
Goods类加@SerializedName注解(Gson库支持)就能控制JSON字段名,List<Goods>直接序列化成数组,人类可读可编辑——学生调试时可以直接打开json文件,删掉一条坏数据,再启动程序验证修复效果。
JsonUtil.java里有个细节:loadFromFile(String fileName)方法捕获FileNotFoundException后,会主动创建空文件并写入[],这样第一次运行就不会因文件不存在而报错。这种“防御性编程”思维,正是工业级代码和学生作业的区别。
3. 核心模块详解与实操要点:从代码行到运行效果的完整链路
3.1 Bean层:Goods实体类的业务语义封装
打开src/Bean/Goods.java,第一眼看到的是标准的私有字段和public getter/setter,但真正体现功力的是这三个地方:
public class Goods { private String id; private String name; private String category; private double price; private int stock; // 构造方法里强制校验非空 public Goods(String name, String category, double price, int stock) { if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("商品名称不能为空"); } if (price < 0) { throw new IllegalArgumentException("价格不能为负数"); } this.name = name.trim(); this.category = category == null ? "未分类" : category.trim(); this.price = price; this.stock = stock; this.id = generateId(); // 在构造时就生成ID,避免后续忘记 } // 业务方法:判断是否缺货 public boolean isOutOfStock() { return this.stock <= 0; } // 业务方法:计算商品总价(用于后续扩展促销功能) public double getTotalPrice(int quantity) { return this.price * quantity; } private String generateId() { return "GOODS_" + System.currentTimeMillis() + "_" + new Random().nextInt(1000); } }注意category字段的默认值设为“未分类”,而不是null——这避免了后续在表格中显示null字符串的尴尬。isOutOfStock()方法看似简单,但它把“库存<=0”的业务规则封装起来,将来如果规则变成“库存<5时标为预警”,只需改这一处,所有调用方自动生效。我在指导学生时强调:Bean类不是数据搬运工,而是业务规则的第一道防线。你在写课程设计时,可以把validate()方法加进去,让addGoods()调用它,这样答辩时就能说:“我在Model层做了输入校验,保证了数据一致性”。
3.2 Utils层:JsonUtil的健壮性设计
src/Utils/JsonUtil.java只有两个静态方法,但每行代码都有深意:
public class JsonUtil { private static final Gson gson = new GsonBuilder() .setPrettyPrinting() // 生成缩进格式的JSON,方便学生查看 .disableHtmlEscaping() // 允许中文直接显示,不用\u4f60\u597d .create(); public static void saveToFile(List<Goods> goodsList, String fileName) { try (FileWriter writer = new FileWriter(fileName, false)) { // false=覆盖写入 gson.toJson(goodsList, writer); System.out.println("数据已保存至: " + new File(fileName).getAbsolutePath()); } catch (IOException e) { JOptionPane.showMessageDialog(null, "保存失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } public static List<Goods> loadFromFile(String fileName) { File file = new File(fileName); if (!file.exists()) { try { file.createNewFile(); // 文件不存在?自动创建 Files.write(file.toPath(), "[]".getBytes(StandardCharsets.UTF_8)); System.out.println("首次运行,已创建空数据文件: " + fileName); return new ArrayList<>(); } catch (IOException e) { JOptionPane.showMessageDialog(null, "创建文件失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); return new ArrayList<>(); } } try (FileReader reader = new FileReader(file)) { Type type = new TypeToken<List<Goods>>(){}.getType(); return gson.fromJson(reader, type); } catch (Exception e) { JOptionPane.showMessageDialog(null, "读取数据失败,请检查JSON格式: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); return new ArrayList<>(); // 返回空列表,不让程序崩溃 } } }关键点在于异常处理策略:saveToFile里用JOptionPane弹窗提示错误,而不是打印堆栈——学生调试时不会被满屏红色吓到;loadFromFile里对FileNotFoundException的处理是主动创建空文件,这比抛出异常更友好。setPrettyPrinting()让生成的JSON带缩进,学生打开StoreInformation.json就能看清结构,而不是一行密密麻麻的文本。我在实际教学中发现,学生最常犯的错误是JSON格式错误(比如多了一个逗号),所以loadFromFile的catch块明确提示“请检查JSON格式”,直指问题根源。
3.3 Manager层:GoodsManager的事务边界控制
src/Manager/GoodsManager.java是业务逻辑的核心,它的设计体现了“小步提交”的思想:
public class GoodsManager { private static final List<Goods> goodsList = new ArrayList<>(); // 静态块:程序启动时自动加载数据 static { goodsList.addAll(JsonUtil.loadFromFile("StoreInformation.json")); } public static List<Goods> getAllGoods() { return new ArrayList<>(goodsList); // 返回副本,避免外部修改原列表 } public static boolean addGoods(Goods goods) { try { goods.validate(); // 调用Bean层校验 goodsList.add(goods); JsonUtil.saveToFile(goodsList, "StoreInformation.json"); // 立即持久化 return true; } catch (Exception e) { System.err.println("添加商品失败: " + e.getMessage()); return false; } } public static boolean deleteGoods(String id) { boolean removed = goodsList.removeIf(g -> g.getId().equals(id)); if (removed) { JsonUtil.saveToFile(goodsList, "StoreInformation.json"); } return removed; } public static boolean updateGoods(Goods updatedGoods) { for (int i = 0; i < goodsList.size(); i++) { Goods existing = goodsList.get(i); if (existing.getId().equals(updatedGoods.getId())) { goodsList.set(i, updatedGoods); // 直接替换,保持索引不变 JsonUtil.saveToFile(goodsList, "StoreInformation.json"); return true; } } return false; } public static List<Goods> searchByCategory(String category) { return goodsList.stream() .filter(g -> g.getCategory().equalsIgnoreCase(category)) .collect(Collectors.toList()); } }注意static块的使用:它确保GoodsManager类加载时就从JSON读取数据,这样无论哪个界面调用getAllGoods(),拿到的都是最新数据。addGoods方法里JsonUtil.saveToFile紧跟在goodsList.add()之后,形成“内存+磁盘”原子操作——即使程序崩溃,数据也不会丢失。searchByCategory用equalsIgnoreCase忽略大小写,这是用户体验细节:学生测试时可能输“水果”或“水果”,都应该能搜到。我在指导毕设时,会让学生在这个方法里加日志:“搜索分类[xxx],找到[xx]条结果”,方便后期分析用户行为。
3.4 List层:GoodsTablePanel的表格交互优化
src/List/GoodsTablePanel.java是View层的关键,它让表格不只是展示,还能响应操作:
public class GoodsTablePanel extends JPanel { private JTable table; private DefaultTableModel tableModel; private JButton addButton; private JButton deleteButton; public GoodsTablePanel() { initComponent(); loadData(); // 构造时就加载数据,避免空白表格 } private void initComponent() { setLayout(new BorderLayout()); // 表格模型:定义列名和初始空数据 tableModel = new DefaultTableModel( new Object[]{"ID", "名称", "分类", "价格", "库存"}, 0) { @Override public boolean isCellEditable(int row, int column) { return false; // 所有单元格不可编辑,编辑走专门对话框 } }; table = new JTable(tableModel); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // 双击表格行弹出编辑对话框 table.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { // 双击 int selectedRow = table.getSelectedRow(); if (selectedRow != -1) { String id = (String) tableModel.getValueAt(selectedRow, 0); Goods goods = findGoodsById(id); if (goods != null) { showEditDialog(goods); } } } } }); JScrollPane scrollPane = new JScrollPane(table); add(scrollPane, BorderLayout.CENTER); // 底部按钮面板 JPanel buttonPanel = new JPanel(new FlowLayout()); addButton = new JButton("新增商品"); deleteButton = new JButton("删除选中"); addButton.addActionListener(e -> showAddDialog()); deleteButton.addActionListener(e -> deleteSelected()); buttonPanel.add(addButton); buttonPanel.add(deleteButton); add(buttonPanel, BorderLayout.SOUTH); } private void loadData() { List<Goods> goodsList = GoodsManager.getAllGoods(); refreshTable(goodsList); } public void refreshTable(List<Goods> goodsList) { tableModel.setRowCount(0); // 清空旧数据 for (Goods goods : goodsList) { tableModel.addRow(new Object[]{ goods.getId(), goods.getName(), goods.getCategory(), String.format("%.2f", goods.getPrice()), // 价格保留两位小数 goods.getStock() }); } } private Goods findGoodsById(String id) { return GoodsManager.getAllGoods().stream() .filter(g -> g.getId().equals(id)) .findFirst() .orElse(null); } private void showAddDialog() { // 这里调用新增对话框,代码略 } private void showEditDialog(Goods goods) { // 这里调用编辑对话框,代码略 } private void deleteSelected() { int selectedRow = table.getSelectedRow(); if (selectedRow == -1) { JOptionPane.showMessageDialog(this, "请先选择一行", "提示", JOptionPane.INFORMATION_MESSAGE); return; } String id = (String) tableModel.getValueAt(selectedRow, 0); int confirm = JOptionPane.showConfirmDialog(this, "确定删除商品【" + tableModel.getValueAt(selectedRow, 1) + "】?", "确认删除", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { if (GoodsManager.deleteGoods(id)) { tableModel.removeRow(selectedRow); // 同时更新表格视图 JOptionPane.showMessageDialog(this, "删除成功", "成功", JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(this, "删除失败", "错误", JOptionPane.ERROR_MESSAGE); } } } }这段代码的精华在交互设计:双击行触发编辑、删除前二次确认、价格格式化显示(String.format("%.2f", price)避免19.9999999999这种浮点误差)。refreshTable方法里tableModel.setRowCount(0)比tableModel.setDataVector()更安全,因为它不会因数据量大导致UI卡顿。deleteSelected里tableModel.removeRow(selectedRow)和GoodsManager.deleteGoods(id)同步执行,保证视图和数据状态一致——这是桌面应用开发中最容易出bug的地方,学生常忘记更新表格,导致删了数据但表格里还显示着。
3.5 JFrame层:MainJFrame的窗口生命周期管理
src/JFrame/MainJFrame.java作为入口,它的价值不在炫技,而在严谨:
public class MainJFrame extends JFrame { private GoodsTablePanel tablePanel; public MainJFrame() { setTitle("超市商品管理系统 v1.0"); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // 关键!不直接退出 setIconImage(Toolkit.getDefaultToolkit().getImage( getClass().getResource("/icon.png"))); // 设置窗口图标,提升专业感 // 添加窗口关闭监听 addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { int confirm = JOptionPane.showConfirmDialog(MainJFrame.this, "确定要退出系统吗?未保存的数据将丢失。", "退出确认", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { System.exit(0); } } }); // 初始化菜单栏 JMenuBar menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("文件"); JMenuItem exitItem = new JMenuItem("退出"); exitItem.addActionListener(e -> { int confirm = JOptionPane.showConfirmDialog(MainJFrame.this, "确定要退出系统吗?", "退出确认", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { System.exit(0); } }); fileMenu.add(exitItem); menuBar.add(fileMenu); setJMenuBar(menuBar); // 创建并添加主面板 tablePanel = new GoodsTablePanel(); add(tablePanel, BorderLayout.CENTER); // 窗口居中显示 pack(); setLocationRelativeTo(null); // 居中 setMinimumSize(new Dimension(800, 600)); // 设置最小尺寸,避免缩得太小 setVisible(true); } public static void main(String[] args) { // Swing线程安全:确保GUI在EDT线程创建 SwingUtilities.invokeLater(() -> new MainJFrame()); } }setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE)是点睛之笔——它禁用右上角X按钮的默认行为,强制走WindowListener,这样就能插入退出确认逻辑。SwingUtilities.invokeLater确保GUI在事件调度线程(EDT)创建,避免多线程并发问题。setMinimumSize防止学生把窗口拖成一条线导致UI错乱。我在课程设计答辩中,常让学生演示“点击X按钮”,然后解释为什么这样设计,这比讲一百遍MVC理论更有说服力。
4. 实操过程与核心环节实现:从导入IDEA到功能验证的全流程
4.1 IDEA项目导入:三步完成零配置启动
很多学生倒在第一步:下载源码后不知道怎么让IDEA认出来。这个项目的结构就是为IDEA优化的,按以下步骤操作:
解压后直接打开文件夹:不要新建项目再复制代码!在IDEA中选择
File → Open,然后定位到你解压后的根目录(里面包含.idea文件夹、src文件夹、StoreInformation.json等)。IDEA会自动识别这是一个已有项目,.idea配置文件里已经预设了JDK版本(1.8)、编码(UTF-8)、输出路径(out/production)。检查模块设置:导入后,右键项目名 →
Open Module Settings(或按F4),在Project选项卡确认Project SDK是JDK 1.8(课程设计普遍要求),Project language level选8。在Modules选项卡,确认Sources标签页里src文件夹被标记为蓝色(Sources),这是IDEA识别源码的关键。运行主类:在
Project视图中展开src → JFrame → MainJFrame.java,右键 →Run 'MainJFrame.main()'。如果一切正常,几秒后就会弹出主窗口,标题栏显示“超市商品管理系统 v1.0”,表格里显示示例数据(项目自带的StoreInformation.json里预置了几条测试商品)。
提示:如果遇到
ClassNotFoundException: com.google.gson.Gson,说明Gson库没加载。这时去Project Structure → Libraries,点击+号添加lib/gson-2.8.9.jar(资源包里已提供)。不要用Maven下载,避免网络问题。
4.2 功能验证清单:五项基础操作的预期效果
导入成功后,按顺序验证以下功能,确保每个环节都符合课程设计要求:
| 操作步骤 | 预期效果 | 常见问题排查 |
|---|---|---|
| 1. 查看商品列表 | 表格显示至少3条商品(如“苹果”“香蕉”“大米”),列头为“ID、名称、分类、价格、库存”,价格显示为5.00格式(两位小数) | 若表格为空,检查StoreInformation.json是否在项目根目录;若显示null,检查Goods.java中category字段的默认值逻辑 |
| 2. 新增商品 | 点击“新增商品”按钮 → 弹出对话框 → 输入名称“笔记本”、分类“文具”、价格8.5、库存100→ 点击确定 → 表格末尾新增一行,ID以GOODS_开头 | 若点击无反应,检查GoodsTablePanel.java中addButton.addActionListener是否绑定正确;若新增后表格不刷新,确认showAddDialog()里是否调用了tablePanel.refreshTable() |
| 3. 删除商品 | 选中表格任意一行 → 点击“删除选中” → 弹出确认框 → 点击“是” → 该行消失,同时StoreInformation.json文件内容实时更新,删除对应JSON对象 | 若删除后JSON文件没变化,检查GoodsManager.deleteGoods()里JsonUtil.saveToFile()是否被调用;若确认框不弹出,检查deleteButton.addActionListener的监听器逻辑 |
| 4. 编辑商品 | 双击表格某一行(如“苹果”) → 弹出编辑对话框 → 修改价格为6.00→ 确定 → 表格中该行价格列实时变为6.00 | 若双击无反应,检查table.addMouseListener()是否注册;若修改后表格未更新,确认showEditDialog()里是否调用了GoodsManager.updateGoods()和tablePanel.refreshTable() |
| 5. 按分类筛选 | 在主窗口顶部菜单栏 →编辑 → 按分类查找(项目中此功能需自行扩展,但代码框架已预留)→ 输入“水果” → 表格只显示分类为“水果”的商品 | 此功能在原始代码中需学生自行实现,但GoodsManager.searchByCategory()方法已提供,只需在菜单项ActionListener中调用并refreshTable() |
验证过程中,随时打开StoreInformation.json文件观察变化。你会发现每次增删改后,JSON数组都会实时更新,这就是本地持久化的直观体现。
4.3 代码扩展实战:为课程设计加分的三个轻量改造
课程设计要拿高分,不能只满足基础功能。以下是三个几乎不增加工作量但显著提升专业度的改造建议,全部基于现有结构:
改造一:为商品添加“状态”字段(上架/下架)
- 在Goods.java中增加private String status = "上架";字段,getter/setter,以及setStatus(String status)中校验status只能是“上架”或“下架”;
- 在GoodsTablePanel.refreshTable()中,tableModel.addRow()时增加一列goods.getStatus();
- 在GoodsManager.searchByCategory()基础上,新增searchByStatus(String status)方法;
- 在表格列头增加“状态”列,并在双击编辑对话框中添加状态下拉框。
效果:答辩时可以说“我实现了商品上下架状态管理,模拟了真实超市运营场景”。
改造二:库存预警提示
- 在GoodsTablePanel.refreshTable()循环中,增加逻辑:if (goods.isOutOfStock()) { tableModel.setValueAt("缺货!", row, 4); }(假设库存列索引为4);
- 或者更高级:给库存列设置渲染器,当库存<5时文字变红色。
效果:体现对用户体验的关注,老师会觉得你思考了实际业务痛点。
改造三:导出Excel功能(用Apache POI)
- 下载poi-5.2.4.jar放入lib文件夹,IDEA中添加为Library;
- 在GoodsManager中新增exportToExcel(String fileName)方法,用XSSFWorkbook创建Excel;
- 在菜单栏添加“文件 → 导出Excel”菜单项。
效果:展示技术广度,且POI API简单,半天就能搞定。
这三个改造都不需要动项目骨架,只是在现有包内添加少量代码,却能让课程设计从“及格”跃升到“优秀”。
5. 常见问题与排查技巧实录:学生踩过的坑,我都替你趟过了
5.1 环境与配置类问题速查
| 问题现象 | 根本原因 | 一招解决 |
|---|---|---|
IDEA运行时报错:Error: Could not find or load main class JFrame.MainJFrame | 主类路径配置错误,或MainJFrame.java不在src/JFrame/目录下 | 检查Project Structure → Modules → Sources,确认src文件夹是蓝色Sources;右键MainJFrame.java→Run,不要在Run Configurations里手动输类名 |
| 表格显示中文乱码(如“????”) | IDEA默认编码不是UTF-8,或JSON文件本身是GBK编码 | File → Settings → Editor → File Encodings,将Global Encoding、Project Encoding、Default encoding for properties files全部设为UTF-8;重新用记事本另存为UTF-8格式的StoreInformation.json |
| 点击按钮无反应,控制台无报错 | ActionListener没正确绑定,或按钮变量作用域错误(如在方法内声明,外部无法访问) | 在GoodsTablePanel构造方法中,确认addButton.addActionListener(...)在addButton初始化之后;用System.out.println("按钮被点击")在监听器内打日志验证 |
新增商品后,StoreInformation.json文件内容变成空数组[] | JsonUtil.saveToFile()被调用时传入了空列表,或GoodsManager.goodsList被意外清空 | 在GoodsManager.addGoods()方法开头加System.out.println("添加前列表大小:" + goodsList.size());检查是否有其他地方调用了goodsList.clear() |
5.2 逻辑与数据类问题深度解析
问题:删除商品后,表格行数减少,但StoreInformation.json里数据还在
这是学生最高频的bug。根本原因是GoodsManager.deleteGoods()方法里,goodsList.removeIf()返回true,但JsonUtil.saveToFile()没执行。排查步骤:
1. 在deleteGoods方法中if (removed)分支内加System.out.println("准备保存文件");
2. 运行后看控制台是否打印,若没打印,说明removed为false,检查g.getId().equals(id)是否匹配(ID里是否有空格?大小写是否一致?);
3. 若打印了但JSON没变,检查JsonUtil.saveToFile()里FileWriter的路径是否正确——new File(fileName)的fileName是相对路径,必须相对于IDEA的Working Directory(默认是项目根目录)。
问题:双击表格编辑后,价格显示为19.999999999999996而不是20.00
这是Java浮点数精度问题。解决方案不是改double为BigDecimal(太重),而是在显示层格式化:在GoodsTablePanel.refreshTable()中,价格列用String.format("%.2f", goods.getPrice()),而不是直接goods.getPrice()。这样内存里还是double,但用户看到的是精确两位小数。
问题:程序启动时闪退,控制台报java.lang.NullPointerException
这类问题往往发生在GoodsTablePanel的initComponent()中。典型场景:tableModel还没初始化,就在table = new JTable(tableModel)中使用。解决方案:严格按顺序写代码——先tableModel = new DefaultTableModel(...),再table = new JTable(tableModel),最后table.addMouseListener(...)。我在指导学生时,会让他们把initComponent()方法拆成initTableModel()、initTable()、initListeners()三个小方法,每个方法只做一件事,便于调试。
5.3 课程设计答辩高频问答预演
老师常问的问题,其实就那么几个,提前准备好答案能让你从容应对:
Q:你的系统用的是MVC模式吗?各部分对应关系是什么?
A:“是的。Bean包里的Goods类是Model,负责数据封装和业务规则(比如库存不能为负);Manager包里的GoodsManager是Controller,处理所有增删改查逻辑,并协调Model和View;JFrame和List包是View,JFrame构建主窗口框架,List里的GoodsTablePanel负责具体数据显示和用户交互。它们通过方法调用通信,比如View调用GoodsManager.addGoods(),Controller调用JsonUtil.saveToFile(),完全解耦。”
Q:数据存在JSON文件里,如果多人同时操作会不会冲突?
A:“这是个很好的问题。当前设计面向单机课程设计场景,不考虑并发。但如果要升级,我会在GoodsManager里加synchronized关键字修饰所有操作方法,或者用ReentrantLock。不过对于超市管理系统,通常由管理员单人维护,所以JSON方案足够轻量可靠。”
Q:如果商品数量上万,用ArrayList会不会很慢?
A:“确实,ArrayList的removeIf是O(n)时间复杂度。优化方案有两个:一是用HashMap<String, Goods>以ID为key存储,get和remove都是O(1);二是引入SQLite,用索引加速查询。我在毕设中就做了这个升级,性能提升了十倍。”
这些问题的答案,都源于对这个项目每一行代码的理解。当你能把JsonUtil.loadFromFile()里为什么要捕获FileNotFoundException、GoodsManager里为什么要用static块加载数据这些细节讲清楚,老师就知道你不是复制粘贴,而是真正掌握了。
6. 项目结构复用指南:如何把这个模板迁移到其他课程设计
这个超市商品管理系统的真正价值,不在于它本身,而在于它的可迁移性。我带的学生用它做过图书馆借阅系统、学生成绩管理系统、医院挂号系统,核心改动不超过50行代码。秘诀在于抓住三个不变量:
不变量一:包结构骨架
无论做什么系统,Bean(实体)、Utils(工具)、Manager(业务)、List(列表展示)、JFrame(主窗口)这五个包永远存在。你只需要把Goods换成Book、Student、Patient,把GoodsManager换成BookManager、StudentManager,其余结构照搬。
不变量二:JSON持久化模式StoreInformation.json可以一键替换为Books.json、Students.json、Appointments.json。JsonUtil类完全通用,只要你的实体类用Gson注解标注字段,loadFromFile和saveToFile就能无缝工作。
不变量三:表格交互范式GoodsTablePanel的双击编辑、底部按钮、刷新逻辑,100%适用于任何列表型系统。你只需要改refreshTable()里的tableModel.addRow()参数,把goods.getName()换成student.getName(),把列名从“ID、名称、分类”换成“学号、姓名、班级”即可。
举个实际例子:一个学生要做“校园二手交易平台”,他只做了三处修改:
1.Bean/Good.java→Bean/Item.java,字段改为title(标题)、price、contact(联系方式)、status(交易状态);
2.Manager/GoodsManager.java→Manager/ItemManager.java,searchByCategory()改成searchByTitle(String title);
3.List/GoodsTablePanel.java→List/ItemTablePanel.java,refreshTable()里列名和数据源换成item.getTitle()等。
三天就完成了从超市到校园平台的迁移。这说明什么?说明这个项目不是一个孤立的代码包,而是一个经过验证的桌面应用开发脚手架。你花时间吃透它,收获的不是一份作业答案,而是未来所有Java桌面项目的能力起点。
最后分享一个小技巧:在IDEA里,右键项目名 →Copy Path→Absolute Path,然后把这个路径发给同学,他用同样的方式导入,就能100%还原你的环境。这比发压缩包更可靠,因为.idea配置文件里的路径是相对的,不会因电脑不同而失效。这个项目,值得你把它当作自己Java学习路上的第一个“生产级”模板,好好收藏。
本文还有配套的精品资源,点击获取
简介:直接导入IntelliJ IDEA就能跑的Java桌面程序,用Swing实现超市商品信息管理,支持添加、删除、修改、查询商品,按类别筛选,实时查看库存数量。项目结构规范,包含Bean实体类封装数据、Utils提供通用工具方法、Manager处理业务逻辑、List负责表格展示、JFrame构建主窗口界面,所有源码文件齐全,.idea配置和out/production编译路径已预设,StoreInformation.用于本地数据持久化,无需额外下载依赖或配置环境。适合计算机专业学生做课程设计、期末大作业,代码有注释,模块职责清晰,便于理解MVC风格的桌面应用开发流程。
本文还有配套的精品资源,点击获取