Generic Throws: Another Little Java Idiom
A seemingly little known fact about Java generics is that you
can write generic throws declarations by declaring a type parameter
that extends Exception
. For example, the following
interface defines a generic finder that looks up a value of type
T, returns null if it is not found,or may report that
the lookup failed completely by throwing an exception of type
X:
public interface Finder<T, X extends Exception> { @Nullable T find(String criteria) throws X; }
Different implementations can fail in different ways. A finder that performed an HTTP query can fail with an IOException. A finder that queries a database can fail with a SQLException. And so on.
A Cunning Idiom for Classes That Cannot Fail
But what about queries that cannot fail? You might want to
implement the query in-memory, with a HashMap or something. You
don't want to declare that find throws a checked exception.
Therefore bind X to RuntimeException
. The
compiler will ignore the throws clause and you don't even need
to include it in implementing classes:
public class InMemoryFinder<T> extend Finder<T, RuntimeException> { private final Map<String,T> entries; public InMemoryFinder(Map<String,T> entries) { this.entries = entries; } @Nullable T find(String criteria) { // No need for a throws clause return entries.get(criteria); } }
Generic throws can remove a lot of boilerplate code to pass checked exceptions through interfaces by wrapping them in more abstract checked exceptions. Hopefully one day Java will provide anchored exceptions so we can avoid all this generics jiggery-pokery.
Generic Throws and Polymorphism
Wrapped exceptions are still necessary when an interface must be
used polymorphically. A type that declares generic throws is
generic but not polymorphic: an interface
T<IOException>
cannot be used wherever a
T<SQLException>
is acceptable.
If necessary, an interface with generic throws can be converted by an Adaptor into a version that throws wrapped exceptions and can be used polymorphically.
public class PolymorphicFinder<T,X> extend Finder<T, FinderException> { private final Finder<T,X> implementation; public PolymorphicFinder(Finder<T,X> implementation) { this.implementation = implementation; } @Nullable T find(String criteria) throws FinderException { try { return implementation.find(criteria); } catch (X x) { throw new FinderException("query failed", x); } } }
Wishful Thinking
Imagine if the Iterable
and Iterator
interfaces were parameterised by exception type:
public interface Iterator<T, X extends Exception> { boolean hasNext() throws X; T next() throws X; } public interface Iterable<T, X extends Exception> { Iterator<T,X> iterator(); }
Collections could implement
Iterable<T,RuntimeException>
and appear no
different from the way they are now. However, I/O streams, SQL
result sets and other streams of data read from the program's
environment could be represented as Iterable objects and the
for-each loop could be used to iterate over their contents.
For example, BufferedReader could implement
Iterable<String,IOException>
, which would let
you write:
try { BufferedReader reader = new BufferedReader(...); for (String line : reader) { ... do something with the line } } catch (IOException e) { ... iteration failed }
Unfortunately, it's probably too late to make this change because it would break backward compatibility.