-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
226: Building a Personal Expense Tracker with File Persistence
I remember the first time I tried to make a simple program "remember" data between restarts. I thought I had it all figured out: write some strings to a file, read them back, and boom—persistence. But then I kept getting a NumberFormatException that seemed to happen randomly, even though my data looked perfectly fine in the text editor. It took me an hour of staring at the screen to realize I was trying to parse a ghost.
The Dreaded NumberFormatException on the Last Line
// Imagine we have a file 'expenses.txt' with:
// Coffee,5.50
// Rent,1200.00
// Internet,60.00
// [Empty Line Here]
public List<Expense> loadExpenses() throws IOException {
List<Expense> expenses = new ArrayList<>();
BufferedReader reader = new BufferedReader(new FileReader("expenses.txt"));
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(",");
String description = parts[0];
double amount = Double.parseDouble(parts[1]); // CRASH happens here!
expenses.add(new Expense(description, amount));
}
reader.close();
return expenses;
}
If you run this, it works great... until you have a trailing newline at the end of your file. Most text editors add a hidden newline at the end of the document. When readLine() hits that last empty line, it doesn't return null immediately; it returns an empty string. split(",") then produces an array with one empty element, and when you try to access parts[1] or parse that empty string into a double, Java throws a fit.
Filtering Out the Empty Noise
The fix is simple, but it's a habit you need to build: never trust the input file. I always add a guard clause to skip empty or blank lines. It makes your code resilient to how different operating systems handle line endings.
while ((line = reader.readLine()) != null) {
if (line.isBlank()) continue; // Skip the ghost lines
String[] parts = line.split(",");
if (parts.length < 2) continue; // Also protect against malformed lines
String description = parts[0];
double amount = Double.parseDouble(parts[1]);
expenses.add(new Expense(description, amount));
}
By using isBlank() (introduced in Java 11), we ignore lines that are either totally empty or just contain whitespace. I also added a length check on the parts array. If a user accidentally deletes a price in the text file, the program won't crash; it'll just skip that bad entry. It's better to miss one expense than to have the whole app crash on startup.
Structuring the CSV for Easy Recovery
For a personal expense tracker, we don't need a full database. A CSV (Comma Separated Values) format is plenty. However, the trick to making this maintainable is to let the Expense object handle its own "serialization." Don't scatter split(",") logic all over your main class. Instead, give your Expense class a method to convert itself to a string and a static factory method to recreate itself from a string.
This way, if you decide later to add a "Date" field or a "Category" field, you only have to change the logic in one place—the Expense class—rather than hunting through every file-reading loop in your project.
Ensuring the Data Actually Hits the Disk
When writing the data back, you'll likely use a PrintWriter or a BufferedWriter. One thing that catches people off guard is the buffer. Java doesn't always write to the disk the millisecond you call println(); it holds the data in memory for efficiency.
If your program crashes or you forget to close the stream, that data stays in the buffer and never actually reaches the file. I strongly recommend the try-with-resources statement. It ensures that the file is closed (and thus flushed) regardless of whether the code finishes successfully or throws an exception. I've lost too many hours of test data to "missing" file writes because I forgot to call .close().
public void saveExpenses(List<Expense> expenses) throws IOException {
try (PrintWriter writer = new PrintWriter(new FileWriter("expenses.txt"))) {
for (Expense e : expenses) {
writer.println(e.toCsv());
}
} // writer is automatically closed and flushed here
}📋 Practical Task
Exercise: Implementing a Category-Based Budget Filter for the Expense File
Your expense tracker is working, but now you want to be able to load only specific types of spending. Currently, the loadExpenses() method loads every single line from expenses.txt into memory.
Your Task: Modify the file-loading logic to implement a loadExpensesByCategory(String targetCategory) method.
- Assume your CSV format has been updated to
Description,Amount,Category(e.g.,"Latte,5.50,Food"). - The method should read the file line-by-line.
- It should only add the
Expenseobject to the returned list if the category matches thetargetCategory(case-insensitive). - Ensure you maintain the "guard clauses" learned in the lesson to prevent
NumberFormatExceptionorArrayIndexOutOfBoundsExceptionif the file has empty lines or malformed data.
There are no comments for now.