﻿# Reading files and handling exceptions

> Turning a text file into objects line by line, and handling every way it can be wrong: checked and unchecked exceptions, your own exception with the line number, try-with-resources – and how a complete first assignment is structured.

Source: https://laszlonemes.com/university/programming-technology/files-exceptions · Author: László Nemes

Session 4 · Programming Technology

Turning a text file into objects line by line, and handling every way it can be wrong: checked and unchecked exceptions, your own exception with the line number, try-with-resources – and how a complete first assignment is structured.

20 min read `Line-by-line parsing with Scanner` `Checked vs unchecked exceptions` `Exception translation and propagation` `try-with-resources` `Anatomy of the first assignment`

The first assignment (beadandó) always starts the same way: a text file describes the input, your program reads it, builds objects from it and runs a simulation or computes statistics. Reading the file is the easy half. The other half is what happens when the file is missing, a line has one number too few, or a letter stands where a number should be – and that is where most first assignments lose points.

This session covers both halves with three examples: `Vehicles` (refuelling statistics, worked through in class), `AnimalFarm` (the practice task) and `Race`, a complete sample solution of a first assignment, with tests and documentation. All three share the same three building blocks: reading a file line by line, throwing an exception when the data is wrong, and an abstract base class that the concrete types extend.

> **The rule of thumb:** detect an error where it happens, describe it where you know the context (which line?), and report it in exactly one place – the part of the program that talks to the user. Everything in between just lets the exception pass.

After this session you can

- read a text file line by line and turn each line into an object with a Scanner
- tell checked and unchecked exceptions apart and write your own checked exception
- decide where an exception should be caught, translated or simply passed on
- close a file reliably with try-with-resources, also when reading fails
- structure a first assignment into a user interface, an input reader and a model

## From a line of text to an object

The `Vehicles` example keeps a log of refuels. The first line is the number of vehicles; every further line is one vehicle: a type letter (`S` car, `B` bus, `T` truck), the licence plate, the number of refuels, then that many amounts.

data.txt

```text
3
S AAA-111 4 6 7 8 9
B ABC-123 2 3 13
T FFF-888 7 1 2 3 4 5 6 7
```

The structure of the file tells you the structure of the reading code. One line is one object, so read the file **line by line**, and take each line apart with its own `Scanner`. A Scanner splits the line at whitespace – any number of spaces or tabs – and hands out the fields one by one: `next()` for a word, `nextInt()` for a whole number (Fig. 1).

> **Fig. 1** · The fields are read in order, and each one has a job. The count field is the interesting one: it is not stored anywhere, it only says how many numbers to read next.
>
> *Diagram:* One line of the data file, B ABC-123 2 3 13, is split into fields by a Scanner and read left to right with next and nextInt. The type code picks the subclass, the plate goes to the constructor, and the count says how many refuel amounts follow. The result is one Bus object with plate ABC-123 and refuels 3 and 13.

Database.read – the reading loop

```java
try (BufferedReader reader = Files.newBufferedReader(Path.of(filename), StandardCharsets.UTF_8)) {
    String line;
    int lineNumber = 0;
    while ((line = reader.readLine()) != null) {   // null = end of file
        lineNumber++;
        if (line.isBlank()) {
            continue;
        }
        Scanner fields = new Scanner(line);        // one Scanner per line
        loaded.add(parseVehicle(fields, lineNumber));
    }
}
```

Database.parseVehicle – one line

```java
String type = fields.next();
String plate = fields.next();
Vehicle vehicle = createVehicle(type, plate);

int numRefuels = fields.nextInt();
for (int i = 0; i < numRefuels; i++) {
    vehicle.addRefuel(fields.nextInt());   // throws if the line runs out
}
if (fields.hasNext()) {
    throw new IllegalArgumentException("More data than expected: " + fields.next());
}
```

Which subclass to create depends on the type letter, so the letter goes into a small **factory method**: a `switch` that returns a `Car`, a `Bus` or a `Truck`, and throws for anything else. The rest of the program only ever sees a `Vehicle`. Keep the letter itself as a constant in the subclass (`Bus.CATEGORY = "B"`), so the file format is written down in one place.

Database.createVehicle – letter → subclass

```java
private static Vehicle createVehicle(String type, String plate) {
    return switch (type) {
        case Car.CATEGORY -> new Car(plate);
        case Bus.CATEGORY -> new Bus(plate);
        case Truck.CATEGORY -> new Truck(plate);
        default -> throw new IllegalArgumentException("Unknown vehicle type: " + type);
    };
}
```

