Java Swing记账本实战:从零实现文件持久化与UI交互
发布时间:2026/10/30 17:21:23 锦皓数字建站

简介这是一份面向Java初学者与桌面应用开发入门者的实战项目资源基于J2SE技术栈实现轻量级个人记账管理功能聚焦GUI编程、SQLite嵌入式数据库操作与事件驱动逻辑整合。资源共173个文件含46个核心Java源码覆盖DAO层、UI面板、工具类等、49个编译后class文件、42个依赖jar包含SQLite-JDBC驱动、16个界面图标png及2个SQLite数据库文件整体压缩包29.45MB结构清晰便于逐模块学习调试。已有744人下载学习适合通过完整可运行项目掌握Swing组件布局、CRUD数据交互、用户输入验证及跨平台打包部署等关键能力。代码注释详实预览可见RecordDAO、SpendPanel、ChartUtil等典型模块涵盖账户管理、收支记录、历史查询与图表辅助分析功能是理解Java桌面应用工程化实践的优质范例。1. 为什么一个“小小记账本”能成为Java新手绕不开的实战锚点不是所有Java项目都叫“小小记账本”但几乎所有刚写完HelloWorld、学完ArrayList和Scanner、卡在“不知道代码怎么组织成程序”的人最后都会亲手敲出一个带增删改查、能存本地文件、启动就弹窗的记账本——它不炫技不连数据库不套Spring却把面向对象设计、IO流、Swing/AWT界面、数据持久化、异常处理这五根Java主干全串起来了。你可能在蓝桥杯模拟题里见过“记录每日收支”的需求在Java面试题中被问过“如何保证金额精度”或“多线程下如何避免重复记账”甚至在IDEA里反复调试FileWriter编码乱码问题到凌晨两点……这些真实痛点全藏在这个看似简单的项目里。它不是玩具而是你从语法练习走向工程思维的第一块真实跳板没有Maven依赖冲突不涉及分布式事务但每一步都得自己扛——文件读写失败怎么兜底日期格式输错怎么提示删除前要不要二次确认这些细节才是Java工程师真正开始“写程序”而不是“抄代码”的分水岭。2. 用纯JavaSwing搭起最小可用记账本从零跑通核心流程2.1 为什么选Swing而不是JavaFX或Web——轻量、可控、无额外依赖很多新手一上来就想用Spring Boot搭个网页版记账本结果卡在Tomcat配置、Maven依赖冲突、前端路由上三天没写一行业务逻辑。而“小小记账本”的原始定位就是脱离框架、直面JDK原生能力。Swing虽老但JDK 8自带无需额外引入组件稳定JFrame、JTable、JOptionPane事件模型清晰ActionListener最重要的是——它强制你思考“界面状态怎么和数据对象同步”。比如用户点击“添加”按钮后是直接往JTable的DefaultTableModel里塞新行还是先创建Transaction对象、再调用addRow()这个选择背后就是MVC分层意识的萌芽。我一般会禁用JavaFX需额外模块、拒绝Web方案引入Servlet容器即失焦就用javax.swing.*和java.io.*两个包打完全场——目标明确练手不是炫技。2.2 三步跑通定义实体 → 实现文件持久化 → 绑定界面交互定义核心实体类Transaction与AccountBook// Transaction.java —— 每一笔收支的原子单位 public class Transaction { private String date; // 格式yyyy-MM-dd避免Date类时区陷阱 private String category; // 收入/餐饮/交通/娱乐等 private double amount; // 用double需谨慎实际项目应改用BigDecimal此处为简化教学 private String remark; // 备注可为空 // 构造方法、getter/setter省略IDEA可自动生成 public Transaction(String date, String category, double amount, String remark) { this.date date; this.category category; this.amount amount; this.remark remark; } }// AccountBook.java —— 记账本容器管理Transaction列表及文件操作 import java.io.*; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.*; public class AccountBook { private ListTransaction transactions; private static final String DATA_FILE account_data.txt; // 纯文本存储不依赖数据库 private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd); public AccountBook() { this.transactions new ArrayList(); loadFromFile(); // 启动时自动加载历史数据 } // 从文件读取按行解析字段用|分隔如2024-05-20|餐饮|45.5|外卖 public void loadFromFile() { try (BufferedReader reader new BufferedReader(new FileReader(DATA_FILE))) { String line; while ((line reader.readLine()) ! null) { String[] parts line.split(\\|, -1); // -1保留空字段 if (parts.length 4) { Transaction t new Transaction( parts[0].trim(), parts[1].trim(), Double.parseDouble(parts[2].trim()), parts[3].trim() ); transactions.add(t); } } } catch (FileNotFoundException e) { // 文件不存在是正常情况首次运行时创建空文件 saveToFile(); // 写入空文件避免后续读取报错 } catch (IOException | NumberFormatException e) { System.err.println(加载数据失败 e.getMessage()); } } // 保存到文件覆盖写入简单粗暴 public void saveToFile() { try (PrintWriter writer new PrintWriter(new FileWriter(DATA_FILE))) { for (Transaction t : transactions) { writer.println(String.format(%s|%s|%.2f|%s, t.getDate(), t.getCategory(), t.getAmount(), t.getRemark())); } } catch (IOException e) { System.err.println(保存数据失败 e.getMessage()); } } // 提供业务方法 public void addTransaction(Transaction t) { transactions.add(t); saveToFile(); // 每次操作立即落盘牺牲性能换可靠性小数据量够用 } public ListTransaction getAllTransactions() { return new ArrayList(transactions); // 返回副本避免外部修改原始列表 } public void deleteTransaction(int index) { if (index 0 index transactions.size()) { transactions.remove(index); saveToFile(); } } }关键参数说明DATA_FILE设为相对路径account_data.txt程序运行目录下自动生成无需手动创建split(\\|, -1)中-1参数确保末尾空字段如备注为空不被丢弃%.2f格式化输出强制金额保留两位小数避免double显示45.49999999999999这类玄学问题new ArrayList(transactions)返回副本而非引用是Java集合操作的血泪经验——防止UI层误改底层数据。Swing界面搭建用JTable展示JDialog录入// MainFrame.java —— 主窗口 import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.time.LocalDate; import java.time.format.DateTimeFormatter; public class MainFrame extends JFrame { private AccountBook book; private JTable table; private DefaultTableModel tableModel; private JButton addButton, deleteButton; public MainFrame() { this.book new AccountBook(); initUI(); loadDataToTable(); } private void initUI() { setTitle(小小记账本); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 表格区域 String[] columns {日期, 类别, 金额, 备注}; tableModel new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; // 表格只读编辑走弹窗 } }; table new JTable(tableModel); JScrollPane scrollPane new JScrollPane(table); add(scrollPane, BorderLayout.CENTER); // 按钮面板 JPanel buttonPanel new JPanel(new FlowLayout()); addButton new JButton(添加); deleteButton new JButton(删除); buttonPanel.add(addButton); buttonPanel.add(deleteButton); add(buttonPanel, BorderLayout.SOUTH); // 事件绑定 addButton.addActionListener(e - showAddDialog()); deleteButton.addActionListener(e - deleteSelectedRow()); pack(); setLocationRelativeTo(null); setVisible(true); } private void loadDataToTable() { tableModel.setRowCount(0); // 清空现有数据 for (Transaction t : book.getAllTransactions()) { tableModel.addRow(new Object[]{ t.getDate(), t.getCategory(), t.getAmount(), t.getRemark() }); } } private void showAddDialog() { JDialog dialog new JDialog(this, 添加记账, true); dialog.setLayout(new GridLayout(5, 2, 5, 5)); dialog.setSize(300, 200); JLabel dateLabel new JLabel(日期yyyy-MM-dd); JTextField dateField new JTextField(LocalDate.now().format(DateTimeFormatter.ISO_DATE)); JLabel categoryLabel new JLabel(类别); JTextField categoryField new JTextField(餐饮); JLabel amountLabel new JLabel(金额); JTextField amountField new JTextField(); JLabel remarkLabel new JLabel(备注); JTextField remarkField new JTextField(); JButton confirmButton new JButton(确定); confirmButton.addActionListener(e - { try { String date dateField.getText().trim(); String category categoryField.getText().trim(); double amount Double.parseDouble(amountField.getText().trim()); String remark remarkField.getText().trim(); if (date.isEmpty() || category.isEmpty() || amount 0) { JOptionPane.showMessageDialog(dialog, 日期、类别、金额必填且金额0); return; } book.addTransaction(new Transaction(date, category, amount, remark)); loadDataToTable(); // 刷新表格 dialog.dispose(); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(dialog, 金额必须是数字); } }); dialog.add(dateLabel); dialog.add(dateField); dialog.add(categoryLabel); dialog.add(categoryField); dialog.add(amountLabel); dialog.add(amountField); dialog.add(remarkLabel); dialog.add(remarkField); dialog.add(new JLabel()); dialog.add(confirmButton); dialog.setLocationRelativeTo(this); dialog.setVisible(true); } private void deleteSelectedRow() { int selectedRow table.getSelectedRow(); if (selectedRow -1) { JOptionPane.showMessageDialog(this, 请先选择要删除的记录); return; } if (JOptionPane.showConfirmDialog(this, 确定删除该笔记录, 确认删除, JOptionPane.YES_NO_OPTION) JOptionPane.YES_OPTION) { book.deleteTransaction(selectedRow); loadDataToTable(); } } public static void main(String[] args) { SwingUtilities.invokeLater(() - new MainFrame()); } }逻辑说明JTable使用DefaultTableModel动态管理数据loadDataToTable()每次刷新都清空重载避免状态不同步添加弹窗用JDialog模态对话框阻塞主窗口操作符合用户心智模型LocalDate.now().format(...)生成默认日期比new Date()更安全无时区干扰删除前JOptionPane.showConfirmDialog二次确认这是UI交互的底线要求漏掉会被用户骂死SwingUtilities.invokeLater确保GUI在事件分发线程EDT中创建避免多线程渲染异常。3. 文件持久化避坑指南那些让记账本“丢钱”的隐藏雷区3.1 现象程序重启后数据全没了 → 原因文件路径没搞清写到了错误目录 → 解决用绝对路径日志验证新手常以为new FileWriter(data.txt)会把文件写到项目根目录实际写入位置是JVM启动时的工作目录——可能是IDEA的bin目录、命令行执行时的/home/user甚至某个临时路径。结果就是你看到account_data.txt生成了但下次运行时找不到它因为工作目录变了。解决启动时打印绝对路径并记录日志System.out.println(数据文件路径 new File(account_data.txt).getAbsolutePath()); // 输出示例/home/user/idea-project/account_data.txt更稳妥的做法是显式指定路径private static final String DATA_FILE System.getProperty(user.dir) /account_data.txt; // 或用Class.getResource()获取jar同级路径打包后也有效3.2 现象中文备注显示为乱码如“外卖”变“???” → 原因FileReader/FileWriter默认用系统编码Windows是GBKLinux/macOS是UTF-8 → 解决强制指定UTF-8编码FileReader和FileWriter不接受编码参数必须升级为InputStreamReader/OutputStreamWriter// 错误写法依赖系统默认编码 new FileReader(account_data.txt) // 正确写法强制UTF-8 new InputStreamReader(new FileInputStream(account_data.txt), StandardCharsets.UTF_8) // 对应写入 new OutputStreamWriter(new FileOutputStream(account_data.txt), StandardCharsets.UTF_8)提示所有涉及文本IO的操作只要含中文必须显式指定StandardCharsets.UTF_8这是Java IO的铁律。3.3 现象连续添加两笔相同金额的记录表格里只显示一笔 → 原因JTable的DefaultTableModel未触发视图刷新 → 解决调用fireTableDataChanged()或重置模型tableModel.setRowCount(0)后逐行addRow()是安全的但若直接操作tableModel.getDataVector()再fireTableDataChanged()易遗漏。最稳做法是每次操作后重建模型数据private void loadDataToTable() { // 方案1清空重加当前代码已采用可靠 tableModel.setRowCount(0); for (Transaction t : book.getAllTransactions()) { tableModel.addRow(new Object[]{...}); } // 方案2替换整个数据向量适合大数据量 // tableModel.setDataVector(dataVector, columnNames); }3.4 现象删除第3条记录后原本第4条变成第3条但用户仍选中旧索引导致删错 → 原因JTable.getSelectedRow()返回的是视图行号非数据模型行号 → 解决用convertRowIndexToModel()Swing表格支持排序/过滤视图行号view row和模型行号model row可能不一致。正确删除姿势private void deleteSelectedRow() { int viewRow table.getSelectedRow(); if (viewRow -1) return; int modelRow table.convertRowIndexToModel(viewRow); // 关键转换 book.deleteTransaction(modelRow); loadDataToTable(); }3.5 现象金额输入10.555保存后变成10.55或10.554999999999999→ 原因double二进制浮点数精度丢失 → 解决业务金额必须用BigDecimal虽然教学版用double简化但真实记账绝不允许精度误差。改造Transactionimport java.math.BigDecimal; public class Transaction { private BigDecimal amount; // 替换double public Transaction(String date, String category, BigDecimal amount, String remark) { this.amount amount.setScale(2, RoundingMode.HALF_UP); // 强制保留2位小数 } // getter返回BigDecimaltoString()用amount.toString()而非doubleValue() }对应UI层输入校验try { BigDecimal amount new BigDecimal(amountField.getText().trim()); // ... 创建Transaction } catch (NumberFormatException e) { JOptionPane.showMessageDialog(dialog, 金额格式错误请输入数字如10.50); }注意BigDecimal构造函数绝不能传doublenew BigDecimal(10.55)仍会继承double的精度缺陷必须传String。4. 从“能跑”到“能用”三个让记账本真正落地的关键增强4.1 金额精度加固用BigDecimal替代double的完整改造清单模块改动点关键代码示例Transactionprivate BigDecimal amount构造时new BigDecimal(String)setScale(2)this.amount new BigDecimal(amountStr).setScale(2, RoundingMode.HALF_UP);AccountBookaddTransaction()参数改为BigDecimal文件读写用amount.toString()writer.println(String.format(%sUI层输入校验用new BigDecimal(text)显示用amount.toString()tableModel.addRow(new Object[]{..., t.getAmount().toString(), ...});为什么必须这么做Java中0.1 0.2 ! 0.3是经典陷阱。记账场景下100.01 * 3算出300.03000000000003这种结果用户会直接卸载应用。BigDecimal是唯一合规解代价是代码稍冗长但值得。4.2 日期输入防错用JFormattedTextField替代普通JTextField手动输2024-05-32或2024/05/20会导致解析失败。用日期格式化器约束输入import javax.swing.text.DateFormatter; import java.text.SimpleDateFormat; // 在showAddDialog()中替换dateField JFormattedTextField dateField new JFormattedTextField( new DateFormatter(new SimpleDateFormat(yyyy-MM-dd)) ); dateField.setValue(new Date()); // 默认设为今天 dateField.setColumns(10);优势用户只能输入合法日期格式非法输入如2024-13-01自动恢复为上次有效值无需手动校验。4.3 数据导出Excel用Apache POI实现一键生成报表用户常问“能不能导出Excel”——这是记账本进阶刚需。引入POI依赖Mavendependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version /dependency导出方法加到AccountBook类import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; public void exportToExcel(String filename) throws IOException { try (Workbook workbook new XSSFWorkbook(); FileOutputStream fileOut new FileOutputStream(filename)) { Sheet sheet workbook.createSheet(记账明细); // 表头 Row headerRow sheet.createRow(0); String[] headers {日期, 类别, 金额, 备注}; for (int i 0; i headers.length; i) { Cell cell headerRow.createCell(i); cell.setCellValue(headers[i]); } // 数据行 int rowNum 1; for (Transaction t : transactions) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(t.getDate()); row.createCell(1).setCellValue(t.getCategory()); row.createCell(2).setCellValue(t.getAmount().doubleValue()); // Excel只认double row.createCell(3).setCellValue(t.getRemark()); } // 自动列宽 for (int i 0; i headers.length; i) { sheet.autoSizeColumn(i); } workbook.write(fileOut); } }调用时机在主界面加一个“导出Excel”按钮点击后调用book.exportToExcel(account_export.xlsx)。注意setCellValue()对BigDecimal需转doubleValue()Excel本身不支持高精度小数存储。5. 验证你的记账本是否“真可用”四步压力测试法5.1 场景化测试清单不靠代码覆盖率靠真实操作流别只测addTransaction()方法是否返回true要模拟用户真实行为链测试场景操作步骤预期结果高频添加连续添加50笔记录含中文备注、不同日期文件大小合理增长约2KB表格滚动流畅无内存溢出边界日期输入2024-02-30无效、2024-02-29闰年有效、2023-13-01无效无效日期被拦截并提示有效日期成功录入金额极端值输入0.001、999999999.99、-100、abc小数自动四舍五入到分超大数正常显示负数被拦截非数字弹出格式错误提示并发误操作快速连点“添加”按钮3次再点“删除”不崩溃最终数据条数准确添加3条→删除1条剩2条执行建议把这些场景写成.txt文档贴在项目根目录每次重构后花5分钟手动跑一遍——比写JUnit单元测试更能暴露Swing线程问题。5.2 文件损坏容灾模拟断电后数据恢复能力关机前强制杀掉Java进程kill -9再启动程序✅合格表现程序能正常加载已有数据新添加记录不丢失❌翻车表现启动报NumberFormatException或部分记录解析失败。根因与修复问题saveToFile()是覆盖写入写到一半断电文件被截断解决改用“先写临时文件再原子替换”public void saveToFile() { String tempFile DATA_FILE .tmp; try (PrintWriter writer new PrintWriter( new OutputStreamWriter(new FileOutputStream(tempFile), StandardCharsets.UTF_8))) { for (Transaction t : transactions) { writer.println(String.format(%s|%s|%s|%s, t.getDate(), t.getCategory(), t.getAmount().toString(), t.getRemark())); } // 原子替换Linux/macOS用Files.moveWindows需用File.renameTo Files.move(Paths.get(tempFile), Paths.get(DATA_FILE), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { System.err.println(保存失败 e.getMessage()); } }5.3 性能基线1000条记录下的响应时间实测用脚本批量生成1000条测试数据// 在main方法中临时添加 AccountBook testBook new AccountBook(); for (int i 0; i 1000; i) { testBook.addTransaction(new Transaction( 2024-01- String.format(%02d, (i % 28) 1), Arrays.asList(餐饮, 交通, 购物, 娱乐).get(i % 4), new BigDecimal(10.00).add(new BigDecimal(i % 100)), 测试记录 i )); }实测指标i5-8250U / 8GB内存加载1000条 200ms纯文本解析足够快添加1条 50ms含文件IO删除1条 30ms导出Excel 1s500条以内。结论纯Java Swing方案在千条数据内完全胜任无需过早优化。5.4 用户习惯适配暗色模式与快捷键支持程序员喜欢暗色主题记账本也该跟上。Swing本身不支持系统级暗色但可手动切换// 在MainFrame构造函数中添加 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); // 或强制设为Nimbus带现代感 // UIManager.setLookAndFeel(javax.swing.plaf.nimbus.NimbusLookAndFeel);快捷键提升效率// 在initUI()中为按钮绑定快捷键 addButton.setMnemonic(KeyEvent.VK_A); // AltA deleteButton.setMnemonic(KeyEvent.VK_D); // AltD // 表格支持空格键快速删除选中行后按空格 table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) .put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), delete); table.getActionMap().put(delete, new AbstractAction() { Override public void actionPerformed(ActionEvent e) { deleteSelectedRow(); } });我坚持给每个Java新手项目加这两项——不是为了炫技而是让他们从第一天就建立“软件要适配人不是让人适应软件”的产品意识。那个总在深夜调试IO编码问题的你终有一天会感谢今天多写的这几行setMnemonic和UIManager。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。