I believe that, if done right, checked exceptions can benefit an API. However, they weren’t given the proper TLC in the new Java 8 APIs, particularly in the Streams. But if Java was enhanced with default generics, the mess could be cleaned up.
The problem
Java 8 introduced a plethora of functional interfaces, but none of the methods throw exceptions. That’s fine if you’re only throwing extensions of RuntimeExcption
, but it becomes extremely painful when dealing with checked exceptions. For example, consider the following:
public class Example {
public void example(Stream<MyType> stream) {
stream
.filter(myType::isAvailable)
.map(myType::process)
.forEach(this::writeToFile);
}
private void writeToFile(String value) throws IOException {
// implementation details...
}
// Assume both exceptions are checked exceptions.
public interface MyType {
boolean isAvailable() throws AvailabilityException;
Object process() throws ProcessException;
}
}
This will fail to compile, miserably. This is what has to be done to fix it:
public void example(Stream<MyType> stream) {
stream
.filter(myType -> {
try {
return myType.isAvailable();
} catch (AvailabilityException e) {
// TODO What can we do here?
}
})
.map(myType -> {
try {
return myType.process();
} catch (ProcessException e) {
// TODO What can we do here?
}
})
.forEach(myType -> {
try {
Example.this.writeToFile(myType);
} catch (IOException e) {
// TODO No, seriously, what can we do here?
}
});
}
Terrible, isn’t it? What do we do inside all of the catch blocks? Each is probably too low level to properly handle the exception, so the best we can do—assuming we cannot rewrite the checked exceptions into runtime exceptions—is wrap them in a RuntimeException
, rethrow it, and put a try
/catch
around the stream
call chain:
public void example(Stream<MyType> stream) {
try {
stream
.filter(myType -> {
try {
return myType.isAvailable();
} catch (AvailabilityException e) {
throw new RuntimeException(e);
}
})
.map(myType -> {
try {
return myType.process();
} catch (ProcessException e) {
throw new RuntimeException(e);
}
})
.forEach(myType -> {
try {
Example.this.writeToFile(myType);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (RuntimeException e) {
Exception cause = e.getCause();
if (e instanceof AvailabilityException) {
AvailabilityException real
= (AvailabilityException) cause;
// properly handle the exception
} else if (e instanceof ProcessException) {
ProcessException real
= (ProcessException) cause;
// properly handle the exception
} else if (e instanceof IOException) {
IOException real
= (IOException) cause;
// properly handle the exception
} else {
throw e;
}
}
}
This looks even worse; there must be a better way!
Default Generic Type
What if Function
where changed to this:
@FunctionalInterface
public interface Function<T, R, E extends Exception> {
R apply(T t) throws E;
// ...helper methods omitted...
}
First, it would break backwards compatibility (the “omitted helper methods” would be trickier to implement), and all existing implementations would have to be rewritten so that E
was defined as RuntimeException
.
But this might be avoided if default generics were added to the language:
@FunctionalInterface
public interface Function<T, R,
E extends Exception default RuntimeException> {
R apply(T t) throws E;
// ...helper method omitted...
}
Likewise, many of the Java 8 APIs could be rewritten to account for this. For example, the Stream#map
function:
public interface Stream<T> extends BaseStream<T, Stream<T>> {
// ...omitted...
<R, E extends Exception default RuntimeException>
Stream<R> map(
Function<? super T, ? extends R, E> mapper) throws E;
// ...omitted...
}
Like Java 8’s default methods, default generics could provide a means of API evolution.
Putting it Together
How does this help? It allows checked exceptions to pop out of the Stream
methods like map
, filter
, reduce
in a much more elegant way. Our previous example might look something like this:
public void example(Stream<MyType> stream) throws IOException {
try {
stream
.filter(myType::isAvailable)
.map(myType::process)
.forEach(this::writeToFile);
} catch (AvailabilityException e) {
// properly handle the exception
} catch (ProcessException e) {
// properly handle the exception
}
}
And isn’t this what is should look like?
The Fine Print
- AFAIK, default generics is in no way being considered for inclusion into Java. Call me a dreamer.
- Adding default generics to Java would be a rather large change to the language. While it solves the irritating case above, there are undoubtedly unforeseen consequences I have not considered.
- There can be more than one exception in a
throws
list, but generics are limited to one. This makes generic exceptions tricky, as discussed more here.
- There can be more than one exception in a
- I’m not qualified to answer how this would affect backwards compatibility.