Good to know

**You don't need the first line**

Because every line is processed on its own, the count in the first line isn't needed: read until `readLine()` returns `null`. If the count is there, use it as a *check* – compare it with the number of lines you actually read and complain if they differ. This is more robust than a `for (i = 0; i < n; i++)` loop, which silently ignores extra lines and crashes on missing ones.

Common mistake

**Counting fields by hand**

`line.split(" ")` followed by `parts[3]`, `parts[4]` … breaks on the first double space (it produces empty strings) and on every line that is one field short (`ArrayIndexOutOfBoundsException` with no hint which line). If you split, split on `"\\s+"` and check the length before indexing. A Scanner per line avoids both problems: it skips any amount of whitespace and throws a specific exception when a field is missing.

Common mistake

**nextInt() followed by nextLine()**

If you read the whole file with one Scanner, mixing `nextInt()` and `nextLine()` bites: `nextInt()` stops right after the number, before the line break, so the next `nextLine()` returns the empty rest of that line.

```java
Scanner in = new Scanner(file);
int n = in.nextInt();         // reads "3" – the line break stays in the input
String first = in.nextLine(); // returns "" – the rest of line 1, not line 2!
```

Either read whole lines everywhere and parse each with its own Scanner (as above), or call an extra `nextLine()` to consume the line break.

## Checked and unchecked exceptions

An exception is an object that describes a problem. `throw` stops the current method immediately, and Java looks for a matching `catch` in the caller, then in its caller, and so on. Which exceptions you *must* deal with is decided by where the class sits in the hierarchy (Fig. 2).

> **Fig. 2** · Everything under Exception except RuntimeException is checked – the compiler makes you catch it or declare it with throws. RuntimeException and its subclasses are unchecked.
>
> *Diagram:* The Java exception hierarchy. Throwable has two branches, Error and Exception. Subclasses of Exception are checked – IOException, NoSuchFileException and your own InvalidInputException – so the compiler forces you to catch or declare them. RuntimeException and its subclasses are unchecked: IllegalArgumentException, NumberFormatException, NoSuchElementException and InputMismatchException.

- **checked** – The caller must react: `catch` it, or declare it in the method header with `throws`. Used for problems that are not bugs and can happen in a correct program – a missing file, invalid input.
- **unchecked** – Subclasses of `RuntimeException`. No declaration needed. Used for broken arguments and programming errors: `IllegalArgumentException`, `NullPointerException`, `IllegalStateException`.
- **Error** – Problems of the JVM itself (out of memory, stack overflow). Not for your program to catch.

For bad input, write your **own checked exception**. Extending `Exception` makes it checked, so nobody can forget to handle a broken file – the compiler won't let them. Give it a constructor that takes the line number and the original exception (the *cause*), so the message is useful to the user and the stack trace is still useful to you.

InvalidInputException.java

```java
public class InvalidInputException extends Exception {   // extends Exception = checked

    public InvalidInputException(String message) {
        super(message);
    }

    public InvalidInputException(int lineNumber, String message, Throwable cause) {
        super("Line " + lineNumber + ": " + message, cause);
    }
}
```

In your assignment

**One exception type for bad input**

In your assignment, every problem with the *content* of the file should end up as your own checked exception, with a message a user understands: `Line 3: Unknown species code: x`, not `java.util.InputMismatchException`. A missing or unreadable file stays an `IOException` – that is a different problem with a different message. Decide this before you write the reader.

## Where to catch: let it travel up

The most common beginner instinct is to wrap every risky line in its own `try`/`catch`. The opposite is right: catch an exception only where you can *do something useful* with it. In the `Race` sample, reading a bad file throws deep inside the parser, and the exception travels up through three methods that don't catch it at all (Fig. 3).

> **Fig. 3** · Reading teszt_invalid.txt. Only two methods catch anything: parseNextLine, which knows the line number, and Main, which knows how to talk to the user.
>
> *Diagram:* The call stack while reading teszt_invalid.txt. Main.run calls read, read calls readRace, readRace calls parseNextLine, which calls parseCreature. parseCreature throws an IllegalArgumentException for the unknown species code. parseNextLine catches it and throws an InvalidInputException with the line number instead, which passes through readRace and read, where the file is closed, up to Main, which prints the message and exits with code 1.

