Phase 1 · Objects
A crash is an object you can catch
By the end of this page you'll catch a failure before it ends your program, read a file that always gets closed, and write an exception of your own that names the line where a file went wrong.
Ask a friend to pick up a book from the library on their way home. The library turns out to be shut. A good friend doesn't come back with some other book and say nothing. They don't stand outside the door all night, either. They come home and tell you: the library was shut. Then you decide what happens next. Until today, your programs had no way to do that. Every problem ended the whole program.
You've met the ending many times. In 0.4 it was / by zero. In 0.5 it was slot 5 of an array with five slots. In 1.1 it was a dot on null. In 1.2 you threw one yourself, and that chapter promised that catching would come later. This is later.
So this chapter is about bad news, and who gets to hear it. It's also about files, because files are where bad news comes from most. They go missing. They get half-written. They're full of typos.
Fair warning about two things coming up. One program will tell you, with total confidence, that three players scored nothing. And one method will run a line of code after its return. Neither is a bug in Java.
What you'll be able to do
- Catch an exception, and say who should catch it and who should let it fall.
- Explain why
javacforces you to deal withIOExceptionbut never withIllegalArgumentException. - Read a file one line at a time, and know for certain it gets closed, even when something goes wrong.
- Design an exception of your own that names the line of a file that was wrong, and keeps the original error inside it.
Section 1Bad news instead of an answer
Chapter 0.4 gave you a one-line meaning. An exception is Java's word for "something went wrong and I'm stopping". That was true. Here's the bigger truth underneath it.
An exception is an object. 1.2 hinted at this: new IllegalArgumentException("…") uses new, so it builds an object. Its class says what kind of problem it is. Its message says what happened. It lives on the heap, like every object since 1.1.
When something goes wrong, Java builds one of these objects and throws it. Throwing means three things at once. Stop this method right now. Don't hand back a value. Give this object to whoever called me instead.
Say your friend asked their brother to get the book. The brother asked a neighbour. The neighbour finds the library shut. The bad news travels back along that chain, one person at a time. Each person can either deal with it or pass it on. If it gets all the way back to you and you can't deal with it, the whole errand is off.
The chain of people is the stack of plates from 0.4. When a method throws, its plate comes off the spike. The exception lands on the plate below. If that method doesn't deal with it, its plate comes off too. And so on, down the spike.
If the exception falls off the bottom, out of main, the program stops. Java prints the stack trace on the way out. That's every wall of red text you've ever seen.
The new part is dealing with it. Here's a program that does. It's a cut-down version of 0.4's Boom, with a safety net at the bottom.
static int divide(int top, int bottom) {
return top / bottom;
}
static int average(int total, int count) {
int answer = divide(total, count);
System.out.println("average got its answer");
return answer;
}
public static void main(String[] args) {
try {
System.out.println("asking for the average");
int result = average(60, 0);
System.out.println("the average is " + result);
} catch (ArithmeticException e) {
System.out.println("caught it: " + e.getMessage());
}
System.out.println("main carries on");
}divide and average you know. average asks divide for an answer, prints a line saying it got one, and hands the answer back.
main is where the new words are. try { opens a block of code that you want to try. Everything up to the matching } is the try block. Inside it are three lines. A message, then the call to average with a count of zero, then a line printing the result.
catch (ArithmeticException e) { is the safety net under that block. It reads: "if an ArithmeticException falls out of the try block, catch it here." The e is a box, like a parameter. It gets a ticket for the exception object that was caught. You can call e anything, but e is what nearly everyone writes.
Inside the catch block, e.getMessage() asks the exception object for its message. Every exception has this method. Then the last line of main sits after the whole try-and-catch, on its own.
Five println lines in this program. Bet on which ones print. Two of them never do, and the interesting question is which two.
Which of the five println lines print, and in what order? What exactly does the caught it: line say?
$ java Fall
asking for the average
caught it: / by zero
main carries onaverage got its answer never printed. Neither did the average is. And there's no stack trace at all.
If you expected average got its answer, that's a fair guess. The line comes straight after the call, and the call did happen. But the call never came back. divide threw instead of returning, so average was stopped mid-line too. Its plate came off before its next line could run.
The message / by zero is the same text 0.4's trace showed. Last time it was the end of the program. This time main read it, printed it, and carried on.
That's the whole mechanism, and it's worth watching plate by plate.
Call stack — top plate first
Printed so far
Step 1 — inside the try block. One plate, for main. It has stepped into the try block and printed its first line. The catch block is waiting underneath, like a net. Nothing has gone wrong yet.
Call stack — top plate first
Printed so far
Step 2 — two calls deep. main called average, and average called divide. Three plates, the same picture as 0.4. Both lower plates are paused mid-line, waiting for an answer.
Call stack — top plate first
Heap
Step 3 — the throw. 60 / 0 has no answer. So Java builds an ArithmeticException object on the heap, with its message inside. Then it throws it. divide has no catch, so its plate comes off. No value gets handed back. The exception is handed down instead.
Call stack — top plate first
Heap
Step 4 — still falling. The exception lands in average, in the middle of int answer = divide(...). That line isn't inside any try block, so average can't deal with it. Its plate comes off too. answer never got a value, and average got its answer never runs.
Call stack — top plate first
Heap
Step 5 — caught. It lands in main, inside the try block. Java checks the catch: does it catch ArithmeticException? Yes. So the rest of the try block is skipped, and the average is never runs. The catch block starts. Its box e gets a ticket for the exception object.
Call stack — top plate first
Printed so far
Step 6 — carrying on. The catch block prints the message. Then it ends, and main carries on with the line after it, as if nothing happened. Nothing points at the exception object any more, so Java is free to reclaim it later.
A catch only catches its own kind. If the catch had said NullPointerException, the arithmetic one would fall straight past it. The program would stop with the usual trace. You'll try that in Your turn.
And one more thing to see before moving on. throw, the word you wrote in 1.2, does by hand what 60 / 0 did by itself. throw new IllegalArgumentException("cannot add USD to GBP") builds the object and starts it falling. Now you know where it falls to.
- An exception is an object. Its class is the kind of problem, and
getMessage()gives its message. - Throwing stops the method on the spot, hands back no value, and passes the exception down to the caller.
- It keeps falling, plate by plate, until a
catchfor its kind catches it. Out ofmain, and the program stops with a stack trace. - When it's caught, the rest of the try block is skipped. The code after the catch carries on as normal.
Section 2Who has to plan for it: checked and unchecked
Back in 0.6 you read a whole file with one line. The main method in that file ended with two words you were told to copy: throws IOException. Time to find out what happens if you don't.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public static void main(String[] args) throws IOException {
String text = Files.readString(Path.of("scores.txt"));
System.out.print(text);
}The three import lines fetch tools from Java's toolbox, as import did for Scanner in 0.3. IOException lives in java.io, and Files and Path in java.nio.file. Then comes main, with the copied phrase on the end. The next line reads the whole of scores.txt into one String, exactly like 0.6. The line after prints it. print is println without the jump to a new line, because the file already ends with one.
Open ReadScores.java, delete throws IOException from main's first line, and save. Then compile it. Most people guess it compiles fine. The phrase looks like a comment for humans. Does javac even read it?
With throws IOException deleted, does javac ReadScores.java compile it? If it refuses, which line does it point at?
$ javac ReadScores.java
ReadScores.java:13: error: unreported exception IOException; must be caught or declared to be thrown
String text = Files.readString(Path.of("scores.txt"));
^
1 errorIt refuses. And it points at readString, the one call that can fail, not at main's first line.
Read the message slowly, because it lists your only two choices. must be caught: put the call in a try block with a catch for IOException. or declared to be thrown: write throws IOException on the method, which passes the problem on to whoever called you.
That second choice is what 0.6 had you do. main is the bottom plate, and nobody calls it except Java. So "pass it on" meant: if it happens, stop the program with a trace. Now you know what you were copying. Put the phrase back before you go on.
Now the puzzle. Fall had 60 / 0 in it, and javac never once asked you to catch ArithmeticException. 1.1's NullDog crashed with no complaint at compile time. So why is IOException special?
Two families, and why
An IOException is a checked exception. The compiler checks every call that might throw one. Each call must sit inside a try that catches it, or inside a method that declares it with throws. If not, nothing compiles.
The ones you'd met before were all unchecked exceptions. The compiler doesn't check them at all. You may catch them, and you may not.
The difference is about whose fault the problem is.
You're cooking. Burning the toast is your mistake, and you can avoid it by watching the toaster. Nobody makes you sign a plan for burnt toast. A power cut is different. It can happen however careful you are. So a good kitchen keeps a torch in the drawer. Care alone can't prevent a power cut, so you plan for it.
Dividing by zero, a dot on null, slot 5 of five: those are burnt toast. They're bugs, and the fix is to change the code. Imagine a forced catch around every division. Every program would be buried in catch blocks for things that should never happen.
A file is the power cut. Someone deletes it while your program runs. A USB stick gets pulled out. The disk fills up. None of that is a bug in your code, and no amount of care prevents it. So Java makes you plan for it, in writing, before it will compile.
The family tree
Every exception class has a parent class, named with extends, the way 1.3's Square had Rectangle. The whole family looks like this. A few in-between parents are left out to keep it readable.
Throwable anything that can be thrown
├── Error the machinery broke: don't catch
│ └── StackOverflowError 0.4's runaway recursion
└── Exception
├── IOException CHECKED: reading and writing files
│ └── FileSystemException
│ └── NoSuchFileException
└── RuntimeException UNCHECKED: this one and all below it
├── ArithmeticException 0.4's / by zero
├── ArrayIndexOutOfBoundsException 0.5's slot 5 of five
├── NullPointerException 1.1's dot on null
├── ClassCastException 1.4's cast that lied
└── IllegalArgumentException 1.2's pounds plus dollars
└── NumberFormatException you'll meet this in Section 4The rule is one sentence. Anything under RuntimeException or Error is unchecked, and everything else under Exception is checked.
Error is 0.4's warning made official. "An Error is not the same as an Exception," it said. Now you can see where each one sits.
And here is the answer to "who decides?". Whoever writes the exception class decides, when they choose its parent. Java's authors put IOException under Exception, so every caller in the world must plan for it. Later this chapter you write an exception of your own. Choosing its parent will be your decision, and you'll have to defend it.
Going deeper Why "throws" on a method is a promise to every caller
throws IOException isn't only about the method it's written on. It's part of the method's first line, the bit callers see. So it changes what they must do.
Say loadGame() declares throws IOException. Now every method that calls loadGame() faces the same two choices: catch it, or declare it too. Declarations spread down the call stack until someone catches.
That spreading is the main complaint against checked exceptions. People get tired of it. Then they write an empty catch to make the compiler stop asking, which is worse than not catching at all. Section 3 shows exactly how much worse.
Unchecked exceptions can appear in a throws too. It compiles, and it means nothing to javac. Some libraries write it anyway, as a note to human readers.
Which of these lines would javac refuse to compile on its own, with no try around it and no throws?
- A checked exception must be caught, or declared with
throws. If not,javacwon't compile it.IOExceptionis the one you'll meet most. - An unchecked exception is anything under
RuntimeExceptionorError. The compiler leaves you to it. - The split is about fault. Unchecked usually means a bug you fix in the code. Checked means trouble that care can't prevent, like a missing file.
- The author of an exception class decides which family it's in, by choosing its parent.
Section 3One file, three wrong ways
Here's the file all three programs read. It's three players and their scores.
$ cat scores.txt
Ada 12
Grace 15
Linus 9Files.readString would hand you all of that in one go. That's perfect for a small file. A 10 GB log file is another matter. It would need 10 GB of memory to hold the text before you'd read a word of it.
So there's a second way to read, one line at a time. And it comes with a responsibility that readString quietly handled for you.
When your program opens a file, the operating system lends it a slot for open files. There are only so many slots, shared by everything on the computer. Giving the slot back is called closing the file. The thing that reads it for you, line by line, is a reader. Here it is, in the first of three wrong programs.
Wrong way 1: never giving it back
LeakyCount.java starts with four imports. Three are the ones from ReadScores, and one is new.
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;BufferedReader lives in java.io, next to IOException. Every program in this section and the next starts with these same lines.
static int countLines(Path path) throws IOException {
BufferedReader reader = Files.newBufferedReader(path);
int count = 0;
String line = reader.readLine();
while (line != null) {
count = count + 1;
line = reader.readLine();
}
return count;
}Line 1 says this method needs a Path, hands back an int, and might throw IOException. It doesn't know what to do about a missing file, so it passes the problem to its caller. That's the "declared" choice from Section 2.
Line 2 opens the file. Files.newBufferedReader(path) borrows a slot and hands you a BufferedReader object. "Buffered" means it reads a big chunk from the disk at once, then hands it to you a line at a time. Disks are slow, so that saves a lot of waiting.
Line 3 starts a count at zero. Line 4 asks the reader for the first line of the file. readLine() hands back one line of text, without the new-line character on the end.
Line 5 is the clever bit. When there are no lines left, readLine() hands back null — 1.1's empty pocket. So "keep going while the line isn't null" means "keep going until the end of the file".
Lines 6 and 7 count the line and fetch the next one. Line 9 hands back the count. And notice what isn't there. Nothing ever gives the slot back.
main calls this method 100,000 times in a loop. A busy program might check a file over and over like that. It's inside a try block. If an IOException comes out, the catch prints how far it got, then prints e itself. Printing an exception shows its full class name and its message, on one line.
How many files may one program hold open at once? It's a limit set by your computer, usually somewhere in the thousands. That's big enough to hide this leak for a long time. So we'll lower it to 100 for one run.
$ (ulimit -n 100; java LeakyCount)ulimit -n 100 tells the terminal "let a program hold at most 100 open files". The ; separates two commands. The round brackets run both of them in a throwaway copy of your terminal. So the limit disappears when they finish, and your real terminal is untouched.
100,000 reads of a three-line file, and a limit of 100 open files. Does it finish? If not, roughly when does it stop, and what does the last line say?
$ (ulimit -n 100; java LeakyCount)
stopped after 95 reads
java.nio.file.FileSystemException: scores.txt: Too many open filesIt stopped after 95 reads. Not 100, because Java had a few files open for itself before your code started. The terminal it prints to is one. Your number may be slightly different.
Look at the message. Too many open files. Every one of those 95 readers was still holding its slot. The file was fine. The disk was fine. The program had borrowed every slot it was allowed.
This is a resource leak, and it's nasty precisely because it's slow. A test that reads the file once passes. A program that runs for a week on a busy server falls over on Thursday.
Wrong way 2: the empty catch
The second program adds up the scores. It uses two things worth a sentence each.
line.split(" ") is from 0.6. It chops "Ada 12" at the space into an array of two Strings, "Ada" and "12". Integer.parseInt(...) is new. It turns the text "12" into the number 12, so you can add it up.
int total = 0;
try {
BufferedReader reader = Files.newBufferedReader(Path.of("score.txt"));
String line = reader.readLine();
while (line != null) {
String[] parts = line.split(" ");
total = total + Integer.parseInt(parts[1]);
line = reader.readLine();
}
reader.close();
} catch (IOException e) {
}
System.out.println("total: " + total);Line 1 starts the total at zero. Line 3 opens the file. Look carefully at the name: score.txt, with no s. That's the typo. Lines 4 to 9 are the reading loop you know. Now it splits each line and adds slot 1, the number, to the total.
Line 10 closes the reader, giving the slot back. close() is the method that does it. Line 11 is the catch, and its block is empty. Somebody wrote it to make javac stop asking, which is the exact temptation from Section 2. Line 13 prints the total.
There's no file called score.txt. So newBufferedReader will throw. What does the program print?
$ java SilentTotal
total: 0total: 0. No error, no trace, no hint. The program has told you, with total confidence, that three players scored nothing.
The exception did happen. It was thrown, it fell into the catch, and the catch did nothing with it. Then the program carried on, printing a total that was never added up.
An empty catch is taking the battery out of a smoke alarm because it beeps when you make toast. The alarm is quiet now. That was never the problem.
Wrong way 3: catching too much
The third program has the right file name. It has a different bug: parts[2] instead of parts[1]. That's one slot too far, in an array with two slots. And its catch is wider.
total = total + Integer.parseInt(parts[2]);
...
} catch (Exception e) {
System.out.println("could not read scores.txt - is it missing?");
}The first line is the bug. The catch now says Exception. Look at the family tree. Exception is the parent of nearly everything, so this catch catches nearly everything. It assumes the only thing that can go wrong is the file, and says so.
This one catches professionals. The author meant well. They wanted a friendly message instead of a crash.
scores.txt is right there in the folder. What does BlameTheFile print? Is the message telling the truth?
$ java BlameTheFile
could not read scores.txt - is it missing?
total: 0The file isn't missing. The file was opened and read perfectly well. The real problem was parts[2], which threw an ArrayIndexOutOfBoundsException, a bug in our code.
But catch (Exception e) caught that too, because it's under RuntimeException, which is under Exception. Then the catch printed the one excuse it knew. So now you'll spend your afternoon checking a file that was never the problem.
There's a second leak hiding in here too. The exception was thrown from inside the loop. So reader.close() on the line after the loop never ran. Remember that for the next section.
Close what you open, every time, even when something goes wrong. Never leave a catch empty. If you truly can't do anything, don't catch. Catch the narrowest kind you can actually deal with. Let your own bugs crash loudly, with their real name and line number.
- A
BufferedReaderreads a file a line at a time.readLine()hands backnullat the end of the file. - An open file is a borrowed slot. Forget to
close()it, and a long-running program runs out: Too many open files. - An empty catch turns an error into a believable wrong answer. That's worse than a crash.
catch (Exception e)catches your own bugs too, and blames them on whatever you guessed.
Section 4Closed on the way out, whatever happens
BlameTheFile called reader.close(). It was right there, on the line after the loop. It still leaked, because the exception jumped straight past it. Lines after a throw don't run. That's Section 1's whole story.
So "close it at the end" isn't enough. You need "close it at the end, however the end happens". Java has a block for exactly that.
finally
static String check(int n) {
try {
System.out.println("trying " + n);
if (n == 0) {
throw new IllegalArgumentException("zero is not allowed");
}
return "fine";
} finally {
System.out.println("finally, for " + n);
}
}
public static void main(String[] args) {
System.out.println(check(1));
System.out.println(check(0));
}check takes a number. Inside the try block it prints that it's trying. If the number is zero, it throws an IllegalArgumentException, like 1.2's Money did. If not, it returns the text "fine".
Then comes a new word. finally { opens a finally block. Its rule: this code runs when the try block ends, however it ends. Normally, by a return, or by an exception falling out. There's no catch here at all, which is allowed. A try needs a catch, a finally, or both.
main calls check(1) and prints what comes back. Then it does the same with check(0).
Here's the one to think hard about. check(1) reaches return "fine";. A return ends the method on the spot — 0.4 called it "the door". So does the finally block get a chance at all?
Write down every line of output, in order. Where does fine appear compared with finally, for 1? And what happens with check(0)?
$ java FinallyRuns
trying 1
finally, for 1
fine
trying 0
finally, for 0
Exception in thread "main" java.lang.IllegalArgumentException: zero is not allowed
at FinallyRuns.check(FinallyRuns.java:11)
at FinallyRuns.main(FinallyRuns.java:21)finally, for 1 comes before fine. If that surprised you, good, because it surprises nearly everyone.
return "fine" did decide the answer. But before the method could leave, Java ran the finally block. Only then did the answer reach main, and main printed it. The door was open, and finally still got through it first.
Then check(0) threw. Nothing caught it. But on its way out of check, the exception stopped for the finally block too. Only then did it carry on falling, out of main, into the trace.
So you could put reader.close() in a finally block, and it would always run. People did, for years. It's fiddly, though. The reader has to be declared outside the try. And you have to check it isn't null, in case opening failed. So Java added a shorter form that does all that for you.
try-with-resources
Here's LeakyCount's method again, with one change. It's in src/TidyCount.java.
static int countLines(Path path) throws IOException {
int count = 0;
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line = reader.readLine();
while (line != null) {
count = count + 1;
line = reader.readLine();
}
}
return count;
}Look at line 3. There are round brackets straight after try, and the reader is opened inside them. This is called try-with-resources. Anything opened in those brackets is a resource: something borrowed that must be given back.
The rule: when the block ends, Java calls the resource's close() for you. Normally, or by return, or by an exception falling out. It's a finally block that you don't have to write. Lines 4 to 8 are the same reading loop as before. Line 10 hands back the count, after the file is already closed.
There's no catch here, and that's fine. countLines still declares throws IOException, so a missing file still falls to its caller. It falls with the slot already given back.
$ (ulimit -n 100; java TidyCount)
read the file 100000 timesSame limit of 100. A hundred thousand reads, and never more than a few files open at once. One pair of brackets is the whole fix.
What can go in the brackets, and the order they close in
Not just readers. Anything whose class implements AutoCloseable can go in there. AutoCloseable is an interface, like 1.3's Shape, with a single method: close(). That close() is the method try-with-resources calls when the block ends. Files, network connections and database connections all implement it.
So you can write your own. src/CloseOrder.java has a tiny class that prints a line when it's opened and when it's closed.
static class Noisy implements AutoCloseable {
private final String name;
Noisy(String name) {
this.name = name;
System.out.println("open " + name);
}
@Override
public void close() {
System.out.println("close " + name);
}
}
public static void main(String[] args) {
try (Noisy a = new Noisy("A"); Noisy b = new Noisy("B")) {
System.out.println("working...");
int score = Integer.parseInt("twelve");
System.out.println("never printed " + score);
} catch (NumberFormatException e) {
System.out.println("caught: " + e.getMessage());
}
}Noisy keeps a name in a private final field, from 1.2. Its constructor stores the name and prints open. Its close() prints close. @Override asks javac to check that this really is the close() the interface promised, as in 1.2.
In main, the brackets open two resources, separated by ;: first a, then b. Inside, it prints a line, then asks Integer.parseInt to read "twelve". That's not digits, so it throws a NumberFormatException. This time there's a catch for it, after the block.
Two questions to bet on. The exception happens while both are open. Do they get closed at all? And does the closing happen before or after the catch prints?
Write the six lines in order. Which closes first, A or B? And does caught: come before the closing or after it?
$ java CloseOrder
open A
open B
working...
close B
close A
caught: For input string: "twelve"B closes first. Resources close in the reverse order they opened. Think of putting on socks and then shoes. You take off the shoes first. A later resource is often built on top of an earlier one. A reader sits on top of an open file, for example. So it must go first.
And both close before the catch runs. By the time your catch block starts, everything in the brackets has already been given back. Your catch never has to wonder whether the file is still open.
The scores, done properly
src/ScoresRight.java fixes all three wrong ways at once.
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
static int total(Path path) throws IOException {
int total = 0;
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line = reader.readLine();
while (line != null) {
String[] parts = line.split(" ");
total = total + Integer.parseInt(parts[1]);
line = reader.readLine();
}
}
return total;
}
public static void main(String[] args) throws IOException {
String file = "scores.txt";
if (args.length > 0) {
file = args[0];
}
try {
System.out.println("total: " + total(Path.of(file)));
} catch (NoSuchFileException e) {
System.out.println("There is no file called " + file + ".");
}
}The imports are LeakyCount's four, plus NoSuchFileException, which lives in java.nio.file with Files and Path. total is TidyCount's shape, adding up slot 1 of each line. The file is always closed, by the brackets. It declares throws IOException, because total has no idea what a missing file should mean to your program. Its caller does.
main picks the file name. It's scores.txt unless you type another name after the program's name. args holds those extra words, as in 0.7's Wordle.
Then main catches exactly one kind: NoSuchFileException, the child of IOException from the tree. That's the problem it knows how to explain, so that's the only one it catches. Any other disk trouble still falls out, because main says throws IOException. And a bug like parts[2] would crash with its real name, because nothing here catches RuntimeExceptions.
$ java ScoresRight
total: 36
$ java ScoresRight score.txt
There is no file called score.txt.36 is 12 + 15 + 9. The typo now gets a clear sentence instead of a zero.
You can have several catch blocks, one after another. Java tries them top to bottom. So try adding a catch (IOException e) above the NoSuchFileException one, printing could not read it.
$ javac ScoresRight.java
ScoresRight.java:39: error: exception NoSuchFileException has already been caught
} catch (NoSuchFileException e) {
^
1 errorEvery NoSuchFileException is an IOException, so the first catch would always get it. The second could never run. javac refuses to compile dead code like that. Most specific first, most general last.
- A finally block runs however the try ends, even after a
return. - try-with-resources,
try (X x = …) { }, callsx.close()for you, however the block ends. Use it for every file you open. - Resources close in reverse order, and before any catch block runs.
- Declare what you can't sensibly handle. Catch only the specific kinds you can turn into a clear message.
Section 5An exception of your own
ScoresRight has one weakness left. Here's a file a tired teacher typed.
$ cat bad-scores.txt
Ada 12
Grace twelve
Linus 9Feed that to ScoresRight and Integer.parseInt("twelve") throws a NumberFormatException. Its message is For input string: "twelve". That's true, and nearly useless. Which file? Which line? A teacher with a 300-line file needs to know where.
None of Java's ready-made exceptions carries a line number. So we write our own.
public class ScoreFileException extends RuntimeException {
private final int lineNumber;
public ScoreFileException(String problem, int lineNumber, Throwable cause) {
super(problem + " on line " + lineNumber, cause);
this.lineNumber = lineNumber;
}
public int lineNumber() {
return lineNumber;
}
}Line 1 is the decision, and it's three words long. extends, from 1.3, says ScoreFileException is a kind of RuntimeException. Look back at the family tree. That puts it on the unchecked side, for every caller, forever.
Line 3 is one field, private final, as in 1.2's Money. It holds the number of the line that was wrong.
Line 5 is the constructor. It takes three things: a description of the problem, the line number, and a cause. The cause is the exception that led to this one, here the NumberFormatException. Its type is Throwable, the top of the family tree. So any exception at all can be a cause.
Line 6 is super, which 1.3's squares used. It runs the parent's constructor, RuntimeException's, and hands it two things. One is the full message, like not a score: twelve on line 2. The parent stores it, and that's what getMessage() hands back later. The other is the cause, which the parent keeps too. Every exception class in Java has a constructor like this, taking a message and a cause.
Line 7 fills our field. Lines 10 to 12 are a getter. So the line number isn't only buried inside the message text. A program can ask for it as a number.
Throwing it, with its cause
src/ScoreReader.java is ScoresRight with one change inside the loop.
static int total(Path path) throws IOException {
int total = 0;
int lineNumber = 0;
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line = reader.readLine();
while (line != null) {
lineNumber = lineNumber + 1;
String[] parts = line.split(" ");
try {
total = total + Integer.parseInt(parts[1]);
} catch (NumberFormatException e) {
throw new ScoreFileException("not a score: " + parts[1], lineNumber, e);
}
line = reader.readLine();
}
}
return total;
}Line 3 starts a line counter, and line 7 adds one to it for each line read. Lines 9 to 13 are a try inside the loop, around the one call that can fail on bad text.
Line 11 catches the NumberFormatException. Line 12 throws ours in its place. It hands over the bad word, the line number, and e, the original, as the cause. So nothing the original knew is lost. We've only added what it couldn't know: where.
A catch that throws something else is called wrapping an exception. The new one is the wrapper, and the old one is inside it.
Why unchecked? Both sides
This was a real choice, and reasonable people disagree. So here are both sides.
For unchecked. Often the file is one you made yourself, and if it's broken, there's nothing useful to do except stop. Forcing a try/catch around every total call is what makes people write Section 3's empty catch. Then a broken file becomes a silent wrong answer.
Against. Sometimes the file came from somebody else, and you should catch the error and explain it. With an unchecked exception, javac won't remind you. Forget, and your program crashes on the first bad file.
We take the first side. Real libraries that read text people type often make the same choice. Gson, Google's library for reading JSON in Java, does. Its JsonSyntaxException extends a class that extends RuntimeException. You can check this in Gson's source, in the file com/google/gson/JsonSyntaxException.java.
Here's the choice in action. src/ScoresUncaught.java adds up the bad file, with no catch for our exception anywhere.
public static void main(String[] args) throws IOException {
System.out.println("adding up bad-scores.txt");
int total = ScoreReader.total(Path.of("bad-scores.txt"));
System.out.println("total: " + total);
}Line 1 declares throws IOException, because total declares it. That covers the checked one, and only that one. Line 2 prints a message. Line 3 calls ScoreReader's static total, from the other file, on the bad file. Line 4 would print the result.
Does this compile, with no catch for ScoreFileException? If it runs, what's the message? And there's a cause inside our exception. Does the trace show it?
$ java ScoresUncaught
adding up bad-scores.txt
Exception in thread "main" ScoreFileException: not a score: twelve on line 2
at ScoreReader.total(ScoreReader.java:27)
at ScoresUncaught.main(ScoresUncaught.java:12)
Caused by: java.lang.NumberFormatException: For input string: "twelve"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at java.base/java.lang.Integer.parseInt(Integer.java:786)
at ScoreReader.total(ScoreReader.java:25)
... 1 moreIt compiled without a word. That's what unchecked means.
Then it stopped, with two exceptions in one trace. The top one is ours. It says what was wrong and where: line 2 of the file. Its first at line is line 27 of ScoreReader.java, the throw.
Below it, Caused by: is the original, with its own little trace. The java.base lines are inside Java's own parseInt. Then line 25 of ScoreReader.java, the parseInt call. ... 1 more means the rest is the same as above, which is main. Nothing was lost on the way.
Now the version that catches it. ScoreReader's own main catches ScoreFileException and uses the getter.
} catch (ScoreFileException e) {
System.out.println(file + ": " + e.getMessage());
System.out.println("Fix line " + e.lineNumber() + " and run it again.");
}Line 2 prints the file name and our message. Line 3 asks for the line number as a number, and builds an instruction out of it.
$ java ScoreReader
total: 36
$ java ScoreReader bad-scores.txt
bad-scores.txt: not a score: twelve on line 2
Fix line 2 and run it again.A good file still adds up to 36. A bad one gets a message that names the file, the word and the line. That's what a person can act on.
Wrapping a checked one in an unchecked one
The same constructor does a second job, and 1.8's object mapper leans on it. Sometimes you catch a checked exception and don't want to pass the checked kind on. Maybe nobody calling loadSettings can do anything about a missing settings file. Then making every caller deal with IOException helps nobody.
static String loadSettings() {
try {
return Files.readString(Path.of("settings.json"));
} catch (IOException e) {
throw new RuntimeException("could not load the settings", e);
}
}
public static void main(String[] args) {
System.out.println("starting up");
String settings = loadSettings();
System.out.println(settings);
}Line 3 tries to read settings.json, and there isn't one in the folder. Line 4 catches the IOException. Line 5 throws a plain RuntimeException, which is unchecked, with a message saying what we were doing and e as the cause. So loadSettings needs no throws, and main needs no catch.
$ java Wrapped
starting up
Exception in thread "main" java.lang.RuntimeException: could not load the settings
at Wrapped.loadSettings(Wrapped.java:17)
at Wrapped.main(Wrapped.java:23)
Caused by: java.nio.file.NoSuchFileException: settings.json
… then about a dozen lines inside Java's own file-reading code. I trimmed them.Same shape as before. On top, what we were doing. Under Caused by:, what actually went wrong. Remember it: catch the checked one, throw an unchecked one, pass the original as the cause. The object mapper you build in 1.8 uses exactly this.
- An exception class of your own is a class that
extendsan exception class. Its parent decides checked or unchecked. super(message, cause)hands both to the parent's constructor. That's wheregetMessage()and the Caused by: line come from.- Put useful facts in fields with getters, like
lineNumber(), not only inside the message text. - Wrapping keeps the original as the cause and adds what it couldn't know. It also turns a checked exception into an unchecked one.
Everything is in this chapter's src/ folder. Compile it all once with javac *.java, then run each program. A few steps ask you to break a file on purpose. Put it back afterwards, or the next javac *.java will stop on it. Predict on paper before every run.
Section 6Check yourself
A friend's method reads a settings file. They've wrapped it in try { … } catch (Exception e) { } "so it never crashes". What's your advice?
You're writing class SaveFailedException extends ???. You want javac to make every caller of saveGame() plan for it. What goes after extends?
Judgment call. Teachers upload their own scores files to your program, which calls ScoreReader.total on each one. ScoreFileException is unchecked. What should you do?
Section 7What broke, and what surprised you
Write this down in your own words before moving on. The predictions you got wrong are the useful ones. Each points at one belief you can now replace.
Worth answering properly, on paper:
- Which predictions did you get wrong? For each one, what were you picturing instead?
- In
Fall, why didaverage got its answernever print, even though the call todividedid happen? - Why does
javacforce you to plan forIOExceptionbut not forArithmeticException? Use the toast and the power cut. - Did
finally, for 1coming beforefinesurprise you? Explain it in one sentence. - What does
ScoreFileExceptiontell a person that theNumberFormatExceptioninside it couldn't? - Why does
ScoreReaderpasseinto the new exception? What would you lose without it?
Your programs can now read files, fail loudly, and say exactly where a file went wrong. Chapter 1.6 asks a stranger question: can a program look inside a class it has never seen? Then, in 1.8, you'll use everything from this chapter to build the part of a JSON library that reads text, and reports a bad file by its line and column.
Nice. Chapter marked complete.