2014-10-29
Use Cases for ValEx
The last blog entry introduced the idea
of ValEx
as a variation of Java's Optional
. It can be initalized not only
with a value, but alternatively with an exception which explains
why no value is available. The central method of this class is the
getter for the contained value:
public class ValEx<T,E extends RuntimeException> { ... public T get() throws E { if (t==null) throw e; return t; } }
It allows the caller to decide how sure (s)he is that a value
is available. If, from our code structure, we are sure that there
is a value, we can just call get()
without previously
checking with isPresent()
. If we are wrong, this is a
bug and an unchecked exception is thrown. How does this feel in
practical use. Lets look at some typical exceptions in Java.
Exceptions When Parsing Strings
There are many cases where strings are parsed or converted into
objects. This includes parsing dates and regular expressions, or
even very simple things like getting Charset
or an
encoding. As already shown in the last blog
entry, converting static strings should not throw a checked
exception. Who has not yet written code like
try { ... Writer w = new OutputStreamWriter(out, "UTF-8"); ... } catch (UnsupportedEncodingException e) { log.error("how can UTF-8 be missing", e); ... }
and wondered why the wrong character set results in a checked exception. The solution here is actually to use a slightly different call:
Writer w = new OutputStreamWriter(out, Charset.forName("UTF-8"));
where forName()
throws an unchecked exception that
will only be thrown if things are getting
weird. Does VarEx
have an application here. Well, not
as long as 100% of the cases involve static strings. But suppose
the character set name is read from a property.
String csName = System.getProperty("application.charset.name"); Writer w = new OutputStreamWriter(out, Charset.forName(csName));
This code is now missing a try/catch
, because it
is not improbably that a system property contains a wrong
string. In this particular case we can switch back to not using
forName
, but with a hypothetical factory method returning a
VarEx
we can have both easily. With a static string
Writer w = Files.streamWriter(out, "UTF-8").get();
we get an exception from the get()
in case we have
a typo in the code. And with a character set name from a
property
String csName = System.getProperty("application.charset.name"); VarEx<Writer,?> vw = Files.streamWriter(out, csName); if (vw.isEmpty()) { // handle the problem, possibly re-throwing throw w.getException(); } Writer w = vw.get();
we can first check whether we got something back. So the
VarEx
allows us to have both, the convenience of an
unchecked exception with the minor price to pay being the
unshielded get()
call, as well as the awareness about
a potentially unsuccessfull operation that can be checked for with
isEmpty()
.
IOException
An IOException
is probably one of the most
frequent exceptions to deal with. Let's see whether
VarEx
can help here too. With a hypothetical factory
method again, assume we had
String readFile(String name) throws IOException { VarEx<Reader,IOException> vr = Files.newReader(name); if (vr.isEmpty()) { throw vr.getException(); } Reader r = vr.get(); ... }
Is this any better than the try/catch
version?
Probably not if used this way. What still bugs me is that an
exception is prepared in newReader()
and
eventually thrown despite the fact that absolutely nothing
exceptional is going on. Opening a file and finding that this
cannot be done is completely normal business. Even if we just
wrote the file, some other process or thread could have
intercepted already and messed with the file in all kinds of ways
that prevent us from opening it — normal, not
exceptional!
In this case it may be worth considering this implementation.
VarEx<String,String> readFile(String name) { VarEx<Reader,String> vr = Files.newReader(name); if (vr.isEmpty()) { return VarEx.empty(vr.getCause()); } Reader r = vr.get(); ... }
Here I start to change my mind with regard to how exactly
VarEx
should be implemented. In the previous blog I
proposed to always use an exception as the description of what
went wrong. But this forces the provider of the VarEx
to create an exception with its full stack trace, even for normal
business. Therefore I now rather would implement the VarEx with a
completely unrestrained second generic argument.
public class VarEx<T,Cause> { ... public static <T,Cause> ValEx<T,Cause> of(T value) { return new ValEx<>(value, null); } public static <T,Cause> ValEx<T,Cause> empty(Cause cause) { return new ValEx<>(null, cause); }
The get()
method becomes a bit more involved, but
would basically throw an IllegalStateException
with
the Cause
either as the message or as the cause of
the exception.
Wrapup
It looks like VarEx
allows to combine the benefits
of checked exceptions — visibility in the api
— with the convenience of unchecked exceptions thrown
only when the cause is a programming problem. By allowing
VarEx
to store a cause also just as a message, with
no exception, the expensive exception generation can be prevented
where a failure to provide a value is normal and expected. Still,
if it is necessary to log the case, the
VarEx
improves over Optional
in that in
can provide the cause why no value is available.