One of the peculiar features of Java, in what’s otherwise a rather mundane language, is checked exceptions. What do checked exceptions do? The short explanation is: They make information about what exceptions a method may throw part of its signature, and force you to actually handle all exceptions that may arise.
A better way to explain it is through a few small examples…
Checked exceptions
Let’s try to implement a trivial method that may throw an exception:
class QuickMaths {
class ZeroDivisionException extends Exception {
}
// Divide x by y, making sure y isn't zero.
Double divide(Number x, Number y) {
if (y == 0) {
throw new ZeroDivisionException();
} else {
return x * 1.0 / y;
}
}
}
The method divide
is going to cause a compiler error, because it may throw an exception but we haven’t encoded this information in its signature. (Sure, the compiler could infer this, but it doesn’t do so for the return value either, and it doesn’t make much sense anyway for a reason we will see later.)
So here’s the corrected version:
class QuickMaths {
class ZeroDivisionException extends Exception {
}
// Divide x by y, making sure y isn't zero.
Double divide(Number x, Number y) throws ZeroDivisionException {
if (y == 0) {
throw new ZeroDivisionException();
} else {
return x * 1.0 / y;
}
}
}
Cool, we’ve encoded the “may throw exception of type X” information in the method signature. Now let’s try to use the method:
class QuickMaths {
// ...
// Divide x by y, then the result by y again.
Double divideTwice(Number x, Number y) {
return divide(divide(x, y), y);
}
}
Once again, we will be faced with a compiler error. We’re calling a method which may throw an exception, but we’re not handling it. There’s two ways to solve this. The first is, of course, to catch the exception:
class QuickMaths {
// ...
// Divide x by y, then the result by y again.
Double divideTwice(Number x, Number y) {
try {
return divide(divide(x, y), y);
} catch (ZeroDivisionException e) {
return Double.NaN;
}
}
}
But the whole point of exceptions is that they have the ability to travel upwards through the stack of function calls in the program, to be handled at some higher level. (Fancy people call that a non-local exit.) To allow this to happen, the calling method must likewise declare in its signature that it may throw an exception of that same type:
class QuickMaths {
// ...
// Divide x by y, then the result by y again.
Double divideTwice(Number x, Number y) throws ZeroDivisionException {
return divide(divide(x, y), y);
}
}
This effectively forces the programmer to always ask the question, “should I handle this exception here, or can it travel further up?” And at some point, eventually, it will have to be handled, because the signature of the main
method is not allowed to declare any exceptions.
(This is the point where it should become apparent that it wouldn’t make much sense for the compiler to try to infer thrown exception types. You have to be explicit about it for pretty much all your methods anyway, and it makes little sense to special-case those who contain a throw
statement directly in their body.)
Actually, there’s a special subtype of Exception which is called RuntimeException, and it and all of its subclasses are exempt from these compiler checks (they’re unchecked exceptions), so it’s also possible to wrap a checked exception into an unchecked one and forget about it (getting the same behavior you would in any other programming language) but in this blog post we’re not interested in that possibility.
Moving on.
Type parameters and higher-order functions
I’m not going to explain how generics and type parameters work in Java, because that would take a lot more time than the explanation of checked exceptions above. Likewise for higher-order functions, which you must already be familiar with to understand the following.
Here’s some examples showing how higher-order functions may interact with type parameters. Let’s assume we’re implementing the map
function, that is part of the bread and butter of any program written in a functional style:
class Fn {
// Functional interface to map any input into its corresponding output.
interface Mapper {
Object apply(Object input);
}
// Turn a list of inputs into corresponding outputs via a Mapper.
static List<Object> map(List<Object> inputs, Mapper mapper) {
List<Object> outputs = new ArrayList<>(inputs.size());
for (Object input : inputs) {
outputs.add(mapper.apply(input));
}
return outputs;
}
}
That’s all fine and dandy but what’s with all that use of Object? This code is sorely in need of some genericization! Sprinkle some type parameters in there:
class Fn {
// Functional interface to map any input into its corresponding output.
interface Mapper<I, O> {
O apply(I input);
}
// Turn a list of inputs into corresponding outputs via a Mapper.
static
<I, O>
List<O> map(List<I> inputs, Mapper<I, O> mapper) {
List<O> outputs = new ArrayList<>(inputs.size());
for (I input : inputs) {
outputs.add(mapper.apply(input));
}
return outputs;
}
}
Now that’s cool. We can map a list of any type into a list of any other (or the same) type, using a Mapper that does the corresponding value conversion. Let’s try to use it, to divide a whole list of numbers by a constant factor in one fell swoop:
class QuickMaths {
// ...
List<Double> divideAll(List<Number> xs, Number y) {
return Fn.map(xs, x -> divide(x, y));
}
}
Obviously, the compiler won’t like that, because divide
may throw an exception which we aren’t handling. Well easy peasy, just declare that it may be propagated further upwards in the stack, just like we did with divideTwice
:
class QuickMaths {
// ...
List<Double> divideAll(List<Number> xs, Number y)
throws ZeroDivisionException
{
return Fn.map(xs, x -> divide(x, y));
}
}
Uh oh, we don’t have one but two compiler errors now, what the heck?
Well, here’s the problem: the Mapper interface’s apply method, which is what our lambda expression "x -> divide(x, y)"
here implements, is not declared to throw any exception, so first of all the compiler doesn’t like that.
Secondly, the declaration that divideAll
throws an exception is technically wrong, since the only method the compiler knows it to be calling here is Fn.map
which is declared not to throw anything. (The compiler can’t know what the implementation of Fn.map
will do with the lambda object it’s passed.)
How do we solve this issue?
Parameterized exception types
Comes out that type parameters in Java can not only be used for the parameters and return value for a method, but also the exception types that are part of its signature! Watch this:
class Fn {
// Functional interface to map any input into its corresponding output.
interface Mapper<I, O, C extends Exception> {
O apply(I input) throws C;
}
// Turn a list of inputs into corresponding outputs via a Mapper.
static
<I, O, C extends Exception>
List<O> map(List<I> inputs, Mapper<I, O, C> mapper) throws C {
List<O> outputs = new ArrayList<>(inputs.size());
for (I input : inputs) {
outputs.add(mapper.apply(input));
}
return outputs;
}
}
Neato! Now our Mapper implementations can throw any Exception, and the call to map
will be considered to throw the same type of exception as the Mapper.
(For those curious, I use “C” for the type parameter as an abbreviation for “Condition,” which is what some fancy languages call an exception. I avoid “E” since it’s commonly used for “Element” in collection types.)
The following now works seamlessly:
class QuickMaths {
// ...
List<Double> divideAll(List<Number> xs, Number y)
throws ZeroDivisionException
{
return Fn.map(xs, x -> divide(x, y));
}
}
Problem solved, and everyone lived happily ever after. Isn’t that a nice ending!
~~~ fin ~~~