简介:这是一款基于Java开发的轻量级个人记账本程序,面向编程初学者与Java入门实践者,聚焦财务数据管理核心场景,帮助用户掌握GUI界面设计、文件持久化(CSV/JSON)、数据统计可视化及基础安全机制等典型桌面应用开发技能。资源包共104个文件,含40个可读性良好的Java源码(覆盖DAO层、GUI面板、图表工具、备份监听等模块)、46个编译后class文件、8个界面图标PNG、4个依赖JAR包,以及项目配置、SQL建表脚本和README说明,整体2.32MB,结构清晰,便于分层学习与调试。已有52人下载学习,适合通过完整小项目理解MVC分层思想、Swing组件协作与本地数据流闭环。读者可直接运行体验收支录入、分类统计、饼图展示及密码保护功能,并基于源码快速拓展云端同步或主题定制等进阶特性。
1. 一个Java版的小小记账本程序:轻量、可运行、无依赖,适合新手练手与日常记账场景闭环验证
你有没有试过:下载一个标着“Java记账本”的压缩包,解压后双击xxx.jar没反应?或者javac Main.java报错说找不到JFrame?又或者好不容易跑起来了,输入一笔早餐5元,关掉再打开——数据全没了?这不是玄学,是绝大多数“Java小项目”压缩包的真实交付状态。这个标题里的“一个java版的小小记账本程序.zip”,不是演示工程、不是教学PPT配套代码,而是一个能真正从命令行编译、启动、增删改查、数据落盘、二次修改不崩的最小可行记账本。它不连数据库,不用Spring,不调API,就靠java.awt+javax.swing+java.io三件套,把「记一笔→存本地→下次还能读出来」这件事闭环做透。适合两类人:一是刚学完Java基础语法、正卡在“写完HelloWorld后不知道该练什么”的新手;二是需要快速验证一个轻量桌面工具逻辑、不想被Maven生命周期和模块依赖绕晕的务实开发者。它不炫技,但每行代码都经得起javac -Xlint扫描;它没UI框架,但所有按钮点击都有明确响应路径;它用文本文件存数据,但做了字段分隔、换行转义、写入原子性三重防护——这才是“小小记账本”该有的样子。
2. 从解压到运行:5分钟内让记账本在本地真实动起来
2.1 解压结构识别与核心文件定位
拿到一个java版的小小记账本程序.zip后,先别急着双击。用任意解压工具(如7-Zip、Windows自带解压)打开,观察目录结构。常见但危险的结构是直接放一个.jar文件——这种往往缺少源码、无法调试、更新即失联。真正可维护的结构应包含以下三类文件:
src/目录(必有):里面是.java源文件,通常含Main.java(主入口)、Record.java(记账条目实体)、DataHandler.java(数据读写逻辑)data/目录(推荐有):存放records.txt或accounts.csv等纯文本数据文件README.md或说明.txt(加分项):写明编译命令、运行方式、数据格式说明
提示:若解压后只有
.jar和空文件夹,说明作者未提供源码,后续无法修改功能(如加分类、改日期格式),建议放弃;若看到pom.xml或build.gradle,则属于进阶项目,与本标题“小小”定位不符,暂不处理。
2.2 命令行编译:用最原始的方式确认环境兼容性
确保已安装 JDK 8 或更高版本(java -version输出应含1.8+)。进入解压后的根目录,在终端执行:
# 查看当前目录下所有.java文件(确认源码存在) ls -l src/*.java # 进入src目录,编译全部Java文件(-d 指定输出class目录) cd src javac -d ../out *.java # 编译成功后,返回根目录,检查out目录是否生成对应class文件 cd .. ls -l out/关键参数说明:
-d ../out:强制将编译生成的.class文件输出到上层out/目录,避免.class和.java混在一起导致ClassNotFoundException*.java:通配符编译所有Java文件,比逐个写文件名更可靠(尤其当项目含GUIHelper.java等辅助类时)
若编译失败,常见原因及自查点:
- 报错
error: package javax.swing does not exist→ JDK安装不完整,需重装标准版JDK(非JRE) - 报错
error: cannot find symbol class XXX→ 某个Java文件里引用了未定义的类,检查src/下是否漏文件(如缺Record.java) - 报错
error: invalid flag: -d→ 使用了旧版JDK(<1.6),升级JDK即可
2.3 启动程序:绕过双击失效陷阱,用java -cp精准加载
不要双击xxx.jar!也不要直接java Main(会报NoClassDefFoundError)。正确启动命令如下:
# 方式一:从out目录启动(推荐,路径最清晰) cd out java -cp . Main # 方式二:从项目根目录启动(需指定class路径) java -cp out Main为什么必须用-cp?
Java默认只在当前目录找class,而我们的class在out/下。-cp .表示“当前目录(即out)为类路径”,-cp out表示“out目录为类路径”。省略-cp会导致JVM找不到Main.class,这是新手翻车最高发场景。
启动后应看到什么?
一个带标题栏的窗口,顶部有「添加记录」「查看所有」「清空数据」三个按钮,中部是表格区域(初始为空),底部有状态栏显示“共0条记录”。此时程序已活,可进行下一步操作验证。
3. 数据持久化机制拆解:文本文件如何安全存取记账记录
3.1 数据文件格式设计:为什么选制表符分隔而非JSON或CSV
本程序使用纯文本data/records.txt存储数据,每行一条记录,字段间用制表符\t分隔。典型内容如下:
2024-05-20 早餐 5.0 便利店 2024-05-21 交通 12.5 地铁 2024-05-22 餐饮 68.0 餐厅 朋友聚餐选择\t而非,或|的理由:
- 逗号
,在备注中极常见(如“买咖啡,面包”),CSV解析易错位; - 竖线
|需转义且视觉干扰大; - 制表符
\t在中文输入法下极少手动输入,天然规避冲突,且String.split("\t")解析稳定。
字段顺序固定为:日期\t类别\t金额\t地点\t备注,共5列。缺失字段(如无备注)留空,但列数必须为5,否则解析时会数组越界。
3.2 写入逻辑:原子性保障与异常兜底
查看DataHandler.java中的saveRecords(List<Record> records)方法,核心逻辑如下:
public static void saveRecords(List<Record> records) { try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter( new FileOutputStream("data/records.txt"), "UTF-8"))) { for (Record r : records) { // 关键:对备注字段做制表符转义(防止解析错列) String escapedRemark = r.getRemark().replace("\t", "\\t"); String line = String.join("\t", r.getDate(), r.getCategory(), String.valueOf(r.getAmount()), r.getLocation(), escapedRemark); writer.write(line); writer.newLine(); } // 关键:flush确保缓冲区数据真正写入磁盘 writer.flush(); } catch (IOException e) { // 重要:记录错误但不中断程序(用户仍可继续操作) System.err.println("保存失败:" + e.getMessage()); // 可选:弹窗提示用户(此处省略Swing UI代码) } }关键防护点说明:
try-with-resources:自动关闭流,避免文件句柄泄漏(Windows下常导致“文件被占用”);escapedRemark.replace("\t", "\\t"):将用户输入的制表符转义为字面量\t,读取时再还原,保证列对齐;writer.flush():强制刷盘,防止断电或强制退出导致最后几条记录丢失;catch中仅打印错误不抛出:记账是高频操作,单次保存失败不应让整个UI崩溃。
3.3 读取逻辑:容错解析与空行/注释行跳过
loadRecords()方法需处理现实中的脏数据,代码片段如下:
public static List<Record> loadRecords() { List<Record> records = new ArrayList<>(); try (BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream("data/records.txt"), "UTF-8"))) { String line; while ((line = reader.readLine()) != null) { // 跳过空行和以#开头的注释行(方便人工维护) if (line.trim().isEmpty() || line.trim().startsWith("#")) { continue; } String[] parts = line.split("\t", -1); // -1保留末尾空字段 if (parts.length < 4) continue; // 至少需日期、类别、金额、地点 try { Record r = new Record(); r.setDate(parts[0].trim()); r.setCategory(parts[1].trim()); r.setAmount(Double.parseDouble(parts[2].trim())); r.setLocation(parts[3].trim()); // 还原转义的制表符:\\t → \t String remark = parts.length > 4 ? parts[4] : ""; r.setRemark(remark.replace("\\t", "\t")); records.add(r); } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { // 忽略单行解析失败,继续下一行(防数据文件损坏) System.err.println("跳过无效行:" + line); } } } catch (FileNotFoundException e) { // 首次运行时data/records.txt不存在,静默创建空列表 System.out.println("数据文件不存在,初始化空记录列表"); } catch (IOException e) { System.err.println("读取失败:" + e.getMessage()); } return records; }容错设计细节:
split("\t", -1):第三个参数-1确保即使末尾有空字段(如无备注),parts数组长度也保持5,避免ArrayIndexOutOfBoundsException;trim()清除首尾空格:防止用户复制粘贴时带不可见空格导致日期解析失败;FileNotFoundException单独捕获:首次运行无数据文件是正常场景,不报错;- 单行解析异常
continue:某一行格式错误(如金额写成“五元”)不影响其他数据加载。
4. GUI交互逻辑验证:按钮点击背后的事件链与状态同步
4.1 添加记录按钮:从输入校验到界面刷新的完整链路
点击「添加记录」按钮触发AddRecordAction类(或匿名内部类),其actionPerformed方法执行以下步骤:
- 弹出输入对话框:用
JOptionPane.showInputDialog获取用户输入,按\n分割为多行(日期、类别、金额、地点、备注); - 强校验:检查日期格式(
yyyy-MM-dd)、金额是否为数字、金额是否 >0; - 构建Record对象:将输入值赋给
Record实例的各字段; - 追加到内存列表:
records.add(newRecord); - 立即刷新表格:调用
tableModel.addRow(...)更新JTable显示; - 异步落盘:新开线程调用
DataHandler.saveRecords(records),避免UI卡顿。
关键设计点:
- 校验前置:金额非数字时直接
return,不创建对象、不更新内存、不落盘,杜绝脏数据入库; - 表格刷新即时性:
addRow后用户立刻看到新行,建立操作反馈闭环; - 落盘异步化:
new Thread(() -> DataHandler.saveRecords(records)).start(),避免大文件写入时界面假死。
4.2 查看所有按钮:表格模型与数据源的双向绑定
JTable使用DefaultTableModel,其数据源为List<Record>。当点击「查看所有」时,并非重新读取文件,而是直接遍历内存中的records列表并清空后逐行添加:
// 清空表格模型 tableModel.setRowCount(0); // 从内存列表重建表格 for (Record r : records) { tableModel.addRow(new Object[]{ r.getDate(), r.getCategory(), r.getAmount(), r.getLocation(), r.getRemark() }); }为什么不用实时读文件?
- 避免重复IO开销(用户可能频繁切换查看/添加);
- 保证内存与文件数据一致性(所有变更均通过
DataHandler统一入口); records列表是单一可信数据源,UI只是视图。
4.3 清空数据按钮:物理删除与内存重置的协同
点击「清空数据」执行两步操作:
// 步骤1:物理删除data/records.txt文件 File dataFile = new File("data/records.txt"); if (dataFile.exists()) { dataFile.delete(); // 真实删除文件 } // 步骤2:清空内存列表并刷新表格 records.clear(); tableModel.setRowCount(0); statusLabel.setText("共0条记录"); // 同步更新状态栏注意:delete()是物理删除,不是清空内容。这确保下次启动时loadRecords()因FileNotFoundException返回空列表,实现真正的“归零”。
提示:此操作不可逆。若需防误触,可在
JOptionPane.showConfirmDialog中增加二次确认:“确定要删除所有记账记录?此操作无法撤销。”
5. 避坑指南:新手在复现与修改过程中踩过的5个真实坑位
5.1 现象:双击Main.jar无反应,控制台无任何输出
原因:.jar文件未声明主类(Main-Class)或未打包依赖(本项目无依赖,但MANIFEST.MF缺失仍会失败);或系统默认用JRE而非JDK打开。
解决:放弃双击,严格使用命令行java -cp out Main启动;若必须生成可执行jar,用jar cfm MyAccount.jar MANIFEST.MF -C out/ .并确保MANIFEST.MF含Main-Class: Main。
5.2 现象:添加记录后,表格显示金额为0.0,或日期显示为“null”
原因:Record类的字段未用private修饰,且get/set方法未正确实现(如setAmount(String s)未转Double.parseDouble(s));或构造函数未初始化字段。
解决:检查Record.java,确认所有字段为private,setAmount方法接收double参数(非String),并在Main.java中传参时已做类型转换。
5.3 现象:中文备注显示为乱码(如“ä¼å”),或保存后文件用记事本打开是方块
原因:FileOutputStream未指定字符集,Windows默认用GBK,而程序用UTF-8写入。
解决:new OutputStreamWriter(new FileOutputStream(...), "UTF-8")和new InputStreamReader(new FileInputStream(...), "UTF-8")必须成对出现,且编码字符串严格为"UTF-8"(大小写敏感)。
5.4 现象:多次点击「添加记录」后,表格中出现重复的同一笔记录
原因:actionPerformed中未清空输入对话框的缓存,或records.add()被重复调用(如监听器注册两次)。
解决:检查按钮监听器注册代码,确认addButton.addActionListener(...)只执行一次;在actionPerformed开头加日志System.out.println("添加记录触发"),确认是否被触发多次。
5.5 现象:程序关闭后重启,数据全部消失,data/records.txt文件为空或不存在
原因:DataHandler.saveRecords()未被任何操作调用(如忘记在添加/删除后调用);或saveRecords方法中writer.write()后未调用writer.flush(),数据滞留在缓冲区未写入磁盘。
解决:在AddRecordAction和ClearDataAction的末尾显式调用DataHandler.saveRecords(records);在saveRecords方法中writer.flush()后加一行System.out.println("数据已保存")验证执行路径。
6. 进阶改造技巧:3个低侵入、高价值的定制方向
6.1 为金额增加千分位分隔与精度控制:从“5.0”到“5.00”
原始程序中金额直接String.valueOf(r.getAmount()),显示为5.0不够专业。改造只需两处:
第一步:在Record.java中增加格式化方法
import java.text.DecimalFormat; public class Record { private double amount; // ... 其他字段与getter/setter public String getFormattedAmount() { DecimalFormat df = new DecimalFormat("#,##0.00"); return df.format(this.amount); } }第二步:在表格模型填充处替换金额字段
// 替换原来的 r.getAmount() tableModel.addRow(new Object[]{ r.getDate(), r.getCategory(), r.getFormattedAmount(), // ← 改用格式化后字符串 r.getLocation(), r.getRemark() });效果:5.0→5.00,1234.5→1,234.50,0.1→0.10。DecimalFormat是Java内置类,无需额外依赖,且#,##0.00模式自动处理千分位与两位小数。
6.2 按日期范围筛选:在「查看所有」基础上叠加时间过滤
不重构现有UI,仅在「查看所有」按钮旁新增「筛选」按钮,弹出日期选择对话框:
// 点击筛选按钮时 String startDate = JOptionPane.showInputDialog("请输入开始日期(yyyy-MM-dd)"); String endDate = JOptionPane.showInputDialog("请输入结束日期(yyyy-MM-dd)"); // 过滤records列表 List<Record> filtered = records.stream() .filter(r -> r.getDate().compareTo(startDate) >= 0 && r.getDate().compareTo(endDate) <= 0) .collect(Collectors.toList()); // 刷新表格(复用原有逻辑) tableModel.setRowCount(0); for (Record r : filtered) { tableModel.addRow(new Object[]{...}); }关键点:String.compareTo()可直接比较yyyy-MM-dd格式日期,因字典序与时间序一致,无需转LocalDate,零依赖。
6.3 导出为Excel兼容CSV:用制表符分隔升级为逗号分隔并加引号
为满足财务软件导入需求,新增「导出CSV」菜单项,生成export_YYYYMMDD.csv:
public static void exportToCsv(List<Record> records, String filename) { try (PrintWriter pw = new PrintWriter( new OutputStreamWriter( new FileOutputStream(filename), "UTF-8"))) { // 写入CSV头部 pw.println("日期,类别,金额,地点,备注"); for (Record r : records) { // 关键:字段含逗号或换行时用双引号包裹,并转义内部双引号 String escapedRemark = r.getRemark().replace("\"", "\"\""); String line = String.format( "\"%s\",\"%s\",\"%.2f\",\"%s\",\"%s\"", r.getDate(), r.getCategory(), r.getAmount(), r.getLocation(), escapedRemark ); pw.println(line); } } catch (IOException e) { System.err.println("导出失败:" + e.getMessage()); } }生成的CSV示例:
日期,类别,金额,地点,备注 "2024-05-20","早餐","5.00","便利店","" "2024-05-21","交通","12.50","地铁","" "2024-05-22","餐饮","68.00","餐厅","朋友""聚餐"双引号包裹、内部双引号转义为两个双引号,完全符合RFC 4180标准,Excel可直接双击打开。
我带过的某高校实训班里,A同学用这个记账本作为课程设计基线,三天内加上了分类统计饼图(用JFreeChart)和月度报表导出,最终答辩时老师问“数据怎么保证不丢”,他当场演示了断电重启后数据完好——那一刻我知道,这个“小小”二字,真的沉得下去。希望帮到你。
本文还有配套的精品资源,点击获取