Account与Transaction应职责分离:Account管理余额和元信息,Transaction封装单笔收支(含时间、金额、类型、备注);关键约束须写入构造逻辑,如金额校验。

用 Account 和 Transaction 类建模核心业务
记账的本质是「账户余额随交易动态变化」,不是存一堆数字。必须拆出两个职责明确的类:Account 管总余额与账户元信息,Transaction 封装单笔收支(含时间、金额、类型、备注)。别把所有字段塞进一个类——否则后续加「分类统计」「多账户切换」时会迅速失控。
关键约束要写进构造逻辑:
public class Transaction {
private final LocalDateTime time;
private final double amount;
private final String type; // "income" or "expense"
private final String note;
public Transaction(double amount, String type, String note) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
if (!"income".equals(type) && !"expense".equals(type))
throw new IllegalArgumentException("Type must be 'income' or 'expense'");
this.time = LocalDateTime.now();
this.amount = amount;
this.type = type;
this.note = note == null ? "" : note.trim();
}
}
用 ArrayList 存交易,但别直接暴露集合引用
新手常犯错误:在 Account 里声明 public List,结果外部代码随意 add() 或 clear(),破坏余额一致性。正确做法是只提供受控方法:
-
recordIncome(double, String)和recordExpense(double, String)—— 内部校验、创建Transaction、更新balance、再存入私有ArrayList -
getTransactions()返回Collections.unmodifiableList(transactions),防止外部修改 - 需要按时间查交易?加
getTransactionsSince(LocalDateTime),而不是让调用方自己遍历
用 Scanner 做命令行交互时,必须处理输入异常
用户输个字母就崩,体验极差。重点捕获三类:InputMismatchException(输错类型)、NoSuchElementException(直接按 Ctrl+D)、IllegalStateException(Scanner 关闭后还调用)。示例片段:
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("Enter amount: ");
try {
double amount = scanner.nextDouble();
if (amount <= 0) {
System.out.println("Amount must be > 0");
continue;
}
// ... proceed
} catch (InputMismatchException e) {
System.out.println("Please enter a valid number");
scanner.next(); // consume invalid token
} catch (NoSuchElementException e) {
System.out.println("\nBye!");
break;
}
}
持久化先用 PrintWriter 写文本文件,别一上来碰数据库
小型应用优先选人眼可读、编辑器能直接打开的格式。每行一条交易,用制表符分隔字段(比 CSV 更少转义麻烦):
立即学习“Java免费学习笔记(深入)”;
2024-05-20T14:30:22.123 income 1500.00 Salary 2024-05-21T09:15:03.456 expense 28.50 Coffee
写入时注意:PrintWriter 默认不自动刷新,必须调用 flush();路径用相对路径如 "data/transactions.txt",启动前手动创建 data 目录;读取时用 Files.readAllLines(Paths.get(...)) 加载全量,别边读边解析——小数据够用,逻辑清晰。
真正卡住人的地方往往不在类设计,而在输入校验的边界条件(比如负数、空字符串、时间格式)和文件 I/O 的异常恢复(程序崩溃后文件是否损坏)。这些细节不写进构造函数和方法签名,运行时才暴露。