Only two places catch. `parseNextLine` catches because it knows something the thrower didn't – the line number – and adds it. This is **exception translation**: several low-level exceptions become one exception that means something in your program (Fig. 4).

> **Fig. 4** · Translate low-level exceptions in one place. The original goes along as the cause, so the stack trace still shows where it started.
>
> *Diagram:* Exception translation. Three low-level exceptions – InputMismatchException, NoSuchElementException and IllegalArgumentException – are caught in one place and rethrown as a single InvalidInputException that carries the line number and keeps the original as its cause. The caller needs only one catch.

RaceFileReader.parseNextLine – translation

```java
Scanner fields = new Scanner(line);
try {
    T result = parser.apply(fields);
    // ...
    return result;
} catch (InputMismatchException ex) {          // subclass first ...
    throw new InvalidInputException(lineNumber,
            "Expected a whole number, but found: " + fields.next(), ex);
} catch (NoSuchElementException ex) {          // ... then its superclass
    throw new InvalidInputException(lineNumber, "Data is missing from the end of the line", ex);
} catch (IllegalArgumentException ex) {        // our own checks, e.g. an unknown code
    throw new InvalidInputException(lineNumber, ex.getMessage(), ex);
}
```

The other place is `Main`. It is the only part of the program that prints, so it is the only part that turns an exception into a message – and into an exit code, so a script calling the program can tell success from failure.

Main.run – the one place that reports

```java
try {
    Race race = RaceFileReader.read(Path.of(fileName.strip()));
    race.run();
    printResult(race, out);
    return EXIT_OK;
} catch (NoSuchFileException ex) {
    return fail(err, "the file does not exist: " + fileName);
} catch (InvalidInputException ex) {
    return fail(err, "invalid input in " + fileName + ": " + ex.getMessage());
} catch (IOException ex) {
    return fail(err, "could not read the input: " + ex.getMessage());
}
```

Good to know

**The order of catch blocks**

Java tries the `catch` blocks top to bottom and takes the first that matches. A catch for a superclass also catches all its subclasses, so the more specific one must come first: `InputMismatchException` before `NoSuchElementException`, `NoSuchFileException` before `IOException`. The compiler rejects the wrong order as unreachable code.

Common mistake

**Swallowing the exception**

An empty `catch` block hides the problem and lets the program carry on with wrong data. The `Console` helper class from the first session does exactly this:

```java
try {
    line = reader.readLine();
} catch (IOException e) {
    // nothing here – the program carries on with an empty line
}
```

The same goes for `catch (Exception e) { e.printStackTrace(); }` in the middle of the reader: the user gets a wall of text, and the program still continues as if the line was fine. If you can't handle it here, don't catch it – add `throws` to the method header.

## Closing the file: try-with-resources

An open file is held by the operating system until you close it. Closing it at the end of the method is not enough: if anything throws halfway through, the `close()` line is never reached.

The leak

```java
BufferedReader reader = new BufferedReader(new FileReader(name));
String first = reader.readLine();
int count = Integer.parseInt(first);   // throws – the next line never runs
reader.close();
```

**try-with-resources** fixes this for good. Whatever you open in the parentheses after `try` is closed automatically when the block ends – normally or with an exception (Fig. 5).

> **Fig. 5** · close() runs on both paths, and it runs before the value is returned or the exception continues to the caller.
>
> *Diagram:* try-with-resources. The file is opened in the parentheses, then the block runs. Whether the block finishes normally or throws, close runs automatically before anything else happens; only then does the method return its value or let the exception continue to the caller.

RaceFileReader.read – a try with no catch

```java
public static Race read(Path file) throws IOException, InvalidInputException {
    // No catch: the try is only here to close the file.
    // The exceptions go on to the caller (Main), after the file is closed.
    try (BufferedReader source = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
        return read(source);
    }
}
```

Note that this `try` has no `catch` at all. Its only job is closing the file; the exceptions are declared with `throws` and handled by `Main`. A `try` doesn't have to catch.

Common mistake

**Not closing the file**

On some systems an unclosed file can't be deleted or rewritten until the program ends, and an unclosed writer may never flush its last lines to disk. If you create a `BufferedReader`, `FileReader`, `Scanner` on a file or a writer, it belongs in the parentheses of a `try`.

## Validate everything, keep nothing half-read

A file is either valid or it isn't. If line 5 of 7 is broken, the program should not continue with the first four vehicles as if nothing happened. The `Vehicles` reader collects into a *local* list and adds it to the database only after the last check has passed:

