CSE 331: Error handling and interactive programs
Course motto: Your job is to deliver code that you know works.
A calculator
Today's program is a calculator, in two halves: main, which talks
to the user, and CalcStack, which does the arithmetic. Read both
before you read on. Here is main:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
The calculator keeps a stack, a list of numbers that grows and
shrinks at one end, its top. Typing a number pushes it onto the
top. An arithmetic operation, such as +, pops the top two numbers,
combines them, and pushes the answer. Writing the operation after
its numbers, as in 1 2 +, is called
reverse Polish notation.
In each run below, a line after a > prompt is what the user typed:
$ javac Calculator.java
$ java Calculator
> 1 2 + .
3
> q
1 and 2 go onto the stack, + replaces them with 3, and .
pops the 3 and prints it. Here is a longer run, with p printing
the whole stack, bottom first, after each step:
$ java Calculator
> 42
> 1 3 10 17
> p
[42 1 3 10 17]
> + p
[42 1 3 27]
> - p
[42 1 -24]
> .
-24
> q
42 went onto the stack with no output: the calculator shows its
stack only when asked. + added the top two numbers, 10 and 17.
- then subtracted the top number from the one below it: 3 minus
27, not 27 minus 3, as the class comment says.
Interactive programs
The stack is not why we are looking at the calculator. We are
looking at it because it is interactive.
Lecture 3's Sum
read all its input, computed, wrote its output, and exited: a batch
program.
Lecture 3 promised
a later lecture on programs of the other kind. The calculator is
such a program:
Definition: interactive program
An interactive program responds to each input as it arrives, and keeps running until the user tells it to stop.
main's loop, on lines 41 to 109, never ends on its own. Each time
around, the loop prints the prompt >, reads one line, and
responds to each word on the line. Each such word is a token.
The loop ends in only two ways. q returns from main, on line 52.
Ending the input (Control-D on macOS and Linux, or Control-Z and
Enter on Windows) makes hasNextLine on line 43 false, and break
leaves the loop.
The difference shows when the user makes a mistake. Sum stopped
at the first mistake: it printed a message on standard error and
exited with status 1. A user who types + with one number on the
stack expects to keep the stack and fix the mistake. So the
calculator prints a message and keeps going:
$ java Calculator
> 1 +
error: need two numbers to add
> p
[1]
> x
error: don't understand 'x'
> 2 + p
[3]
> q
Each mistake gets a message. The operation that failed leaves the
stack alone. For that to work, main has to learn of every
operation that could not be done, so that it can tell the user and
go around the loop again.
When a method cannot do its job
That run has two mistakes in it, + on one number and x, both
the user's. Three kinds of trouble matter here:
- A user's mistake. The user typed something the program cannot
use, such as
x. Lecture 3 met these with checks and messages at the edge of the program, where the user's input arrives, and never with a stack trace: that is one of this course's rules. - A programmer's mistake. Some code breaks a promise, such as a precondition. The code may be ours, a coworker's, or a library's. Either way, the specification promises nothing about what happens next.
- A request that cannot be carried out. A client asks a method
for something the method cannot do. Popping a number off an empty
stack is such a request. So is asking
Lecture 2's
indexOffor a value that is not in the array: there is no index to return.
In the calculator, the first leads to the third. The design is why:
the program keeps two concerns apart. main owns the conversation
with the user: the prompt, the tokens, the messages. CalcStack
owns the stack and its arithmetic, and knows nothing of users.
Giving each concern one home is called separation of concerns.
Its price is paid at the boundary. The user's + on a stack of one
number crosses into CalcStack as a request it cannot carry out,
and CalcStack cannot print a message, since its client is main,
not a person. So CalcStack has to tell main that the operation
failed, and main, at the edge, tells the user. The only words the
two halves share are CalcStack's specifications, so every way of
saying no is a clause in one. The question for the rest of this
lecture is this: when a method cannot do its job, how does it say
no to its client?
CalcStack answers that question six different ways, one for each
of its operations that can fail. It does so on purpose, to show all
six side by side. If each method had its own way, a class's clients
would have to learn each way separately. So a real class picks one
way and uses it everywhere. Here is CalcStack, the rest of the
program:
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | |
Most of CalcStack's methods change the stack they are called on.
In a specification, this names that stack. So @modifies this
says that the method may change the stack, and @effects says how.
A later lecture, on data abstraction, gives such specifications
their full form. Here is how each operation says no, and how main
responds:
*, a precondition.multiplyrequires2 <= size(), andmaincheckssize()before the call (line 83).., a special value.popreturnsnull, andmainchecks fornull(line 57).+, a flag.addreturnsfalse, andmainchecks the flag (line 71).t, a wrapper.topreturns an emptyOptionalInt, andmainasks whether it is empty (line 64)./, an exception.dividethrows one, andmaincatches it (lines 92 and 95).-, a checked exception.subtractthrows one, andmaincatches it (line 78).
The run below starts from an empty stack. Each of the six
operations fails in turn, and main reports each failure:
$ java Calculator
> .
error: stack is empty
> t
error: stack is empty
> +
error: need two numbers to add
> -
error: need two numbers to subtract
> *
error: need two numbers to multiply
> /
error: need two numbers to divide
> 1 0 /
error: cannot divide by zero
> p
[1 0]
> q
To the user, the six look alike. In the code, they are six different
contracts between CalcStack and main. A precondition is the
simplest of the six, so it comes first.
A precondition
multiply's specification requires 2 <= size().
As Lecture 2 said,
a precondition is the client's promise, and the method may assume
it. If the client breaks the promise, the method may do anything at
all and still satisfy its specification. So multiply checks
nothing. Checking is main's job, and main does check, on
line 83.
This is
Lecture 3's arrangement.
Lecture 2 gave the reason:
the user owes the program nothing, so the check stays at the edge,
in main, where the user's input arrives. The method's client,
though, is code that can be held to a promise, so the method may
have a precondition.
With a precondition, the method itself never says no: the client
checks before the call. That suits a client that can check cheaply.
Here size() makes the check one comparison.
Checking a precondition anyway, inside the method, is called
defensive programming:
multiply could test size() itself and throw an exception when the
promise is broken. A broken precondition allows anything, so the
specification would be the same either way.
multiply does not check, on
purpose, so that the section
When the client forgets
can show what happens when nobody checks.
Some clients cannot check cheaply. Before calling Integer.parseInt
on line 101, main would have to scan the token for digits and work
out whether the number fits in an int. That is as much work as
parsing it. For such a client, the method itself has to check, and
tell the client what the check found. The other five ways do that.
A special value
pop returns the number it removed. When the stack is empty, there
is no number to return, so it returns null instead. A value that a
method returns only when it could not do its job is a
special value.
Lecture 2's indexOf
used one too, -1 for a value that is not in the array, and left
its downsides for later.
pop's return type is Integer, not int. Java has two kinds of
types. A primitive type, such as int or boolean, holds a
plain value. Every other type, such as String, Integer, or an
array, is a reference type. A variable of a reference type may
hold null, which refers to nothing. int has no null, so
pop uses Integer, a reference type whose values are ints, plus
null. Java converts between the two by itself. On line 134, pop
returns the int last, and Java turns it into an Integer. That
direction always works. The other direction fails on null, as
the section
When the client forgets shows.
A special value has to be a value the method never returns
otherwise. An index is never negative, so -1 works for indexOf.
Suppose top returned -1 for an empty stack. A client that got
-1 could not tell an empty stack from a stack whose top number is
-1, so -1 does not work for a stack of numbers.
A flag or a wrapper
add has no answer to give. Its job is to change the stack, so its
return type could have been void. Instead it returns a boolean,
a flag: true if it replaced the top two numbers with their sum,
and false if the stack had fewer than two numbers. main checks
the flag on line 71. Java's own List.remove(Object) works the same
way: it returns false when the list did not hold the object.
top has an answer to give, the top number, and also needs a way to
say there is none. A flag alone cannot carry both: a method returns
one value, and Java gives it no other channel for an int.1 So
top returns an OptionalInt, a wrapper: a value that either
holds one int or is empty. Think of it as a list of length zero
or one. OptionalInt.of makes a wrapper that holds a number, and
OptionalInt.empty makes an empty wrapper. The client asks
isEmpty, as main does on line 64. If the wrapper is not empty,
getAsInt gives the number.
The flag and the wrapper both widen the return type to make room for
a "no." Integer widened int the same way, with null. The flag
and the wrapper differ in what Java does when the client forgets to
check, the subject of
When the client forgets.
An exception
divide does not return at all when it cannot do its job. Instead,
on line 198 or line 204, it throws an exception. An exception is
not a crash, though a crash is how one usually first appears: a
NullPointerException, and the stack trace a program prints when
nothing catches it. Throwing an exception ends the method at once.
Java then looks for code that has said it will catch that kind
of exception. Only if there is none does the program stop with a
stack trace.
main catches divide's exceptions with a try statement, on
lines 90 to 98:
90 91 92 93 94 95 96 97 98 | |
A try statement has two parts. The try block comes first, here
the one statement on line 91. Then come catch blocks, here two,
on lines 92 and 95, each naming a kind of exception. Java runs the
try block. If no line in it throws, the catch blocks are
skipped. If a line in it throws, Java leaves the block at that line
and runs the first catch block whose kind matches, with the
exception in that block's variable, e. e.getMessage() is the
message divide gave when it threw. Either way, the program goes
on after the statement instead of stopping. Think of a try as an
if whose condition is whether the block threw, with the catch
as its else. To a reader, a try also marks the code inside as
code that can fail.
divide can fail two ways. Each has its own exception, so the
client can tell them apart, though main treats them alike. The
three exception classes after CalcStack are each a few lines that
give a kind of exception a name. divide's specification lists its
two with @throws.
As Lecture 2 said,
the method promises to throw each exception in its case, so
@throws is part of the postcondition.
subtract throws the third class, StackException, and main
catches it the same way, on line 78. Two lines set subtract
apart. Line 168 says throws StackException after the method's
signature, and line 222 extends Exception where divide's two
classes extend RuntimeException. The section
A checked exception says what those two
lines buy.
Integer.parseInt, a library method, says no the same way. On
line 101, it throws a NumberFormatException when the token is not
a whole number that fits in an int, and main catches the
exception on line 103. That is where error: don't understand 'x'
came from.
When the client forgets
All six ways work when the client checks. But a programmer can
forget. So a second question tells the six apart: what happens when
the client forgets to check? Before reading on, decide what you
would want to happen. For each way, we took main's check out of a
copy of the calculator, leaving blank lines so that every other
line keeps its number. Two copies also store the answer in an
int. Here is what each copy did.
The flag. Without the check on lines 71 to 74, main ignores
add's answer:
$ java Calculator
> 1 +
> p
[1]
> q
Nothing happens. add left the stack unchanged, as its
specification says, but main ignored the flag, so nobody told the
user. To the user, + on one number acted as if the missing number
were 0.
The precondition. Without the if on lines 83 to 88, which
leaves only the call on line 87, main calls multiply on a stack
of one number:
$ java Calculator
> 1 *
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "CalcStack.pop()" is null
at CalcStack.multiply(Calculator.java:182)
at Calculator.main(Calculator.java:87)
multiply assumed its precondition, so it popped twice. The second
pop found the stack empty and returned null. Line 182 stores that
null in the int a. Java cannot turn null into an int, so
the program stopped with a NullPointerException. Java turns an
Integer into an int by calling its intValue method, the call
the message names. The message's second half does help: it says
that pop returned the null. Still, it is a confusing message,
about a null the client never saw, and it says nothing about two
numbers. The client broke the precondition, so multiply's
specification promised nothing about this call.
The special value. With Integer kept and only the check
dropped, . on an empty stack prints null to the user, and the
run goes on. In this copy, line 56 also stores pop's answer in an
int instead of an Integer, and lines 57 to 59 and 61 are blank,
leaving only the println on line 60. It compiles, and then:
$ java Calculator
> .
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "CalcStack.pop()" is null
at Calculator.main(Calculator.java:56)
The null met the same conversion, this time in main. Java
converted Integer to int silently, so nothing warned the
programmer before the run. An Integer in a return type does not
say "may be null" to most readers, either. Integer also appears
for another reason. A list cannot hold a primitive type, so a list
of numbers has to be a List<Integer>.
The exception. Without the try on lines 90 to 98, nothing
catches divide's exception:
$ java Calculator
> 1 /
Exception in thread "main" TooFewOperandsException: need two numbers to divide
at CalcStack.divide(Calculator.java:198)
at Calculator.main(Calculator.java:91)
The program stops, and the user sees a stack trace, which this
course's rule forbids. Still, the stack trace gives divide's own
message, and the line that threw the exception.
The wrapper. A client can forget a wrapper's check two ways. In
this copy, lines 64 to 66 and 68 are blank, so line 67 asks
getAsInt without asking isEmpty first. It compiles, and then:
$ java Calculator
> t
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.OptionalInt.getAsInt(OptionalInt.java:130)
at Calculator.main(Calculator.java:67)
getAsInt on an empty OptionalInt throws a
NoSuchElementException, and its message says what was missing. So
a wrapper does not stop a client from forgetting the check. It does
stop the mistake the special value's copy made on line 56. Storing
top's answer in an int on line 63, and printing it without the
check, does not compile:
$ javac Calculator.java
Calculator.java:63: error: incompatible types: OptionalInt cannot be converted to int
int top = stack.top();
^
1 error
Java does not convert an OptionalInt to an int by itself. The
client has to ask for the number with getAsInt, and asking is a
reminder that there may be none.
Of the six copies so far, five reached a run: the flag's silence,
the precondition's confusing crash, the special value's null, the
exception's stack trace, the wrapper's crash at the ask. The sixth,
the wrapper stored in an int, was caught by the compiler, and
only because an OptionalInt is not an int. What would you want
to happen when a client forgets? The best answer is to be told
before the program runs at all. Java can do that for an exception,
if the method's author asks for it.
A checked exception
StackException, on line 222, extends Exception. divide's two
exceptions, on lines 228 and 234, extend RuntimeException
instead. Java has two kinds of exceptions:
- An unchecked exception extends
RuntimeException. A method may throw one without saying so in its code, and its client may catch it or not. - A checked exception extends
Exceptionbut notRuntimeException. A method that may throw one has to say so in athrowsclause, assubtractdoes on line 168. The clause repeats the@throwstag on line 166 in code rather than in a comment, so the compiler reads it. Every client then has to catch the exception or declare it in its ownthrowsclause.
The names do not help: RuntimeException is itself a kind of
Exception. You may have met a checked exception already. Lecture
3's Sum caught a FileNotFoundException, which is checked.
Now the sixth copy. Without the try on lines 76 to 81, it does
not compile:
$ javac Calculator.java
Calculator.java:77: error: unreported exception StackException; must be caught or declared to be thrown
stack.subtract();
^
1 error
The compiler found the forgotten check before anyone ran the
program. main could instead declare throws StackException
itself, on line 38. In a copy that does, with the try still
blank, the compiler is satisfied: main has passed the exception
on, as its clause says. No method called main, so nothing
catches it, and - on too few numbers ends the run with a stack
trace:
$ java Calculator
> 1 -
Exception in thread "main" StackException: need two numbers to subtract
at CalcStack.subtract(Calculator.java:170)
at Calculator.main(Calculator.java:77)
Either way, the compiler held main to the check: catch the
exception, or say that main may throw it.
When the client forgets the check:
- a flag, nothing happens, silently;
- a precondition, anything may happen, here a confusing
NullPointerExceptionabout anullthe client never saw; - a special value, the
nullis printed to the user, or meets a conversion tointand stops the program; - an exception, the program stops, with a stack trace that gives the method's own message;
- a wrapper, asking without checking stops the program at the
ask, and using the wrapper as an
intdoes not compile; - a checked exception, the compiler refuses the program.
Lecture 4 named three ways
to know that code works: testing, tools, and reasoning. A tool can
confirm a claim about every run, but only the one claim it was
built to check. The Java compiler is such a tool. A checked
exception gives the compiler one more claim to check: every caller
of subtract either catches StackException or declares it in its
own throws clause. The author of subtract chose a checked
exception, so the compiler holds every client to the check, however
careless. A mistake the compiler catches never reaches a run, a
user, or a test. So when you can, let the compiler find the
mistake.
Choosing a way
None of the six is best everywhere. The designer of a method has to decide which problems are serious, and which a client can work around. Three questions help:
- Can the client check first, cheaply? Then a precondition is
simplest, as for
multiply. A check before the call also puts the condition in front of whoever readsmain. With a flag or an exception, that reader has to look up the specification. When checking would cost as much as the work, as forparseInt, the method should check and say what it found. - How convenient is the answer to use? A flag or a special value
is checked with a plain
if. An exception needs atry. A checked exception makes every client catch it or declare it, even a client that knows the failure cannot happen. That is the tension in a checked exception: every client pays, with atryor a clause, and in return the compiler checks every client before any run. - What happens when a client forgets? Silence, a confusing crash, a stack trace with the method's own message, or a compile-time error. Of these, only the compile-time error comes before any run.
Bloch gives a rule of thumb for choosing between the two kinds of
exception.2 A problem a careful client should expect and
recover from, such as a file that is not there, suits a checked
exception. A problem that only a programmer's mistake can cause
suits an unchecked exception. The other two ways sort beside them:
a wrapper beside the checked exception, since both make the client
ask, and a precondition beside the unchecked exception, since both
leave a careless client to a crash.
The choice also sets the specification's strength, in
Lecture 2's sense.
A method that promises an exception on a bad call has a stronger
specification than a method that requires a good call.
As Lecture 2 said,
a library method's specification can only grow stronger. So once a
library publishes a @throws clause, it cannot replace the clause
with a precondition. A later lecture returns to exceptions in depth.
There is a seventh way, a bad one: say nothing, and make up an
answer. add could treat a missing number as 0, and that is how
the forgotten flag looked to the user. The program then keeps
running, and the made-up answer travels on into later results. The
mistake may show much later, far from its cause, or never. Three
values are common made-up answers:
undefined. JavaScript often takes the seventh way. Reading an array past its end there givesundefined, instead of an error, andundefined + 1givesNaN.NaNandInfinity. In Java's rules for doubles, these are special values:0.0 / 0.0givesNaN, short for "not a number," and1.0 / 0.0givesInfinity. Neither throws, and a client that never checks for them gets the seventh way:NaN + 1isNaNagain. Lecture 3's notes ran a copy ofSumwithout its count check: it printedNaN, and reported success.null. A method whose specification does not allownull, but which returnsnullanyway, has made up an answer. Thenullis passed along until a line converts it to anintor calls a method on it, and the program stops there, far from where thenullcame from.
Lecture 3's notes quote Kernighan and Pike: "the one guaranteed wrong answer is to ignore the problem." The calculator itself ignores one problem, Lecture 1's overflow:
$ java Calculator
> 2147483647 1 + p
[-2147483648]
> q
The sum is past the largest int, so the addition on line 157 wraps
around to the most negative int. This is a bug, not a way of
saying no. add's specification promises their sum, and on this
run the stack holds something else. Nobody decided what add
should do when the sum does not fit, so Java's arithmetic decided:
the seventh way, by omission. The fix is a decision. add can say
no in one of the six ways, which exercise 2 asks you to choose. Or
its specification can promise less, and say that the sum wraps
around: add is then correct, and a client who wanted the sum
is left without it. Many programmers leave overflow undecided, as
this course leaves running out of memory undecided. The difference
is that a user reaches overflow from the keyboard, in one line.
Interactive programs are harder
Back to the calculator as a whole. Most of the programs you use every day are interactive: an editor, a browser, an app on your phone. Three things make an interactive program harder to get right than a batch program:
- State that lasts. The stack outlives each line the user types. What a line does can depend on every line before it, so a bug may show only after one particular sequence of inputs. Think of a program that crashes on the eleventh click, but only after a certain file was opened.
- A user who chooses the order. A batch program reads its input in one fixed order. An interactive program must respond sensibly to any input at any moment, and keep running afterward.
- State the user cannot see. The calculator shows its stack only
when asked, with
p. A user who forgets what is on it, or a tester who wants to reproduce a bug, has to reconstruct the stack.
The same separation of concerns makes the calculator's state easy
to test on its own. main keeps the conversation with the user:
the prompt, the tokens, the messages. CalcStack keeps the state
and the arithmetic. Lecture 5 said that a method that does one job
is easier to test. Its section
Testable by design
says why the keyboard matters: a method that reads it is hard to
test, and a method that takes values and returns one is easy. A
test of add needs no keyboard: build a CalcStack, push two
numbers, call add, and check the flag and the stack. Here is that
test, and a test of divide, written as
Lecture 4 wrote its tests:
@Test
void addReplacesTwoNumbersWithTheirSum() {
CalcStack stack = new CalcStack();
stack.push(2);
stack.push(3);
assertTrue(stack.add());
assertEquals("[5]", stack.toString());
}
@Test
void divideOnOneNumberThrows() {
CalcStack stack = new CalcStack();
stack.push(1);
assertThrows(TooFewOperandsException.class,
() -> stack.divide());
assertEquals("[1]", stack.toString());
}
The second test checks divide's @throws clause, which
assertEquals cannot check: a call that throws returns nothing to
compare. assertThrows takes the class of the exception expected,
then the code to run, written as () -> stack.divide(). That form
is a lambda: a piece of code handed over to be run later, here
by assertThrows itself. The test passes if the code throws an
exception of the class named, and fails if it throws nothing, or
something else. The last line checks that the failed divide left
the stack alone, as its specification says. Both tests pass. Blank
divide's check on lines 197 to 200, and the second test fails:
the code threw a NullPointerException, not a
TooFewOperandsException. Later this quarter, the lectures on web
apps return to interactive programs.
Exercises
Each answer is folded under its question. Write yours first.
1. Change pop to report an empty stack with an unchecked
exception, as divide does, instead of with null. Give its new
specification and code, and the new code for the . command in
main.
Answer
-
The method. The return type becomes
int, sincenullno longer has a job. The@returnclause loses its second half, and a@throwsclause takes it over:/** * @modifies this * @effects removes the top number from the stack, if the stack is * non-empty * @return the number removed * @throws StackEmptyException if the stack was empty */ public int pop() { if (items.size() == 0) { throw new StackEmptyException("stack is empty"); } return items.remove(items.size() - 1); }StackEmptyExceptionis a new class that extendsRuntimeException, in a few lines like those ofdivide's two exceptions. -
The client.
maincatches it:} else if (token.equals(".")) { try { System.out.println(stack.pop()); } catch (StackEmptyException e) { System.out.println( "error: " + e.getMessage()); } -
The other clients.
add,subtract,multiply, anddividecallpoptoo. Each pops only after checkingitems.size(), or, formultiply, under its precondition, so none needs atry. A forgottentryinmainnow stops the program with a stack trace whose message isstack is empty, instead of aNullPointerException.
2. The run on 2147483647 1 + p shows that add wraps around
past the largest int. Java's Math.addExact(a, b) returns a + b,
or throws an unchecked ArithmeticException when the sum does not
fit in an int. Which of the six ways should add use to report an
overflow, given that it already reports too few numbers with a flag?
Answer
- The trouble with the flag. A
booleanhas room for one "no," andaddalready uses it for too few numbers. A second failure needs a second answer, so the flag runs out of room. -
An exception can report the overflow. Let
ArithmeticExceptionpass tomain, and keep the flag for too few numbers:public boolean add() { if (items.size() < 2) { return false; } int b = pop(); int a = pop(); push(Math.addExact(a, b)); return true; }mainthen catchesArithmeticExceptionaround the call. The specification gains a clause,@throws ArithmeticException if the sum does not fit in an int, and has to say what is left on the stack. As written, the two numbers are already popped whenaddExactthrows, so they are lost.dividekeeps its stack unchanged by pushing both numbers back on line 203 before it throws.addcould do the same with atryinsideadd: catch theArithmeticException, pushaandbback, and throw it again. - One way for both would serve the client better. Two ways of saying no in one method are a sign to pick one, here an exception for both. That is one reason a real class picks one way and keeps to it.
3. The calculator prints its error messages on standard output.
Lecture 3's Sum
printed its messages on standard error. Which is right for the
calculator?
Answer
- For a person at the terminal, it makes no difference. Both streams show up on the screen.
- For a script, it does. A script could feed the calculator its
input through a pipe and read the answers from its output. With
the messages on standard output, the script finds
error: need two numbers to addwhere it expected a number. That is the clutter the Unix maxim in Lecture 3 warns against. With the messages on standard error, the answers stay clean. - A choice the manual should state. Either way, the class
comment says nothing about errors.
As Lecture 3 asked of
Sum, the calculator's manual should say what each mistake prints, and where.
Further reading
- The six ways, and more. Barbara Liskov and John Guttag, Program Development in Java (2001), chapter 4, "Exceptions." It sets out how a specification states the exceptions a procedure throws, and, in section 4.4, when to choose checked or unchecked ones.
- Rules from practice. Joshua Bloch, Effective Java, second edition (2008), chapter 9. Item 57: use exceptions only for exceptional conditions. Item 58: use checked exceptions for recoverable conditions and runtime exceptions for programming errors. Item 59: avoid unnecessary use of checked exceptions. Item 65: don't ignore exceptions.
- The phrase. Edsger Dijkstra coined "separation of concerns" in On the role of scientific thought (1974), a short essay: study one aspect of a program at a time, knowing the others are there and setting them aside for the moment.
- The cost of
null. Tony Hoare added the null reference to the language ALGOL W in 1965. In a 2009 talk at QCon London, he called it his "billion-dollar mistake," for the errors, security holes, and crashes it has led to since.OptionalIntand its relatives are one answer to that mistake.
-
C gives it one. A C function often returns a flag and writes its real answers through pointers the client passed in, "by reference".
scanfworks that way: it returns how many of its arguments it filled. The client has to check that count before using what it passed, since a failed call leaves the variable holding whatever it held before. A check forgotten there reads a value that was never written, a classic source of bugs and security holes. ↩ -
Joshua Bloch gives this rule for exceptions in Effective Java, second edition (2008), item 58. ↩