H
Harold Yarmouth
One of the glaring limitations of Java seems to be in connection with
callback objects.
To illustrate it, consider trying to make a generic cache class, backed
by a map.
public class Cache<K,V> {
protected Map<K,V> map;
private Getter getter;
public interface Getter<KK,VV> {
VV get (KK key);
}
Cache (Getter getter) {
if (getter == null) {
throw new IllegalArgumentException();
}
this.getter = getter;
}
public VV get (KK key) {
VV result = map.get(key);
if (result != null) return result;
result = getter.get(key);
if (result != null) map.put(key, result);
return result;
}
}
Simple enough, right? It returns a cached value but is provided with an
object capable of obtaining a new one.
The problem occurs when someone wants to use a getter that, say, reads
items from disk. The compiler won't let their getter throw IOException.
The class above could be changed to add throws clauses on both get
methods, but what should they throw? It really depends on the
implementation.
Declaring them as just "throws Exception" is ugly.
It is possible to create a generic version: Cache<K,V,E extends
Exception> with both gets declared as "throws E". This runs into more
problems:
First, if more than one unrelated exception type might be thrown by a
getter, E will have to just be Exception, and we're back to square one.
Second, there's no "proper" way of saying that this particular cache's
getter doesn't throw anything (checked) at all. (There's a bit of a
kludge: make E RuntimeException or a subclass. Works, but is ugly.)
Perhaps Java needs "generic exception lists"? Where a type parameter
that is only used in throws clauses, is bounded below by Throwable, and
is the last in the parameter list can be replaced with a whole list of
exception types (so long as they all fit the bound).
This would allow the Cache<K,V,E extends Exception> to be instantiated
as Cache<String, BigExpensiveThingy, IOException, SAXException> for
example, and the get method would be understood by the compiler to be
capable of throwing IOException, SAXException, and errors and unchecked
exceptions, but not, say, AWTException. Likewise its getter's get method
could be implemented to read and parse XML files from disk and just let
escape the IOExceptions and SAXExceptions thus generated from time to time.
Currently, the best that can be done in this case is declare the Cache
as Cache<String, BigExpensiveThingy, Exception>, and if you wrap a call
to its get method in catch(SAXException e) do this, catch(IOException e)
do that, or in a method that can throw both, the compiler will complain
because it thinks there's a chance that the checked exception
AWTException (among others) could come bubbling up from inside of the
call to get and go unhandled. So you need to declare "throws Exception"
wherever you use the cache (thus moving the problem one level up the
hierarchy of method calls) or, worse, "catch (Exception e)" which is
generally bad practise.
Mitigatable:
catch (Exception e) {
throw (RuntimeException)e;
}
Ugly, but stops it from eating RuntimeExceptions and shouldn't throw
ClassCastException if you really have handled every checked exception
your getter can throw. (Unless, of course, it REthrows
ClassCastException.) You still lose the compiler's double-checking that
you've actually handled properly every exception type that really can be
thrown; this is as bad as having thought-to-be-correct generics code
that produces an unchecked conversion warning, though no worse.
Cache could be declared <K,V,E1 extends Exception, E2 extends Exception>
or one could even have E3 and above. Then things get even uglier for the
folks whose getters aren't throwing much of anything:
MyDiskFileGetterThatDoesNotUseXML implements Cache.Getter<String,
BigExpensiveObject, IOException, RuntimeException> (or 2x IOException);
MyExpensiveComputation implements Cache.Getter<Integer,
BigExpensiveMathematicalThingamabob, RuntimeException,
RuntimeException>; and so forth.
So our present-day alternatives, in descending order from bad to worse
to positively abysmal:
Clutter up the code with extra exception patameters that are often
redundant;
Clutter up the code with catches that cast and rethrow, and lose some
amount of exception type-safety;
Abuse RuntimeExceptions (or worse, Errors) to wrap checked exceptions
and smuggle them out of the getter (and lose even more exception
type-safety); and
Throw nothing, and indicate errors by returning out-of-band values
(which is not only horrible practise, but will clutter up the cache);
Modify Cache.Getter to return some sort of Pair<KK,Boolean> so it can
simultaneously return a value and tell the cache whether to actually put
it in the map (which leads to code that's just freaking awful -- caches
with key type Object that sometimes return IOExceptions -- not throw,
return -- and so forth -- so much for type safety).
Object result = myCache.get(key);
if (result instanceof IOException || result instanceof SAXException)
throw result;
BigExpensiveThingy bet = (BigExpensiveThingy)result;
// I want to throw up!
java.lang.ClassCastException
at line 33 of AwfulCode.java
// I give up.
throw new Upchuck();
We could really use "exception type parameter varargs". Really, we
could. Checked exceptions just don't play nice with "inversion of
control" like patterns otherwise.
callback objects.
To illustrate it, consider trying to make a generic cache class, backed
by a map.
public class Cache<K,V> {
protected Map<K,V> map;
private Getter getter;
public interface Getter<KK,VV> {
VV get (KK key);
}
Cache (Getter getter) {
if (getter == null) {
throw new IllegalArgumentException();
}
this.getter = getter;
}
public VV get (KK key) {
VV result = map.get(key);
if (result != null) return result;
result = getter.get(key);
if (result != null) map.put(key, result);
return result;
}
}
Simple enough, right? It returns a cached value but is provided with an
object capable of obtaining a new one.
The problem occurs when someone wants to use a getter that, say, reads
items from disk. The compiler won't let their getter throw IOException.
The class above could be changed to add throws clauses on both get
methods, but what should they throw? It really depends on the
implementation.
Declaring them as just "throws Exception" is ugly.
It is possible to create a generic version: Cache<K,V,E extends
Exception> with both gets declared as "throws E". This runs into more
problems:
First, if more than one unrelated exception type might be thrown by a
getter, E will have to just be Exception, and we're back to square one.
Second, there's no "proper" way of saying that this particular cache's
getter doesn't throw anything (checked) at all. (There's a bit of a
kludge: make E RuntimeException or a subclass. Works, but is ugly.)
Perhaps Java needs "generic exception lists"? Where a type parameter
that is only used in throws clauses, is bounded below by Throwable, and
is the last in the parameter list can be replaced with a whole list of
exception types (so long as they all fit the bound).
This would allow the Cache<K,V,E extends Exception> to be instantiated
as Cache<String, BigExpensiveThingy, IOException, SAXException> for
example, and the get method would be understood by the compiler to be
capable of throwing IOException, SAXException, and errors and unchecked
exceptions, but not, say, AWTException. Likewise its getter's get method
could be implemented to read and parse XML files from disk and just let
escape the IOExceptions and SAXExceptions thus generated from time to time.
Currently, the best that can be done in this case is declare the Cache
as Cache<String, BigExpensiveThingy, Exception>, and if you wrap a call
to its get method in catch(SAXException e) do this, catch(IOException e)
do that, or in a method that can throw both, the compiler will complain
because it thinks there's a chance that the checked exception
AWTException (among others) could come bubbling up from inside of the
call to get and go unhandled. So you need to declare "throws Exception"
wherever you use the cache (thus moving the problem one level up the
hierarchy of method calls) or, worse, "catch (Exception e)" which is
generally bad practise.
Mitigatable:
catch (Exception e) {
throw (RuntimeException)e;
}
Ugly, but stops it from eating RuntimeExceptions and shouldn't throw
ClassCastException if you really have handled every checked exception
your getter can throw. (Unless, of course, it REthrows
ClassCastException.) You still lose the compiler's double-checking that
you've actually handled properly every exception type that really can be
thrown; this is as bad as having thought-to-be-correct generics code
that produces an unchecked conversion warning, though no worse.
Cache could be declared <K,V,E1 extends Exception, E2 extends Exception>
or one could even have E3 and above. Then things get even uglier for the
folks whose getters aren't throwing much of anything:
MyDiskFileGetterThatDoesNotUseXML implements Cache.Getter<String,
BigExpensiveObject, IOException, RuntimeException> (or 2x IOException);
MyExpensiveComputation implements Cache.Getter<Integer,
BigExpensiveMathematicalThingamabob, RuntimeException,
RuntimeException>; and so forth.
So our present-day alternatives, in descending order from bad to worse
to positively abysmal:
Clutter up the code with extra exception patameters that are often
redundant;
Clutter up the code with catches that cast and rethrow, and lose some
amount of exception type-safety;
Abuse RuntimeExceptions (or worse, Errors) to wrap checked exceptions
and smuggle them out of the getter (and lose even more exception
type-safety); and
Throw nothing, and indicate errors by returning out-of-band values
(which is not only horrible practise, but will clutter up the cache);
Modify Cache.Getter to return some sort of Pair<KK,Boolean> so it can
simultaneously return a value and tell the cache whether to actually put
it in the map (which leads to code that's just freaking awful -- caches
with key type Object that sometimes return IOExceptions -- not throw,
return -- and so forth -- so much for type safety).
Object result = myCache.get(key);
if (result instanceof IOException || result instanceof SAXException)
throw result;
BigExpensiveThingy bet = (BigExpensiveThingy)result;
// I want to throw up!
java.lang.ClassCastException
at line 33 of AwfulCode.java
// I give up.
throw new Upchuck();
We could really use "exception type parameter varargs". Really, we
could. Checked exceptions just don't play nice with "inversion of
control" like patterns otherwise.