Database.read – all or nothing

```java
List<Vehicle> loaded = new ArrayList<>();   // a local list, not the field
// ... read every line into loaded; any error throws out of the method ...

checkUniquePlates(loaded);
vehicles.addAll(loaded);                     // only reached if the whole file was valid
```

The rules about a single object – a name must not be blank, the water must be between 1 and the capacity – belong in the **constructor**, which throws an `IllegalArgumentException`. That way an invalid object cannot exist at all, no matter who creates it: the reader, a test, or next month's GUI. The reader then translates that exception into an `InvalidInputException` with the line number.

Creature – the constructor guards its own rules

```java
protected Creature(String name, int initialWater, int maxWater) {
    if (name == null || name.isBlank()) {
        throw new IllegalArgumentException("The name of a creature must not be blank");
    }
    if (initialWater <= 0 || initialWater > maxWater) {
        throw new IllegalArgumentException("Invalid initial water for " + name + ": " + initialWater);
    }
    // ... assign the fields
}
```

Rules about the whole file, like unique names, need a collection. A `HashSet` is the shortest way: its `add` returns `false` when the element is already there.

Race – no two creatures with the same name

```java
Set<String> names = new HashSet<>();
for (Creature creature : creatures) {
    if (!names.add(creature.getName())) {    // add returns false for a duplicate
        throw new IllegalArgumentException("Duplicate creature name: " + creature.getName());
    }
}
```

In your assignment

**List what can go wrong before you code**

Before writing the reader, write down every way the input can be wrong. The sample assignment's README lists them, and each one has a test:

- the file is empty, or the first line is not a whole number ≥ 1
- fewer or more object lines than declared
- a line with too few or too many fields
- an unknown type code (codes are case-sensitive)
- a number that is not a number, not positive, or out of range
- a duplicate name or identifier
- anything after the last expected line

Before you present, run the program with a deliberately broken file yourself. A clear message naming the line is what a finished solution shows; a stack trace or a silently wrong result means the input handling isn't done yet.

## Anatomy of a good first assignment

The `Race` project is a complete solution of a typical first assignment: creatures of three species race for several days; each day is sunny, cloudy or rainy; each species gains or loses water and moves forward according to a table, and dies when its water runs out. The winner is whoever travelled the farthest while staying alive. The input looks like this:

teszt.txt

```text
4
Vandor h 4
Seta l 7
Csuszo s 12
Siklo s 10
nffeeennf
```

Name, species code and initial water per creature, then one letter per day (`n` sunny, `f` cloudy, `e` rainy). What makes the solution good is not the simulation – that is ten lines – but how the program is cut into parts.

### Three layers

Every class has one kind of job (Fig. 6). `Main` talks to the user. `RaceFileReader` turns text into objects. The model – `Race`, `Creature`, `Weather` – knows the rules of the race and nothing about files or the console.

> **Fig. 6** · System.out appears in one class, file handling in one other. The model can be tested, reused and later put behind a GUI without changing a line.
>
> *Diagram:* The three layers of the sample assignment. Main is the user interface: it gets the file name, prints the results, catches the exceptions and sets the exit code. RaceFileReader turns the text into objects and validates it. The model – Race, Creature and Weather – holds the rules only, with no files and no printing, so it can be unit-tested directly.

In your assignment

**Keep printing out of the model**

If `System.out.println` appears in your `Creature` class, the class can no longer be used in a window, and your tests can't check what it did. Let the model *return* results (a list of winners, a distance) and let `Main` print them. The second assignment adds a GUI – a model without printing moves over unchanged.

### An abstract base class that does the work once

The three species differ only in numbers. The *rules* of a day – change the water, cap it, die at zero, otherwise move – are the same for everyone, so they are written once, in the abstract `Creature` (Fig. 7).

> **Fig. 7** · Creature holds the state and the rules; each species overrides two small methods that return numbers from the table. Weather is an enum that knows its own letter in the file.
>
> *Diagram:* Class diagram of the sample assignment. Creature is abstract: it holds the name, water, capacity, distance and alive flag and implements liveThrough once; the abstract waterChange and distanceGain are supplied by the three species, SandWalker, Sponge and Walker. Race aggregates many creatures and many Weather values; Weather is an enum of SUNNY, CLOUDY and RAINY with fromCode.

Creature – the day, written once

