ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java Swing+SQLite+JDBC实战:从零构建可视化本地记账本桌面应用

2026/9/2 2:09:09 拓冰建站 浏览量
Java Swing+SQLite+JDBC实战:从零构建可视化本地记账本桌面应用 很多Java初学者在掌握了基础语法后常常会感到迷茫下一步该做什么如何将零散的知识点串联成一个完整的、能实际运行的应用一个常见的痛点就是虽然理解了类和对象也学了数据库连接但面对一个需要界面、数据存储和业务逻辑的完整项目时却不知从何下手。本文将以一个经典的练手项目——“可视化本地记账本”为例带你从零开始整合Java SwingGUI界面、SQLite嵌入式数据库和JDBC数据库连接三大技术栈。你将亲手搭建一个结构清晰、功能完整的桌面应用不仅能巩固Java SE核心知识更能掌握小型桌面应用从设计到实现的完整闭环。无论你是正在寻找课程设计项目的学生还是希望夯实Java桌面开发基础的开发者都能从本文获得可直接复用的代码和清晰的实现思路。1. 项目概述与技术选型在开始编码之前我们首先需要明确我们要构建什么以及为什么选择这些技术。1.1 项目目标一个什么样的记账本我们的目标是开发一个运行在本地的桌面记账软件它需要具备以下核心功能记录流水能够添加新的收入或支出记录包含金额、类别、日期、备注等信息。数据持久化所有记录必须保存在本地即使关闭程序下次打开数据依然存在。可视化查看以清晰的表格形式展示所有历史记录并支持按条件如时间范围、类别筛选。数据统计提供简单的统计功能如总收入、总支出、结余以及按类别的消费分析图表。数据管理支持对已有记录的修改和删除操作。这是一个典型的CRUD增删改查应用非常适合用来练习数据库操作和界面交互。1.2 为什么选择 Java Swing SQLiteJava Swing 它是Java标准库JFC的一部分无需安装任何额外框架或依赖。对于学习GUI编程和理解事件驱动模型来说Swing是一个绝佳的起点。虽然其外观可能不如现代UI库炫酷但用于构建功能完备的桌面应用完全足够且能让你更专注于业务逻辑而非界面美化。SQLite 它是一个轻量级的、无服务器的、自包含的SQL数据库引擎。整个数据库就是一个单一的.db或.sqlite文件非常适合嵌入式或桌面应用。我们无需像MySQL或PostgreSQL那样安装和配置数据库服务JDBC驱动加载后即可直接操作文件极大简化了部署和分发。JDBC Java数据库连接的标准API。通过它我们可以用统一的Java代码来操作SQLite数据库执行SQL语句。学习JDBC是理解Java后端如何与数据库交互的基础。这个技术组合零外部依赖除了SQLite的JDBC驱动jar包环境搭建简单能让你快速聚焦于核心开发流程。2. 环境准备与项目初始化2.1 开发环境要求操作系统 Windows 10/11, macOS, Linux (本文示例以Windows环境为主命令有差异时会说明)。Java SDK JDK 8 或更高版本 (推荐 JDK 11 或 17 LTS)。确保java和javac命令可以在命令行中运行。集成开发环境 (IDE) IntelliJ IDEA (社区版即可) 或 Eclipse。本文使用 IntelliJ IDEA 进行演示因其对Java项目管理和依赖管理非常友好。SQLite JDBC 驱动 我们需要一个.jar文件来连接SQLite数据库。我们将从Maven中央仓库获取。2.2 创建项目与导入驱动创建新项目 打开IntelliJ IDEA选择New Project。左侧选择Java不要选择Maven或Gradle为了最简化直接点击Next。输入项目名称例如PersonalFinanceLedger选择项目位置点击Finish。下载 SQLite JDBC 驱动访问 Maven 仓库网站搜索sqlite-jdbc。选择一个较新的稳定版本例如3.45.1.0点击进入详情页。找到JAR文件的下载链接下载sqlite-jdbc-3.45.1.0.jar到本地。将驱动 Jar 包添加到项目依赖在IDEA的项目视图中右键点击你的项目根目录 -Open Module Settings(或按F4)。在弹出窗口左侧选择Libraries。点击-Java然后浏览并选择你刚刚下载的sqlite-jdbc-3.45.1.0.jar文件点击OK。确保该库被添加到你的模块依赖中。2.3 项目目录结构规划一个清晰的结构有助于代码管理。在项目的src目录下创建以下包和类src/ ├── com.yourname.finance/ │ ├── model/ │ │ └── Transaction.java // 交易数据模型 (Java Bean) │ ├── dao/ │ │ └── TransactionDAO.java // 数据访问对象负责所有数据库操作 │ ├── util/ │ │ └── DBUtil.java // 数据库连接工具类 │ └── view/ │ ├── MainFrame.java // 程序主窗口 │ ├── AddEditDialog.java // 添加/编辑记录的对话框 │ └── ChartPanel.java // 统计图表面板 (可选) └── resources/ // 存放资源文件如图标、数据库文件 └── finance.db // SQLite数据库文件 (首次运行后生成)注意com.yourname.finance应替换为你自己的包名。3. 核心模块设计与实现我们将采用经典的三层架构思想模型(Model) - 数据访问层(DAO) - 视图(View)。3.1 模型层 (Model)定义数据结构首先创建Transaction类它代表一条记账记录。这是一个简单的Java Bean包含属性、构造方法、getter和setter。// 文件路径src/com/yourname/finance/model/Transaction.java package com.yourname.finance.model; import java.math.BigDecimal; import java.time.LocalDate; /** * 交易记录实体类 */ public class Transaction { private Integer id; // 主键自增长 private LocalDate date; // 交易日期 private String type; // 类型INCOME(收入) / EXPENSE(支出) private String category; // 类别如“餐饮”、“交通”、“工资”等 private BigDecimal amount; // 金额使用BigDecimal避免精度问题 private String description; // 备注 // 无参构造器 (通常用于反射或框架) public Transaction() { } // 全参构造器 (用于方便创建对象) public Transaction(Integer id, LocalDate date, String type, String category, BigDecimal amount, String description) { this.id id; this.date date; this.type type; this.category category; this.amount amount; this.description description; } // 省略所有属性的 getter 和 setter 方法... // 在IDE中可以使用快捷键如AltInsert自动生成。 public Integer getId() { return id; } public void setId(Integer id) { this.id id; } public LocalDate getDate() { return date; } public void setDate(LocalDate date) { this.date date; } public String getType() { return type; } public void setType(String type) { this.type type; } public String getCategory() { return category; } public void setCategory(String category) { this.category category; } public BigDecimal getAmount() { return amount; } public void setBigDecimal(BigDecimal amount) { this.amount amount; } public String getDescription() { return description; } public void setDescription(String description) { this.description description; } Override public String toString() { return Transaction{ id id , date date , type type \ , category category \ , amount amount , description description \ }; } }关键点使用Integer而非int是为了能表示null例如新记录尚未存入数据库时。使用LocalDate处理日期比旧的Date类更直观、安全。使用BigDecimal表示金额这是金融计算的标准能避免double或float带来的精度丢失问题。良好的toString()方法便于调试。3.2 数据访问层 (DAO)封装数据库操作这是项目的核心负责与SQLite数据库交互。我们创建一个TransactionDAO类。首先我们需要一个数据库连接工具类DBUtil.java// 文件路径src/com/yourname/finance/util/DBUtil.java package com.yourname.finance.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; /** * 数据库连接工具类 (单例模式) */ public class DBUtil { private static final String DB_URL jdbc:sqlite:finance.db; // 数据库文件相对路径 private static Connection connection null; // 私有构造器防止外部实例化 private DBUtil() {} /** * 获取数据库连接 * return Connection 对象如果获取失败则返回 null */ public static Connection getConnection() { if (connection null) { try { // 加载SQLite JDBC驱动 (新版驱动通常会自动注册) Class.forName(org.sqlite.JDBC); // 建立连接。如果 finance.db 文件不存在SQLite会自动创建它。 connection DriverManager.getConnection(DB_URL); System.out.println(数据库连接成功); } catch (ClassNotFoundException | SQLException e) { System.err.println(连接数据库失败: e.getMessage()); e.printStackTrace(); return null; } } return connection; } /** * 关闭数据库连接 (本项目简单处理实际可根据需要调整) */ public static void closeConnection() { if (connection ! null) { try { connection.close(); connection null; System.out.println(数据库连接已关闭。); } catch (SQLException e) { System.err.println(关闭数据库连接时出错: e.getMessage()); } } } }接下来是核心的TransactionDAO.java// 文件路径src/com/yourname/finance/dao/TransactionDAO.java package com.yourname.finance.dao; import com.yourname.finance.model.Transaction; import com.yourname.finance.util.DBUtil; import java.math.BigDecimal; import java.sql.*; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; /** * 交易记录的数据访问对象 */ public class TransactionDAO { /** * 初始化数据库创建表如果不存在 */ public void initDatabase() { String sql CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, // SQLite没有专门的DATE类型用TEXT存储ISO格式字符串 type TEXT NOT NULL CHECK(type IN (INCOME, EXPENSE)), category TEXT NOT NULL, amount DECIMAL(10, 2) NOT NULL, // 总共10位小数2位 description TEXT );; try (Connection conn DBUtil.getConnection(); Statement stmt conn.createStatement()) { stmt.execute(sql); System.out.println(表创建成功或已存在。); } catch (SQLException e) { System.err.println(初始化数据库失败: e.getMessage()); e.printStackTrace(); } } /** * 添加一条交易记录 * param transaction 要添加的交易对象 * return 添加成功返回生成的ID失败返回-1 */ public int addTransaction(Transaction transaction) { String sql INSERT INTO transactions (date, type, category, amount, description) VALUES (?, ?, ?, ?, ?); int generatedId -1; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { pstmt.setString(1, transaction.getDate().toString()); // LocalDate转String pstmt.setString(2, transaction.getType()); pstmt.setString(3, transaction.getCategory()); pstmt.setBigDecimal(4, transaction.getAmount()); pstmt.setString(5, transaction.getDescription()); int affectedRows pstmt.executeUpdate(); if (affectedRows 0) { ResultSet rs pstmt.getGeneratedKeys(); if (rs.next()) { generatedId rs.getInt(1); } } } catch (SQLException e) { System.err.println(添加记录失败: e.getMessage()); e.printStackTrace(); } return generatedId; } /** * 查询所有交易记录 * return 交易记录列表 */ public ListTransaction getAllTransactions() { ListTransaction list new ArrayList(); String sql SELECT * FROM transactions ORDER BY date DESC, id DESC; // 按日期倒序最新的在前 try (Connection conn DBUtil.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { Transaction t new Transaction(); t.setId(rs.getInt(id)); t.setDate(LocalDate.parse(rs.getString(date))); // String转LocalDate t.setType(rs.getString(type)); t.setCategory(rs.getString(category)); t.setBigDecimal(rs.getBigDecimal(amount)); t.setDescription(rs.getString(description)); list.add(t); } } catch (SQLException e) { System.err.println(查询所有记录失败: e.getMessage()); e.printStackTrace(); } return list; } /** * 根据ID更新一条交易记录 * param transaction 包含更新信息的交易对象其id必须有效 * return 更新是否成功 */ public boolean updateTransaction(Transaction transaction) { String sql UPDATE transactions SET date?, type?, category?, amount?, description? WHERE id?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, transaction.getDate().toString()); pstmt.setString(2, transaction.getType()); pstmt.setString(3, transaction.getCategory()); pstmt.setBigDecimal(4, transaction.getAmount()); pstmt.setString(5, transaction.getDescription()); pstmt.setInt(6, transaction.getId()); int affectedRows pstmt.executeUpdate(); return affectedRows 0; } catch (SQLException e) { System.err.println(更新记录失败: e.getMessage()); e.printStackTrace(); return false; } } /** * 根据ID删除一条交易记录 * param id 要删除的记录ID * return 删除是否成功 */ public boolean deleteTransaction(int id) { String sql DELETE FROM transactions WHERE id?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setInt(1, id); int affectedRows pstmt.executeUpdate(); return affectedRows 0; } catch (SQLException e) { System.err.println(删除记录失败: e.getMessage()); e.printStackTrace(); return false; } } /** * 根据条件查询交易记录示例按月份和类型筛选 * param yearMonth 格式如 2024-05 * param type 类型可为 null 表示查询所有类型 * return 符合条件的交易记录列表 */ public ListTransaction getTransactionsByCondition(String yearMonth, String type) { ListTransaction list new ArrayList(); StringBuilder sqlBuilder new StringBuilder(SELECT * FROM transactions WHERE 11 ); ListObject params new ArrayList(); if (yearMonth ! null !yearMonth.trim().isEmpty()) { sqlBuilder.append(AND strftime(%Y-%m, date) ? ); params.add(yearMonth); } if (type ! null !type.trim().isEmpty()) { sqlBuilder.append(AND type ? ); params.add(type); } sqlBuilder.append(ORDER BY date DESC); try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sqlBuilder.toString())) { for (int i 0; i params.size(); i) { pstmt.setObject(i 1, params.get(i)); } ResultSet rs pstmt.executeQuery(); while (rs.next()) { Transaction t new Transaction(); t.setId(rs.getInt(id)); t.setDate(LocalDate.parse(rs.getString(date))); t.setType(rs.getString(type)); t.setCategory(rs.getString(category)); t.setBigDecimal(rs.getBigDecimal(amount)); t.setDescription(rs.getString(description)); list.add(t); } } catch (SQLException e) { System.err.println(条件查询失败: e.getMessage()); e.printStackTrace(); } return list; } /** * 获取统计信息总收入、总支出、结余 * return 一个包含三个BigDecimal的数组[总收入 总支出 结余] */ public BigDecimal[] getStatistics() { BigDecimal totalIncome BigDecimal.ZERO; BigDecimal totalExpense BigDecimal.ZERO; String sqlIncome SELECT SUM(amount) FROM transactions WHERE typeINCOME; String sqlExpense SELECT SUM(amount) FROM transactions WHERE typeEXPENSE; try (Connection conn DBUtil.getConnection(); Statement stmt conn.createStatement()) { ResultSet rsIncome stmt.executeQuery(sqlIncome); if (rsIncome.next()) { totalIncome rsIncome.getBigDecimal(1); if (totalIncome null) totalIncome BigDecimal.ZERO; } ResultSet rsExpense stmt.executeQuery(sqlExpense); if (rsExpense.next()) { totalExpense rsExpense.getBigDecimal(1); if (totalExpense null) totalExpense BigDecimal.ZERO; } } catch (SQLException e) { System.err.println(获取统计信息失败: e.getMessage()); e.printStackTrace(); } BigDecimal balance totalIncome.subtract(totalExpense); return new BigDecimal[]{totalIncome, totalExpense, balance}; } }关键点与最佳实践使用PreparedStatement 所有带参数的SQL都使用PreparedStatement可以有效防止SQL注入攻击并且性能更好。资源自动关闭 使用try-with-resources语法确保ConnectionStatementResultSet等资源在使用后被自动关闭避免内存泄漏。异常处理 捕获SQLException并打印日志在实际项目中应使用日志框架如Log4j2, SLF4J而非System.err。条件查询构建getTransactionsByCondition方法展示了如何动态构建SQL语句这是一种灵活的处理方式。BigDecimal处理空值 在统计查询中如果SUM结果为NULL表为空时需要手动转换为BigDecimal.ZERO。3.3 视图层 (View)构建图形用户界面这是用户直接交互的部分。我们将创建主窗口MainFrame.java。// 文件路径src/com/yourname/finance/view/MainFrame.java package com.yourname.finance.view; import com.yourname.finance.dao.TransactionDAO; import com.yourname.finance.model.Transaction; import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.awt.event.*; import java.math.BigDecimal; import java.util.List; /** * 记账本主界面 */ public class MainFrame extends JFrame { private TransactionDAO transactionDAO; private JTable table; private DefaultTableModel tableModel; private JLabel lblIncome, lblExpense, lblBalance; // 表格列名 private final String[] columnNames {ID, 日期, 类型, 类别, 金额, 备注}; public MainFrame() { transactionDAO new TransactionDAO(); initDatabase(); initUI(); loadTransactionData(); updateStatistics(); } private void initDatabase() { transactionDAO.initDatabase(); } private void initUI() { setTitle(个人记账本 - Java Swing SQLite); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(900, 600); setLocationRelativeTo(null); // 窗口居中 // 1. 顶部按钮面板 JPanel topPanel new JPanel(new FlowLayout(FlowLayout.LEFT)); JButton btnAdd new JButton(新增记录); JButton btnEdit new JButton(编辑记录); JButton btnDelete new JButton(删除记录); JButton btnRefresh new JButton(刷新); JButton btnChart new JButton(查看图表); topPanel.add(btnAdd); topPanel.add(btnEdit); topPanel.add(btnDelete); topPanel.add(btnRefresh); topPanel.add(btnChart); // 2. 中间表格面板 tableModel new DefaultTableModel(columnNames, 0) { Override public boolean isCellEditable(int row, int column) { // 禁止直接编辑表格单元格通过按钮操作 return false; } }; table new JTable(tableModel); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // 单选 JScrollPane scrollPane new JScrollPane(table); // 3. 底部统计信息面板 JPanel bottomPanel new JPanel(new FlowLayout(FlowLayout.CENTER)); lblIncome new JLabel(总收入: 0.00); lblExpense new JLabel(总支出: 0.00); lblBalance new JLabel(结余: 0.00); lblIncome.setForeground(Color.GREEN.darker()); lblExpense.setForeground(Color.RED); lblBalance.setForeground(Color.BLUE); bottomPanel.add(lblIncome); bottomPanel.add(Box.createHorizontalStrut(20)); // 添加间距 bottomPanel.add(lblExpense); bottomPanel.add(Box.createHorizontalStrut(20)); bottomPanel.add(lblBalance); // 4. 组装主界面 setLayout(new BorderLayout()); add(topPanel, BorderLayout.NORTH); add(scrollPane, BorderLayout.CENTER); add(bottomPanel, BorderLayout.SOUTH); // 5. 按钮事件监听 btnAdd.addActionListener(e - addTransaction()); btnEdit.addActionListener(e - editTransaction()); btnDelete.addActionListener(e - deleteTransaction()); btnRefresh.addActionListener(e - refreshData()); btnChart.addActionListener(e - showChart()); } /** * 加载数据到表格 */ private void loadTransactionData() { // 清空现有数据 tableModel.setRowCount(0); ListTransaction transactions transactionDAO.getAllTransactions(); for (Transaction t : transactions) { Object[] rowData { t.getId(), t.getDate().toString(), t.getType(), t.getCategory(), t.getAmount(), t.getDescription() }; tableModel.addRow(rowData); } } /** * 更新底部统计信息 */ private void updateStatistics() { BigDecimal[] stats transactionDAO.getStatistics(); lblIncome.setText(String.format(总收入: %.2f, stats[0])); lblExpense.setText(String.format(总支出: %.2f, stats[1])); lblBalance.setText(String.format(结余: %.2f, stats[2])); } /** * 刷新数据重新加载表格和统计 */ private void refreshData() { loadTransactionData(); updateStatistics(); JOptionPane.showMessageDialog(this, 数据已刷新, 提示, JOptionPane.INFORMATION_MESSAGE); } /** * 新增记录 */ private void addTransaction() { // 这里可以弹出一个自定义的对话框AddEditDialog // 为了简化我们先使用JOptionPane输入 // 实际项目中强烈建议创建一个独立的Dialog类 JTextField dateField new JTextField(java.time.LocalDate.now().toString()); JComboBoxString typeCombo new JComboBox(new String[]{INCOME, EXPENSE}); JTextField categoryField new JTextField(); JTextField amountField new JTextField(); JTextField descField new JTextField(); Object[] message { 日期 (YYYY-MM-DD):, dateField, 类型:, typeCombo, 类别:, categoryField, 金额:, amountField, 备注:, descField }; int option JOptionPane.showConfirmDialog(this, message, 新增记录, JOptionPane.OK_CANCEL_OPTION); if (option JOptionPane.OK_OPTION) { try { Transaction t new Transaction(); t.setDate(java.time.LocalDate.parse(dateField.getText().trim())); t.setType((String) typeCombo.getSelectedItem()); t.setCategory(categoryField.getText().trim()); t.setBigDecimal(new BigDecimal(amountField.getText().trim())); t.setDescription(descField.getText().trim()); int newId transactionDAO.addTransaction(t); if (newId 0) { refreshData(); JOptionPane.showMessageDialog(this, 记录添加成功ID: newId, 成功, JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(this, 记录添加失败, 错误, JOptionPane.ERROR_MESSAGE); } } catch (Exception ex) { JOptionPane.showMessageDialog(this, 输入数据格式错误\n ex.getMessage(), 输入错误, JOptionPane.ERROR_MESSAGE); } } } /** * 编辑选中记录 */ private void editTransaction() { int selectedRow table.getSelectedRow(); if (selectedRow -1) { JOptionPane.showMessageDialog(this, 请先选择要编辑的记录, 提示, JOptionPane.WARNING_MESSAGE); return; } // 获取选中行的数据 int id (int) tableModel.getValueAt(selectedRow, 0); String date (String) tableModel.getValueAt(selectedRow, 1); String type (String) tableModel.getValueAt(selectedRow, 2); String category (String) tableModel.getValueAt(selectedRow, 3); BigDecimal amount (BigDecimal) tableModel.getValueAt(selectedRow, 4); String desc (String) tableModel.getValueAt(selectedRow, 5); // 弹出编辑对话框这里同样简化实际应使用独立Dialog JTextField dateField new JTextField(date); JComboBoxString typeCombo new JComboBox(new String[]{INCOME, EXPENSE}); typeCombo.setSelectedItem(type); JTextField categoryField new JTextField(category); JTextField amountField new JTextField(amount.toString()); JTextField descField new JTextField(desc); Object[] message { 日期 (YYYY-MM-DD):, dateField, 类型:, typeCombo, 类别:, categoryField, 金额:, amountField, 备注:, descField }; int option JOptionPane.showConfirmDialog(this, message, 编辑记录, JOptionPane.OK_CANCEL_OPTION); if (option JOptionPane.OK_OPTION) { try { Transaction t new Transaction(); t.setId(id); t.setDate(java.time.LocalDate.parse(dateField.getText().trim())); t.setType((String) typeCombo.getSelectedItem()); t.setCategory(categoryField.getText().trim()); t.setBigDecimal(new BigDecimal(amountField.getText().trim())); t.setDescription(descField.getText().trim()); boolean success transactionDAO.updateTransaction(t); if (success) { refreshData(); JOptionPane.showMessageDialog(this, 记录更新成功, 成功, JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(this, 记录更新失败, 错误, JOptionPane.ERROR_MESSAGE); } } catch (Exception ex) { JOptionPane.showMessageDialog(this, 输入数据格式错误\n ex.getMessage(), 输入错误, JOptionPane.ERROR_MESSAGE); } } } /** * 删除选中记录 */ private void deleteTransaction() { int selectedRow table.getSelectedRow(); if (selectedRow -1) { JOptionPane.showMessageDialog(this, 请先选择要删除的记录, 提示, JOptionPane.WARNING_MESSAGE); return; } int id (int) tableModel.getValueAt(selectedRow, 0); int confirm JOptionPane.showConfirmDialog(this, 确定要删除ID为 id 的记录吗, 确认删除, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); if (confirm JOptionPane.YES_OPTION) { boolean success transactionDAO.deleteTransaction(id); if (success) { refreshData(); JOptionPane.showMessageDialog(this, 记录删除成功, 成功, JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(this, 记录删除失败, 错误, JOptionPane.ERROR_MESSAGE); } } } /** * 显示统计图表功能预留可扩展 */ private void showChart() { JOptionPane.showMessageDialog(this, 图表功能待实现可集成JFreeChart等库。, 功能预览, JOptionPane.INFORMATION_MESSAGE); // 未来可以在这里创建并显示一个ChartPanel } public static void main(String[] args) { // 使用SwingUtilities确保GUI创建在事件分发线程上 SwingUtilities.invokeLater(() - { MainFrame frame new MainFrame(); frame.setVisible(true); }); } }3.4 运行与测试确保所有代码已正确编写项目结构完整。在MainFrame类中右键选择Run MainFrame.main()。程序启动后会自动在项目根目录下创建finance.db数据库文件如果不存在。点击“新增记录”按钮输入测试数据。观察表格是否成功显示数据底部统计信息是否正确更新。尝试编辑和删除功能。至此一个具备基本CRUD功能的可视化记账本已经完成4. 功能扩展与优化建议基础版本已经可用但一个健壮、好用的应用还需要更多打磨。以下是一些扩展方向4.1 改进用户界面 (UI/UX)使用更专业的对话框 将JOptionPane输入替换为自定义的JDialog例如AddEditDialog。可以设计更合理的布局添加日期选择器JDatePicker下拉框预置常用类别等。表格美化 设置表格行高、列宽为“收入”和“支出”设置不同的颜色如收入绿色、支出红色。菜单栏 添加JMenuBar包含“文件”(退出、导出)、“编辑”(增删改)、“视图”(筛选)、“帮助”等菜单。筛选与搜索 在界面顶部增加筛选组件如按月份筛选的下拉框、按类别筛选的组合框、关键词搜索框等并连接TransactionDAO中的条件查询方法。4.2 增强数据层 (DAO)类别管理 创建独立的Category表和CategoryDAO实现类别的增删改查并在记账时从数据库动态加载类别而不是硬编码。分页查询 当数据量很大时实现分页功能避免一次性加载所有数据。数据库连接池 使用如 HikariCP 等连接池管理数据库连接提升性能对于SQLite桌面应用此需求不强烈但作为学习很好。事务处理 对于涉及多个步骤的操作如批量删除使用Connection的setAutoCommit(false)、commit()、rollback()来保证数据一致性。4.3 添加数据可视化集成图表库 使用JFreeChart这个强大的Java图表库来生成统计图表。在项目中添加 JFreeChart 依赖通过Maven或下载JAR。创建ChartPanel类编写方法生成饼图按类别支出比例、柱状图月度收支对比等。在主界面点击“查看图表”时弹出一个新的JFrame或JDialog来展示ChartPanel。4.4 数据导入导出导出为Excel/CSV 使用Apache POI或OpenCSV库将表格数据导出为.xlsx或.csv文件方便用户用其他软件分析。数据备份与恢复 提供功能将整个finance.db文件复制到指定位置进行备份或从备份文件恢复。4.5 异常处理与用户体验统一的错误提示 使用更友好的对话框提示错误而不是在控制台打印堆栈。输入验证 在对话框提交前验证日期格式、金额是否为数字、必填项是否为空等。日志记录 引入SLF4J Logback记录程序运行日志、用户操作和错误信息便于排查问题。5. 常见问题与排查思路在开发过程中你可能会遇到以下问题问题现象可能原因解决思路程序启动时报ClassNotFoundException: org.sqlite.JDBCSQLite JDBC驱动JAR包未正确添加到项目依赖中。1. 检查Project Structure-Libraries中是否有sqlite-jdbc库。2. 检查JAR文件路径是否正确尝试删除后重新添加。3. 如果使用Maven/Gradle检查pom.xml或build.gradle依赖是否正确。运行后找不到finance.db文件或数据未保存数据库文件路径问题。jdbc:sqlite:finance.db是相对路径它会在程序启动的当前工作目录下创建文件。在IDE中运行工作目录通常是项目根目录。1. 在项目根目录下寻找finance.db文件。2. 使用绝对路径如jdbc:sqlite:C:/Users/YourName/data/finance.db。3. 在代码中打印new File(.).getAbsolutePath()查看当前工作目录。表格中金额显示为科学计数法或格式不对BigDecimal在JTable中默认的渲染器可能不是我们想要的货币格式。1. 自定义表格的列渲染器。为金额列设置一个DefaultTableCellRenderer重写其getTableCellRendererComponent方法将数值格式化为两位小数。2. 或者在向tableModel添加行数据时将BigDecimal先格式化为String。编辑/删除操作后表格数据没刷新操作数据库成功后没有刷新tableModel中的数据。确保在addTransaction,updateTransaction,deleteTransaction等方法执行成功后调用loadTransactionData()或refreshData()重新从数据库加载数据并更新表格模型。界面布局错乱或按钮无响应Swing组件创建和更新未在事件分发线程(EDT)上执行。确保所有对Swing组件的创建和修改都包装在SwingUtilities.invokeLater()中。主方法中的setVisible(true)已经这样做了。对于后台线程如从数据库加载大量数据更新UI时也必须使用SwingUtilities.invokeLater()。日期处理报错DateTimeParseException输入的日期字符串格式与LocalDate.parse()期望的ISO格式 (yyyy-MM-dd) 不符。1. 在前端使用日期选择器控件避免手动输入。2. 如果必须手动输入在解析前进行格式校验或使用DateTimeFormatter指定格式进行解析。6. 项目总结与学习路线通过这个“Java Swing SQLite 记账本”项目我们完成了一个小型桌面应用的完整开发流程。你不仅实践了Java核心语法更重要的是掌握了如何将界面(Swing)、业务逻辑(Java Bean/Service)和数据持久化(JDBC SQLite)三层清晰地组织在一起这是任何规模应用开发的基础思想。本文涉及的核心技能点回顾Java SE 面向对象、集合框架、异常处理、时间日期API (LocalDate)、数值计算 (BigDecimal)。Swing GUI 窗口 (JFrame)、面板 (JPanel)、表格 (JTable)、按钮 (JButton)、标签 (JLabel)、对话框 (JOptionPane)、布局管理器 (BorderLayout,FlowLayout)、事件监听 (ActionListener)。JDBC 加载驱动、获取连接 (Connection)、执行语句 (Statement,PreparedStatement)、处理结果集 (ResultSet)、事务控制基础。SQLite 嵌入式数据库的使用基本的SQL语法CREATE TABLE,INSERT,SELECT,UPDATE,DELETE,SUM。项目结构 分层架构Model, DAO, View包的组织工具类的编写。下一步可以深入学习的方向升级技术栈 尝试用JavaFX替代 Swing 构建更现代、更易维护的UI。学习使用Maven或Gradle管理项目依赖。深化数据库 学习更复杂的SQL查询多表连接、子查询、索引优化。尝试使用MyBatis或Hibernate这样的ORM框架来简化数据库操作。架构演进 将业务逻辑从DAO和View中抽离出来形成独立的Service层使结构更清晰。学习设计模式如单例模式我们的DBUtil、观察者模式Swing事件模型本身就是等。工程化实践 为项目添加单元测试JUnit学习使用版本控制Git编写项目文档README。这个项目是一个绝佳的起点你可以基于它不断添加新功能例如多用户登录、预算管理、周期性账单、数据报表等在实践中持续提升你的Java全栈开发能力。动手将文中的代码敲一遍并尝试实现一个扩展功能是巩固学习成果的最佳方式。