2

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?

scubbo
  • 4,969
  • 7
  • 40
  • 71

1 Answers1

3

The method that is called is defined by the static type of the argument.

  • In the case you catch an Exception, the static type is Exception, which is not a subclass of RuntimeException, so the generic of(Object) is called. (Recall that T is translated to Object in compilation).
  • In the case you catch RuntimeException, the static type is RuntimeException, and since it does fit of(RuntimeException), the more specific method is called.

Note that e.getClass().getSimpleName() is giving you the Dynamic type, and not the static one. The dynamic type is unknown during compilation, while which method is invoked is chosen during compile time.

Here is a simpler code that demonstrates the same issue:

public static void foo(Object o) { 
    System.out.println("foo(Object)");
}
public static void foo(Integer n) { 
    System.out.println("foo(Integer)");
}
public static void main (String[] args) throws java.lang.Exception {
    Number x = new Integer(5);
    foo(x);
    System.out.println(x.getClass().getSimpleName());
}

In here, the method foo(Object) is called, even though x is an Integer, because the static type of x, which is known in compile time, is Number, and is not a subclass of Integer.

amit
  • 175,853
  • 27
  • 231
  • 333