Capstone: a small Java program, end to end
Read lines, parse into records, aggregate with streams, handle errors, print a report — every major concept in one program.
The capstone program: Sales Report
This small program brings together nearly every concept in the course: records, streams, try-with-resources file I/O, error handling, static factories, immutability, and clean output.
### What the program does
It reads a CSV file of sales records (one per line: productId,amount,region), parses each line into an immutable SaleRecord, groups them by region using streams, computes total revenue per region, and prints a sorted report.
### Step 1: the data model — a record
Each parsed line becomes a SaleRecord. A record is ideal here: immutable, with equals/hashCode/toString for free.
public record SaleRecord(String productId, double amount, String region) {
public SaleRecord {
if (amount < 0) throw new IllegalArgumentException("negative amount: " + amount);
region = region.trim().toUpperCase();
}
}
The compact constructor runs before fields are assigned. We validate the amount and normalise the region — both are part of the value's invariant.
### Step 2: reading and parsing — try-with-resources
static List<SaleRecord> loadRecords(String path) throws IOException {
var records = new ArrayList<SaleRecord>();
try (var reader = new BufferedReader(new FileReader(path))) {
String line;
while ((line = reader.readLine()) != null) {
try {
records.add(parse(line));
} catch (IllegalArgumentException e) {
System.err.println("Skipping bad line: " + e.getMessage());
}
}
} // reader.close() called automatically
return List.copyOf(records);
}
The outer try-with-resources ensures the file is closed even if an exception escapes. The inner try/catch skips corrupt lines without aborting the whole read. List.copyOf returns an unmodifiable snapshot.
### Step 3: aggregation — a stream pipeline
static Map<String, Double> totalsByRegion(List<SaleRecord> records) {
return records.stream()
.collect(Collectors.groupingBy(
SaleRecord::region,
Collectors.summingDouble(SaleRecord::amount)
));
}
groupingBy partitions the stream by key; the downstream collector accumulates each group's amounts into a total. The result is Map<String, Double>.
### Step 4: printing the report — sorted
static void printReport(Map<String, Double> totals) {
totals.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.forEach(e -> System.out.printf("%-12s %10.2f%n", e.getKey(), e.getValue()));
}
comparingByValue().reversed() sorts entries from highest to lowest total.
### Concepts woven in
| Section | Course concept |
|---|---|
| record SaleRecord | records, compact constructor, immutability |
| List.copyOf | defensive copy out, unmodifiable collections |
| try-with-resources | AutoCloseable, suppressed exceptions, close order |
| inner try/catch per line | checked vs unchecked, partial-failure handling |
| stream().collect(groupingBy(...)) | Stream API, Collectors, method references |
| comparingByValue().reversed() | Comparator chaining, lambda/method reference |
| throws IOException | checked exception in method signature |
This is the shape of real Java work: a clear data model, controlled I/O, business logic expressed as a pipeline, and clean output. Every module of this course contributed a piece.