```java
public final void liveThrough(Weather weather) {
    if (!alive) {
        return;
    }
    water = Math.min(maxWater, water + waterChange(weather));   // capped at the capacity
    if (water <= 0) {
        water = 0;
        alive = false;
        return;                                                  // no move on the day of death
    }
    distance += distanceGain(weather);
}

protected abstract int waterChange(Weather weather);
protected abstract int distanceGain(Weather weather);
```

SandWalker – only the numbers

```java
@Override
protected int waterChange(Weather weather) {
    return switch (weather) {
        case SUNNY -> -1;
        case CLOUDY -> 0;
        case RAINY -> 3;
    };
}
```

`liveThrough` is `final`, so no subclass can accidentally change the rules, and the species classes become a few lines each – almost a copy of the table in the task. This pattern, a fixed algorithm in the base class with abstract "slots" the subclasses fill, is called a **template method**.

The weather letters get the same treatment: an `enum` where every constant knows its letter, and a static `fromCode` that throws for an unknown one. The file format is written down in one place, and the rest of the program works with `Weather.RAINY`, never with `'e'`.

Weather.java

```java
public enum Weather {
    SUNNY('n'), CLOUDY('f'), RAINY('e');

    private final char code;

    Weather(char code) {
        this.code = code;
    }

    public static Weather fromCode(char code) {
        for (Weather weather : values()) {
            if (weather.code == code) {
                return weather;
            }
        }
        throw new IllegalArgumentException("Unknown weather code: '" + code + "'");
    }
}
```

### The simulation and the edge cases

Running the race is two nested loops: days outside, creatures inside, because every creature has to live through day 1 before anyone starts day 2 (Fig. 8).

> **Fig. 8** · The order inside one day matters: water first, then the check for death, and only then the move. A creature that dies today does not move today.
>
> *Diagram:* The simulation: for each day, in file order, every creature lives through the day. A dead creature skips it. A living one first changes its water, capped at the capacity; if the water drops to zero it dies and does not move that day, otherwise it moves forward by its distance for that weather.

The task says "the winner", but a good solution asks: what if two creatures tie, and what if nobody survives? `getWinners` returns a *list* – one element normally, several on a tie, empty if everyone died – and `Main` prints a sentence for each case.

Race.getWinners – ties and no survivors

```java
int bestDistance = 0;
for (Creature creature : creatures) {
    if (creature.isAlive()) {
        bestDistance = Math.max(bestDistance, creature.getDistance());
    }
}
List<Creature> winners = new ArrayList<>();
for (Creature creature : creatures) {
    if (creature.isAlive() && creature.getDistance() == bestDistance) {
        winners.add(creature);
    }
}
```

java race.Main teszt2.txt

```text
The race lasted 9 day(s).

Final standings:
  Vandor (sand walker): distance 17, water 4/8, alive
  Seta (walker): distance 11, water 1/12, alive
  Csuszo (sponge): distance 8, water 4/20, alive

Winner: Vandor (distance: 17)
```

### Tests and documentation

The sample comes with JUnit tests, one test class per class, and every input error from the list above has its own test. Because `Main` doesn't call `System.exit` directly but returns the exit code from a separate `run` method that receives its streams as parameters, even the whole program can be tested.

Main – testable entry point

```java
public static void main(String[] args) {
    System.exit(run(args, System.in, System.out, System.err));
}

// Testable: no System.exit inside, the streams come in as parameters.
static int run(String[] args, InputStream in, PrintStream out, PrintStream err) {
    // ...
}
```

RaceTest – one rule, one test

```java
@Test
public void deadCreatureCannotWinEvenWithTheLargestDistance() {
    // The sand walker runs 9 in three sunny days, then dies on the fourth.
    Race race = finishedRace(List.of(
            new SandWalker("Fast", 4),
            new Walker("Slow", 12)), "nnnn");

    assertFalse(race.getCreatures().get(0).isAlive());
    assertEquals(List.of("Slow"), names(race.getWinners()));
}
```

| Part of the submission | What it contains in the sample |
| --- | --- |
| Task description | The task in your own words, including the table of species and the input format. |
| Class diagram | All classes with their fields, methods and relationships, like Fig. 7. |
| Input validation | The list of invalid inputs and what the program does with each. |
| Usage | How to run it, from the IDE and from the command line, with a sample output. |
| Tests | Which test class covers what; every rule and every error case at least once. |
| Javadoc | A comment on every class and public method: what it does, parameters, what it throws. |

