I've run into an unexpected issue involving Exception catching and Java generics in signatures. Without further ado, the code in question (explanation follows):
public class StackOverflowTest {
private static class WrapperBuilder {
public static <T> ResultWrapper of(final T result) {
return new ResultWrapper<>(result);
}
public static ResultWrapper of(final RuntimeException exc) {
return new ResultWrapper<>(exc);
}
}
private static class ResultWrapper<T> {
private final T result;
private final RuntimeException exc;
ResultWrapper(final T result) {
this.result = result;
this.exc = null;
}
ResultWrapper(final RuntimeException exc) {
this.result = null;
this.exc = exc;
}
public Boolean hasException() {
return this.exc != null;
}
public T get() {
if (hasException()) {
throw exc;
}
return result;
}
}
private static class WrapperTransformer {
public ResultWrapper<Result> getResult(ResultWrapper originalWrappedResult) {
if (originalWrappedResult.hasException()) {
try {
originalWrappedResult.get();
} catch (Exception e) {
return WrapperBuilder.of(e);
}
}
return originalWrappedResult; // Transformation is a no-op, here
}
}
private static class Result {}
WrapperTransformer wrapper = new WrapperTransformer();
@Test
public void testBehaviour() {
ResultWrapper wrappedResult = WrapperBuilder.of(new RuntimeException());
final ResultWrapper<Result> result = wrapper.getResult(wrappedResult);
assertTrue(result.hasException()); // fails!
}
}
Leaving aside, for the moment, questions of bad style (I completely acknowledge that there are better ways to do do what I'm doing here!), this is a trimmed down and anonymised version of the following business logic:
- class
ResultWrapper
wraps the result of a call to a downstream service. It either contains the result of the call, or the resulting exception - class
WrapperTransformer
is responsible for transforming the ResultWrapper in some way (though, here, the "transformation" is a no-op)
The test given above fails. From debugging, I have determined that this is because WrapperBuilder.of(e)
is, in fact, calling the generic method (i.e. of(final T result)
). That (sort of) makes sense, if generic arguments are "greedy" - a RuntimeException
is a T
, so that method is a sensible (though unintended) choice.
However, when the DownstreamWrapper::getResult
method is changed to:
// i.e. explicitly catch RuntimeException, not Exception
} catch (RuntimeException e) {
return WrapperBuilder.of(e)
}
then the test fails - i.e. the Exception
is identified as a RuntimeException
, the non-generic .of
method is called, and so the resulting ResultWrapper
has a populated exc
.
This is completely baffling to me. I believe that, even inside a catch (Exception e)
clause, e
retains its original type (and logging messages System.out.println(e.getClass().getSimpleName()
suggest that is true) - so how can changing the "type" of the catch override the generic method signature?