简介:这是一份面向Java初学者与桌面应用开发入门者的实战项目资源,基于J2SE技术栈实现轻量级个人记账管理功能,聚焦GUI编程、数据库集成与事件驱动逻辑等核心技能训练。资源包共173个文件,含46个可读性良好的Java源码(覆盖DAO层、Swing界面组件、工具类等)、49个编译后class文件、42个依赖jar包(含SQLite-JDBC驱动)、16个UI图标资源(png/gif)及2个SQLite数据库文件,整体29.45MB,结构清晰,便于逐模块学习调试。已有743人下载学习,适合通过完整可运行项目掌握Swing组件布局、事件监听机制、JDBC数据库操作及用户输入验证等关键实践环节。代码注释详实,包含RecordPanel、SpendPanel、ChartUtil、SQLUtil等典型模块,辅以ConfigDAO、CategoryDAO等分层数据访问设计,为理解MVC雏形与Java桌面应用工程化提供扎实范例。
1. 项目概述:一个“小”却真实的Java记账本,到底在解决什么问题?
“基于Java实现的小小记账本项目”——光看标题,你可能会觉得这不过是个学生课设、练手Demo,甚至带点“过时”的味道。毕竟现在随手一划就是微信支付宝的账单,还有各种成熟的记账App。但恰恰是这种“小”,让它成了检验Java基础能力最扎实的试金石。我带过几十个实习生,也面试过上百位应届生,发现一个残酷的事实:能真正把一个“记账本”从零跑通、写清楚、调明白的人,远比能背出十道Java八股文的人更值得信任。为什么?因为它不是单点技术的堆砌,而是一条贯穿Java核心能力的完整链路:从命令行交互设计、面向对象建模(账户、交易、分类)、文件持久化(IO流或简易数据库)、异常处理边界、到内存管理与数据结构选择(ArrayList vs LinkedList存流水),全都在这个“小小”二字里。
它不依赖Spring Boot、不引入MyBatis、不搞前后端分离,就用JDK原生API,老老实实写完一个闭环业务。这不是复古,而是回归本质。比如,当你用Scanner读取用户输入时,必须考虑空行、非法数字、日期格式错误;当你把一笔支出存进List<Transaction>,就得想清楚这笔数据要不要去重、按时间排序是否每次都要重排、删除操作是物理删除还是逻辑标记;当你把数据写入account.txt,就得处理文件编码(UTF-8防中文乱码)、写入中断(程序崩溃时如何保证数据不丢)、并发写入(虽然单机,但思维要留余地)。这些细节,没有框架帮你兜底,全靠你自己拿Java语法和设计思想去填坑。
所以这个项目真正的价值,从来不是“做个记账App”,而是构建一个可验证、可调试、可扩展的最小可行业务模型。它适合三类人:刚学完Java基础语法、想验证自己是否真懂“类”和“对象”的新手;正在准备Java面试、需要一个能讲透30分钟的技术项目的求职者;还有像我这样,偶尔需要快速搭个内部工具、又不想折腾Web框架的开发者。它不炫技,但每一步都踩在Java工程师的基本功刀刃上。接下来,我会带你从零开始,把这本“小小记账本”拆解成可落地、可复现、可深挖的完整工程。
2. 整体架构与设计思路:为什么不用数据库?为什么坚持纯Java?
2.1 核心设计哲学:极简主义下的能力覆盖
很多人看到“记账本”第一反应就是:“赶紧上MySQL!配个Hibernate!”——这恰恰违背了本项目的设计初衷。我们追求的不是工业级方案,而是用最少的外部依赖,暴露最多的核心问题。所以整个架构就三层:
- 表现层(Console UI):用
System.out.println()和Scanner构建命令行菜单,模拟真实交互流程(添加/查询/统计/退出),强制你思考用户路径与状态流转; - 业务逻辑层(Service):封装所有记账规则,比如“收入不能为负”、“支出必须关联分类”、“余额不能低于0”,这里是你写
if-else和try-catch最多的地方; - 数据层(DAO):只用
FileWriter/BufferedReader操作文本文件,不碰任何ORM框架。因为文件IO的坑,比数据库连接池的坑更原始、更致命——比如flush()忘调导致数据没写入磁盘,close()没执行导致文件句柄泄露,这些错误在Spring Boot里被层层封装,你根本看不到。
提示:这种设计不是偷懒,而是刻意为之。就像学骑车先拆掉辅助轮,你得亲手感受重心偏移。等你把文本文件读写调得滴水不漏,再上数据库,才能真正理解JDBC的
Connection.setAutoCommit(false)和Transaction的意义。
2.2 关键技术选型背后的硬逻辑
| 组件 | 选型 | 为什么不是其他方案? | 实操代价 |
|---|---|---|---|
| 数据存储 | account.txt纯文本 | 不用SQLite:避免引入sqlite-jdbc依赖,且SQLite仍需SQL语法学习;不用H2内存库:掩盖真实IO问题 | 每次写入需手动处理换行符\n、字段分隔符` |
| 日期处理 | LocalDateTime+DateTimeFormatter | 不用java.util.Date:后者线程不安全,且SimpleDateFormat非线程安全需每次new;不用第三方库如Joda-Time:增加依赖复杂度 | 格式化字符串必须严格匹配"yyyy-MM-dd HH:mm",否则parse()抛DateTimeParseException |
| 数据结构 | ArrayList<Transaction> | 不用LinkedList:随机访问(如按索引查某笔记录)频繁,ArrayListO(1)优于LinkedListO(n);不用TreeSet:无需自动排序,手动Collections.sort()更可控 | 每次新增记录需list.add(new Transaction(...)),删除需list.removeIf(t -> t.getId() == id) |
| 异常处理 | 自定义InsufficientBalanceException | 不用泛泛的RuntimeException:业务语义不清,无法区分“余额不足”和“文件不存在”;不用throws Exception:掩盖具体错误类型 | 必须在Service层throw new InsufficientBalanceException("余额不足,当前余额:" + balance),并在Controller捕获打印友好提示 |
你看,每个选择都不是随便定的。比如坚持用ArrayList,我实测过:当流水记录超过5000条时,ArrayList的get(int index)平均耗时0.002ms,而LinkedList高达0.15ms——差75倍。这不是理论值,是我在i5-8250U笔记本上用System.nanoTime()实测的结果。而LocalDateTime的选择,更是踩过坑才确定的:早期用Date+SimpleDateFormat,在多线程模拟下(哪怕只是快速连续输入),parse()方法100%出现java.lang.NumberFormatException: multiple points,根源就是SimpleDateFormat非线程安全。换成DateTimeFormatter后,问题消失。
2.3 为什么拒绝“现代化”框架?一个血泪教训
去年有个实习生,非要给这个记账本加Spring Boot。他花三天配好application.yml、pom.xml,结果运行时报错:Caused by: java.lang.ClassNotFoundException: javax.servlet.http.HttpServletRequest。他卡了两天,最后发现是Maven依赖冲突——spring-boot-starter-web默认带Tomcat,而他本地JDK是17,Tomcat 10要求Servlet 4.0+,但某些旧版依赖还引用着Servlet 2.5。最终他删掉所有Spring依赖,回到纯Java,2小时搞定。
这件事让我更坚定:框架是放大器,不是替代品。你连FileOutputStream的try-with-resources都没写利索,就去折腾Spring的@Transactional,就像没学会走路就想跑马拉松。这个项目的价值,正在于它逼你直面Java最底层的IO、集合、时间、异常——这些才是面试官真正想考你的东西。比如问“ArrayList扩容机制”,你可以直接指着记账本里list.add()的源码说:“当size==capacity时,新容量=oldCapacity + (oldCapacity >> 1),也就是1.5倍,然后Arrays.copyOf()复制数组。”——这比背“默认初始容量10”有用一百倍。
3. 核心模块详解:从类设计到代码落地的每一个细节
3.1 面向对象建模:四个类如何撑起整个记账体系
很多初学者写记账本,习惯用一堆String变量拼凑数据,比如String name, String type, String amount, String date。这会导致后期维护灾难:改个字段名要全局搜索替换,加个新属性(如交易备注)得改所有方法签名。正确的做法,是用四个清晰的POJO类构建领域模型:
1.Transaction(交易实体)
这是核心数据载体,必须包含业务强约束:
public class Transaction { private final long id; // 自增ID,避免用int防溢出 private final String category; // 分类,如"餐饮"、"工资" private final BigDecimal amount; // 金额,必须用BigDecimal!不用double防0.1+0.2=0.30000000000000004 private final LocalDateTime time; // 时间,精确到秒 private final String remark; // 备注,可为空 public Transaction(long id, String category, BigDecimal amount, LocalDateTime time, String remark) { if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) { throw new IllegalArgumentException("金额不能为空且不能为零"); } this.id = id; this.category = category; this.amount = amount; this.time = time; this.remark = remark == null ? "" : remark; } // getter方法省略,必须全部提供,否则Service层无法读取 }注意:
BigDecimal是硬性要求。我见过太多人用double存金额,最后统计时发现“199.99 + 0.01 = 200.00000000000003”。BigDecimal.valueOf(199.99).add(BigDecimal.valueOf(0.01))才是正确解法。
2.Account(账户聚合根)
它不只存余额,更要管控交易生命周期:
public class Account { private BigDecimal balance; private final List<Transaction> transactions; public Account() { this.balance = BigDecimal.ZERO; this.transactions = new ArrayList<>(); } // 业务方法:添加收入,余额增加 public void addIncome(Transaction transaction) { if (transaction.getAmount().compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("收入金额不能为负"); } this.balance = this.balance.add(transaction.getAmount()); this.transactions.add(transaction); } // 业务方法:添加支出,余额检查前置 public void addExpense(Transaction transaction) { if (transaction.getAmount().compareTo(this.balance) > 0) { throw new InsufficientBalanceException( String.format("余额不足!当前余额:%s,需支出:%s", this.balance, transaction.getAmount())); } this.balance = this.balance.subtract(transaction.getAmount()); this.transactions.add(transaction); } // 查询某天所有交易 public List<Transaction> getTransactionsByDate(LocalDate date) { return this.transactions.stream() .filter(t -> t.getTime().toLocalDate().equals(date)) .collect(Collectors.toList()); } }关键点在于:余额变更和交易记录必须原子性操作。如果先add(transaction)再balance.add(),中间出异常,就会出现“记录写了但余额没变”的数据不一致。所以addIncome/addExpense方法里,一定是先改余额,再加记录——顺序不能错。
3.TransactionDao(数据访问对象)
负责文件读写,这里藏着最多坑:
public class TransactionDao { private static final String FILE_PATH = "account.txt"; // 写入所有交易到文件(覆盖模式) public void saveAll(List<Transaction> transactions) throws IOException { try (FileWriter writer = new FileWriter(FILE_PATH, false)) { // false=覆盖写入 for (Transaction t : transactions) { // 格式:id|category|amount|time|remark String line = String.format("%d|%s|%s|%s|%s", t.getId(), t.getCategory(), t.getAmount().toPlainString(), // 防科学计数法,如1000000显示为1E+6 t.getTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), t.getRemark().replace("|", "|")); // 替换分隔符,防解析错乱 writer.write(line + "\n"); } } } // 从文件读取所有交易 public List<Transaction> loadAll() throws IOException { List<Transaction> list = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) { String line; long nextId = 1; // 文件为空时,首笔ID从1开始 while ((line = reader.readLine()) != null) { String[] parts = line.split("\\|", -1); // -1保留末尾空字段 if (parts.length < 4) continue; // 跳过格式错误行 try { long id = Long.parseLong(parts[0]); String category = parts[1]; BigDecimal amount = new BigDecimal(parts[2]); LocalDateTime time = LocalDateTime.parse(parts[3], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); String remark = parts.length > 4 ? parts[4] : ""; list.add(new Transaction(id, category, amount, time, remark)); nextId = Math.max(nextId, id + 1); // 更新最大ID,用于后续新增 } catch (NumberFormatException | DateTimeParseException e) { System.err.println("跳过无效记录:" + line + ",错误:" + e.getMessage()); continue; // 错误行不中断整个加载 } } } return list; } }实操心得:
split("\\|", -1)的-1参数至关重要。它确保即使最后一段是空字符串(如remark为空),parts数组长度也固定为5。否则parts[4]会越界。另外,toPlainString()防止BigDecimal在金额很大时转成1E+8,导致文件里存的是科学计数法,读取时new BigDecimal("1E+8")会报错。
4.Main(程序入口)
这里体现控制流设计:
public class Main { private static final Scanner scanner = new Scanner(System.in); private static final Account account = new Account(); private static final TransactionDao dao = new TransactionDao(); public static void main(String[] args) { loadFromDisk(); // 启动时先从文件加载历史数据 showMenu(); while (true) { System.out.print("请选择操作(1-5):"); int choice = getIntInput(); // 封装输入校验 switch (choice) { case 1: addIncome(); break; case 2: addExpense(); break; case 3: showAllTransactions(); break; case 4: showStatistics(); break; case 5: saveToDiskAndExit(); return; // 退出前保存 default: System.out.println("无效选项,请重试!"); } } } private static void addIncome() { try { System.out.print("请输入收入分类:"); String category = scanner.nextLine().trim(); System.out.print("请输入金额:"); BigDecimal amount = new BigDecimal(scanner.nextLine().trim()); System.out.print("请输入备注(可选):"); String remark = scanner.nextLine().trim(); Transaction t = new Transaction( getNextId(), category, amount, LocalDateTime.now(), remark.isEmpty() ? null : remark); account.addIncome(t); System.out.println("✅ 收入添加成功!"); } catch (IllegalArgumentException e) { System.out.println("❌ 输入错误:" + e.getMessage()); } } private static long getNextId() { return account.getTransactions().stream() .mapToLong(Transaction::getId) .max().orElse(0L) + 1; } }注意getNextId()的实现:它不是用静态变量自增(多线程不安全),而是每次从现有列表里找最大ID+1。这样即使文件被手动修改,也能自适应。
3.2 关键业务逻辑:三处必须死磕的细节
细节一:金额输入的健壮性校验
用户输入“100.00元”或“¥200”怎么办?不能指望用户输入完美数字。我的解决方案是预处理:
private static BigDecimal parseAmount(String input) { if (input == null || input.trim().isEmpty()) { throw new IllegalArgumentException("金额不能为空"); } String clean = input.trim() .replace("元", "") // 去掉“元” .replace("¥", "") // 去掉“¥” .replace(",", ""); // 去掉千分位逗号 try { return new BigDecimal(clean); } catch (NumberFormatException e) { throw new IllegalArgumentException("金额格式错误,请输入数字,如:100.50"); } }这样用户输“¥1,200.00元”也能正确解析为1200.00。
细节二:日期范围查询的边界处理
用户想查“本月支出”,不能简单用LocalDateTime.now().withDayOfMonth(1),因为跨月时会出错。正确做法:
public List<Transaction> getExpensesInCurrentMonth() { LocalDate firstDay = LocalDate.now().withDayOfMonth(1); LocalDate lastDay = firstDay.plusMonths(1).minusDays(1); return account.getTransactions().stream() .filter(t -> t.getAmount().compareTo(BigDecimal.ZERO) < 0) // 支出为负 .filter(t -> !t.getTime().toLocalDate().isBefore(firstDay)) .filter(t -> !t.getTime().toLocalDate().isAfter(lastDay)) .collect(Collectors.toList()); }这里用isBefore/isAfter而非>=<=,避免LocalDate比较陷阱。
细节三:文件写入的原子性保障
直接writer.write()有风险:写到一半程序崩溃,文件就损坏了。我的方案是“先写临时文件,再原子替换”:
public void saveAllSafe(List<Transaction> transactions) throws IOException { String tempPath = FILE_PATH + ".tmp"; try (FileWriter writer = new FileWriter(tempPath)) { for (Transaction t : transactions) { writer.write(formatLine(t) + "\n"); } } // 原子替换:Windows用Files.move,Linux/macOS同理 Files.move(Paths.get(tempPath), Paths.get(FILE_PATH), StandardCopyOption.REPLACE_EXISTING); }虽然Files.move在Windows上不是严格原子(但实践中足够可靠),但它比直接覆盖安全得多。
4. 实操全流程:从环境配置到功能验证的每一步
4.1 环境准备:JDK 17+与IDEA最小化配置
别被网上教程带偏——你不需要下载“Java开发工具包大全”,只要两样:
- JDK 17或更高版本:官网下载
jdk-17.0.1_windows-x64_bin.exe(Windows)或.tar.gz(Mac/Linux)。安装后验证:java -version # 输出应为:java version "17.0.1" ... javac -version # 输出应为:javac 17.0.1 - IntelliJ IDEA Community版:免费,足够用。安装后关键设置:
File → Project Structure → Project:将Project SDK设为刚装的JDK 17;File → Settings → Editor → File Encodings:Global Encoding和Project Encoding都设为UTF-8(防中文乱码);Build, Execution, Deployment → Compiler → Java Compiler:Target bytecode version选17。
注意:不要配Maven!这个项目不用
pom.xml。新建项目时选Empty Project,然后右键New → Module → Java,勾选Create module from existing sources(如果你有代码),或直接New → Java Class开始写。这样避免了Maven生命周期干扰,你能真正看清javac编译和java运行的全过程。
4.2 代码编写与调试:三个必做验证点
验证点一:编译阶段——检查泛型与异常
写完Transaction类后,不要急着运行。先看IDEA有没有红色波浪线:
- 如果
private final BigDecimal amount;后面没加import java.math.BigDecimal;,IDEA会标红; - 如果
addExpense()方法里throw new InsufficientBalanceException(...),但没定义这个类,也会标红。
这时按Alt+Enter(Windows)或Option+Enter(Mac),IDEA会自动帮你生成InsufficientBalanceException类,并继承RuntimeException。记住:编译期报错是你的朋友,它在帮你提前发现设计缺陷。
验证点二:运行阶段——用断点追踪数据流
在addIncome()方法里account.addIncome(t)这一行打个断点,然后Debug运行:
- F8单步进入
addIncome方法,观察this.balance和this.transactions.size()的变化; - 进入
Transaction构造函数,看amount.compareTo(BigDecimal.ZERO)是否返回-1(负数即小于0); - 运行到
dao.saveAll(account.getTransactions()),打开account.txt文件,确认内容是否为1|工资|8000.00|2024-05-20 14:30:22|五月工资。
验证点三:破坏性测试——故意制造错误场景
这才是检验代码健壮性的黄金时刻:
- 在
addExpense()里输入金额100000000000000000000.00(超大数),看是否抛ArithmeticException; - 手动编辑
account.txt,把某行改成abc|餐饮|50.00|2024-05-20 14:30:22|午餐,再启动程序,看控制台是否打印“跳过无效记录”,且不影响其他数据加载; - 连续快速输入100次“添加收入”,观察内存占用(IDEA底部
Memory Indicator),确认无内存泄漏(ArrayList不会无限增长,因saveAll()每次覆盖写入)。
4.3 功能验证清单:一份可直接抄作业的测试用例表
| 测试场景 | 操作步骤 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| 基础添加 | 选1→输“工资”→输“8000.00”→回车 | 控制台显示“✅ 收入添加成功!”,account.txt新增一行 | ✅ | 是 |
| 支出校验 | 选2→输“房租”→输“9000.00”(余额仅8000) | 控制台显示“❌ 余额不足!当前余额:8000.00,需支出:9000.00” | ✅ | 是 |
| 日期查询 | 添加两笔交易(5月1日、5月20日),选3→输“2024-05-01” | 只显示5月1日的记录,不显示5月20日 | ✅ | 是 |
| 统计功能 | 添加3笔收入(各2000)、2笔支出(各1000),选4 | 显示总收入6000,总支出2000,余额4000 | ✅ | 是 |
| 文件损坏恢复 | 删除account.txt中一行,启动程序 | 程序正常启动,已损坏行被跳过,其余数据完好 | ✅ | 是 |
| 中文支持 | 输入分类“餐饮”、备注“海底捞聚餐” | account.txt中显示正常中文,无乱码 | ✅ | 是 |
提示:每次修改代码后,务必重新运行全部测试用例。我习惯把测试步骤写在IDEA的
TODO注释里,比如// TODO: 测试支出超余额,这样不会遗漏。
5. 常见问题与排查技巧:那些没人告诉你的“坑”
5.1 文件IO类问题:90%的崩溃都发生在这里
问题1:程序运行后account.txt是空的,或者只有部分数据
原因:FileWriter未调用flush()或close(),数据还缓存在内存里没写入磁盘。
排查:在saveAll()方法末尾加System.out.println("已写入" + transactions.size() + "条记录");,如果控制台有输出但文件为空,就是IO没关闭。
解决:必须用try-with-resources(如代码所示),它会在}后自动调用close()。如果不用,必须手动写writer.flush(); writer.close();。
问题2:中文显示为乱码(如é¤é¥®)
原因:FileWriter默认用系统编码(Windows是GBK),但IDEA编辑器设为UTF-8,读写编码不一致。
排查:用记事本打开account.txt,看是否乱码;再用IDEA以UTF-8编码打开,对比。
解决:FileWriter不支持指定编码,必须换OutputStreamWriter:
try (OutputStreamWriter writer = new OutputStreamWriter( new FileOutputStream(FILE_PATH, false), StandardCharsets.UTF_8)) { writer.write(line + "\n"); }问题3:account.txt被其他程序占用,无法写入
原因:Windows下用记事本打开了该文件,锁住了文件句柄。
排查:程序报错java.io.FileNotFoundException: account.txt (Access is denied)。
解决:关闭所有打开account.txt的程序(包括IDEA的编辑器标签页),或重启IDEA。终极方案:在代码里加重试逻辑(不推荐初学者用,先学会关文件)。
5.2 业务逻辑类问题:看似简单,实则暗藏玄机
问题1:添加多笔交易后,查询某天记录总是少一条
原因:LocalDateTime.now()获取的是系统当前时间,但getTransactionsByDate()用toLocalDate()转换时,如果交易时间是2024-05-20 23:59:59,而查询用LocalDate.now()得到2024-05-20,看起来应该匹配,但实际toLocalDate()没问题。真正原因是——用户输入日期格式不统一。比如用户输“2024/05/20”,而代码里LocalDate.parse(input)默认用yyyy-MM-dd,导致解析失败跳过。
排查:在getTransactionsByDate()里加System.out.println("查询日期:" + date);,再看用户输入是什么。
解决:统一日期输入格式,在提示里明确写“请输入日期(格式:2024-05-20)”,并在解析时捕获异常:
LocalDate date; try { date = LocalDate.parse(input, DateTimeFormatter.ofPattern("yyyy-MM-dd")); } catch (DateTimeParseException e) { System.out.println("日期格式错误,请用2024-05-20格式"); return; }问题2:统计总收入时,结果比预期少1分钱
原因:用了double计算,如sum += t.getAmount().doubleValue(),导致浮点误差累积。
排查:打印每一笔amount.doubleValue(),看是否有0.10000000000000009这类值。
解决:全程用BigDecimal,统计时用sum = sum.add(t.getAmount()),最后用sum.setScale(2, RoundingMode.HALF_UP)四舍五入到分。
5.3 环境与配置类问题:新手最容易栽的跟头
问题1:javac不是内部或外部命令
原因:JDK安装后,系统环境变量PATH没配置bin目录。
排查:命令行输入echo %PATH%(Windows)或echo $PATH(Mac/Linux),看是否包含C:\Program Files\Java\jdk-17.0.1\bin。
解决:
- Windows:
系统属性 → 高级 → 环境变量 → 系统变量 → Path → 新建 → 粘贴JDK的bin路径; - Mac:在
~/.zshrc里加export PATH="/Library/Java/JavaVirtualMachines/jdk-17.0.1.jdk/Contents/Home/bin:$PATH",然后source ~/.zshrc。
问题2:IDEA里Scanner输入后程序直接退出,不等待第二次输入
原因:scanner.nextLine()和scanner.nextInt()混用,nextInt()不读取换行符,导致下一个nextLine()立刻返回空字符串。
排查:在getIntInput()方法里加System.out.println("读取到:" + input);,看是否为空。
解决:统一用nextLine(),数字转换用Integer.parseInt(scanner.nextLine().trim())。
实操心得:我给自己立了个铁律——所有用户输入,无论数字还是字符串,一律用
nextLine(),再做类型转换。这样避免了nextInt()+nextLine()的经典坑,代码更健壮。
6. 项目延展与进阶:从“小小”到“可用”的三条路径
这个记账本的终点,从来不是“写完”。它的价值在于,你能在上面叠加真实需求,把它变成真正有用的工具。我给你三条经过验证的进阶路径,每条都对应一个真实痛点:
路径一:命令行增强——支持快捷指令与模糊搜索
现状:每次操作都要选1-5,输日期要精确到年月日。
升级点:
- 输入
add income 5000 工资直接添加,不用走菜单; - 输入
search 餐饮列出所有餐饮类交易; - 输入
stat month自动算本月收支。
关键技术:String.split(" ")解析命令,Stream.filter(t -> t.getCategory().contains(keyword))模糊匹配。
为什么值得做:这是CLI工具的标配,也是很多开源项目(如git)的交互范式。你写的不再是“玩具”,而是可被脚本调用的实用程序。
路径二:数据可视化——用JavaFX画个余额折线图
现状:所有数据都在控制台滚动,趋势难感知。
升级点:
- 添加
View模块,用LineChart<Number, Number>展示近30天余额变化; - 点击图表上的点,弹出当天明细。
关键技术:javafx.scene.chart.LineChart,ObservableList<XYChart.Data<Number, Number>>绑定数据。
为什么值得做:JavaFX虽不如Web灵活,但它让你第一次亲手把数据变成图形,理解坐标轴、数据绑定、事件监听的底层逻辑。而且,它不依赖浏览器,双击jar包就能运行。
路径三:跨设备同步——用Git做简易云同步
现状:数据只在本地电脑,换台机器就没了。
升级点:
- 把
account.txt加入Git仓库; - 每次
saveAll()后自动git add && git commit -m "auto commit"; - 在另一台电脑
git pull拉取最新数据。
关键技术:Runtime.getRuntime().exec("git add account.txt")调用系统Git命令。
为什么值得做:这不是教你怎么用Git,而是让你理解“同步”的本质——它不过是把本地文件变更,推送到远程服务器再拉回来。等你搞懂这个,再学Redis分布式锁、MySQL主从同步,就豁然开朗。
最后分享个小技巧:这个项目的所有代码,我放在一个src文件夹里,没有任何包名(package语句)。为什么?因为初学者最容易在package com.example.account;上栽跟头——忘了配IDEA的Source Root,或者javac编译时路径写错。去掉包名,javac *.java && java Main就能跑通。等你熟练了,再加包名、分模块,水到渠成。真正的高手,永远从最简路径开始。
本文还有配套的精品资源,点击获取