In your assignment

**What a good first assignment looks like**

- **An abstract base class** with the shared rules, and small subclasses with what differs. If two subclasses have the same method body, it belongs in the base class.
- **A reader class** that validates everything and throws your own checked exception with the line number.
- **A model without I/O**: no `Scanner`, no `System.out`, no file names.
- **One place that reports errors**: `main` catches, prints a clear message and exits with a non-zero code.
- **Edge cases decided on purpose**: ties, empty results, death on the last day. Write down what you decided in the documentation.
- **Tests** for each rule of the table and each kind of bad input, plus a few sample files next to the project – including a deliberately broken one.

## Practice

The `AnimalFarm` task follows the `Vehicles` pattern. A text file lists the animals of a farm; every animal is identified by its name and is a cow (`T`), a horse (`L`), a goat (`K`) or an emu (`E`). A line holds the kind, the name, the weight in kg, the number of meals that day, and the food eaten at each meal in decagrams (dkg, 10 g).

data.txt

```text
3
E Borisz 32 4 20 30 30 20
K Kelemen 21 3 30 40 50
T Narcisz 134 8 20 40 20 30 10 20 40 50
```

Task 1

**Read the farm**

Create an abstract `Animal` class that cannot be instantiated, and a subclass for each kind. Read the file into a list of animals. Throw your own `InvalidInputException` – with the line number – if the file can't be opened or a line is wrong: unknown kind, missing field, not a number, fewer meals than declared, or a non-positive weight or meal.

**Approach:**

Copy the shape of `Database.read`: a `BufferedReader` in try-with-resources, a Scanner per line, a factory method that switches on the kind letter. The weight check belongs in the `Animal` constructor, the meal check in `addMeal`; both throw `IllegalArgumentException`, which the line parser translates.

Task 2

**Malnourished animals**

An animal is malnourished if its weight is below the threshold of its kind: cow 100 kg, horse 60 kg, goat 12 kg, emu 20 kg. List the malnourished animals.

**Where does the threshold go?:**

The threshold differs per kind but the rule is the same – a perfect abstract method. Each subclass returns its own constant; the comparison is written once in the base class.

```java
public abstract class Animal {
    public abstract int getMalnourishedThreshold();

    public boolean isMalnourished() {
        return weight < getMalnourishedThreshold();
    }
}
```

Task 3

**Big eaters**

List the animals that ate more than 1 kg (more than 100 dkg) that day.

**Approach:**

Store the meals in a `List<Integer>` inside the animal, add a `totalConsumed()` method, and a `consumedMore(int dkg)` that compares. Keep the unit in the name or the comment – mixing kg and dkg is the classic bug here.

Task 4

**Without the count**

Change the reader so it works without the first line: read until the end of the file, and if the count is there, only use it to check the number of animals.

**Telling the count line apart:**

Animal lines start with a letter, so a first data line that starts with a number must be the count. Remember it, and compare it with the size of the list after the loop.

```java
// The animal lines start with a letter, so a first data line that
// starts with a number must be the count.
boolean isCountLine = declaredCount == null && loaded.isEmpty() && fields.hasNextInt();
```

Task 5

**Break it on purpose**

Write a broken data file with at least three different errors and run your program on it after each fix. Every run should stop with one clear message naming a line, and an exit code of 1. Then add a duplicate name and check that it is rejected too.

broken.txt

```text
3
E Borisz 32 4 20 30 30
X Kelemen 21 3 30 40 50
T Narcisz -5 1 20
```

**What should each line produce?:**

Line 2 has four meals declared but three amounts (data missing), line 3 has an unknown kind, line 4 a negative weight. Only the first error is reported – the program stops at the first exception, and that is fine. For duplicates, collect the names in a `HashSet` after reading.

## Summary

- Read files line by line and parse each line with its own Scanner; the structure of the file is the structure of the code.
- Use a factory method to turn a type letter into the right subclass, and keep the letters as constants.
- Checked exceptions (Exception) must be caught or declared; unchecked ones (RuntimeException) need not be. Write your own checked exception for bad input.
- Catch only where you can add something: translate low-level exceptions into yours with the line number, and report in one place – Main.
- Open files in try-with-resources; the file is closed on every path, and a try does not need a catch.
- Validate in constructors and in the reader, and keep nothing from a half-read file.
- A good first assignment has three layers – user interface, reader, model – an abstract base class with the shared rules, decided edge cases, tests and documentation.
