// sample-program.java: a small, complete Java program with numbered comments. // Free sample from 99helpers.com/file-types/java // // It reads a short list of shared expenses, skips the lines it can't parse, // and prints a total per person. Needs Java 17 or later. // // Run it straight from the source file (no separate compile step): // java sample-program.java // Or compile it to bytecode first, then run the class: // javac sample-program.java (writes SampleProgram.class and others) // java SampleProgram // 1. Imports name classes from other packages. Everything in java.lang, // such as String, Long and System, is imported automatically. import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; // 2. The class. Every method and field in Java lives inside one. This class // is not `public`: a public class must sit in a file with its exact name, // SampleProgram.java, and a downloaded file called sample-program.java // can't satisfy that. Rename the file and you may add `public`. final class SampleProgram { // 3. An enum lists every value a type can have. Enums can have methods. enum Category { FOOD, TRAVEL, OTHER; static Category fromLabel(String label) { // 4. A switch expression (Java 14+): arrows, no fall-through, // and it returns a value. return switch (label) { case "food" -> FOOD; case "travel" -> TRAVEL; default -> OTHER; }; } } // 5. A record (Java 16+): a data class in one line. The compiler writes // the constructor, the accessors who() and cents(), equals, hashCode // and toString. The block below is a compact constructor that checks // the values. record Expense(String who, long cents, Category category) { Expense { if (cents < 0) { throw new IllegalArgumentException("negative amount: " + cents); } } } // 6. A checked exception. Methods that throw it must say so with // `throws`, and callers must catch it or pass it on. static final class ParseException extends Exception { private static final long serialVersionUID = 1L; ParseException(int line, String message) { super("line " + line + ": " + message); } } // 7. A text block (Java 15+): a multi-line string between triple quotes. // The indentation shared by all its lines is removed. static final String INPUT = """ Ada Example, 42.50, food Ben Example, 120, travel Ada Example, 7.25, other Cleo Example, twelve, food Ben Example, 18.9, food """; static Expense parseLine(int number, String line) throws ParseException { String[] fields = line.split(","); if (fields.length != 3) { throw new ParseException(number, "expected 3 fields"); } String amount = fields[1].strip(); try { return new Expense(fields[0].strip(), parseCents(amount), Category.fromLabel(fields[2].strip())); } catch (NumberFormatException e) { // 8. NumberFormatException is unchecked: nothing forces you to // catch it, so the compiler can't remind you. Here we turn it // into our own checked exception. throw new ParseException(number, '"' + amount + "\" is not an amount"); } } // 9. Static methods belong to the class, not to an object. `long` is a // 64-bit primitive; money is kept in whole cents to avoid rounding. static long parseCents(String text) { String[] parts = text.split("\\.", 2); String fraction = parts.length == 2 ? parts[1] : "0"; if (fraction.length() > 2) { throw new NumberFormatException("too many decimals"); } long whole = Long.parseLong(parts[0]); return Math.addExact(Math.multiplyExact(whole, 100), Long.parseLong((fraction + "0").substring(0, 2))); } static String euros(long cents) { return String.format("€%d.%02d", cents / 100, cents % 100); } // 10. `main` is where the program starts: public, static, void, and an // array of command-line arguments. public static void main(String[] args) { // 11. Generics: List can only hold Expense objects. `var` // (Java 10+) lets the compiler work out the type. var expenses = new ArrayList(); List lines = INPUT.lines().toList(); for (int i = 0; i < lines.size(); i++) { try { expenses.add(parseLine(i + 1, lines.get(i))); } catch (ParseException e) { System.err.println("skipped " + e.getMessage()); } } // 12. Streams and lambdas: group, sum and sort without a loop. // A TreeMap keeps its keys in alphabetical order. Map totals = expenses.stream() .collect(Collectors.groupingBy(Expense::who, TreeMap::new, Collectors.summingLong(Expense::cents))); System.out.println(expenses.size() + " expenses"); totals.forEach((who, cents) -> System.out.printf("%-14s%9s%n", who, euros(cents))); long food = expenses.stream() .filter(e -> e.category() == Category.FOOD) .mapToLong(Expense::cents) .sum(); System.out.println("food " + euros(food) + ", first: " + expenses.get(0)); } }