3

One more rookie question, sorry about that.

Let's consider the following code:

public class ExceptionHandler {

   // simple internal manager
   @FunctionalInterface
   private interface ExceptionManager<D extends Exception> {
     int getErrorCode(D e, WebRequest request, 
                      HttpServletRequest servletRequest);
   }

   // One field, just for the illustration 
   // (TypeMismatchException came from spring framework)
   private ExceptionManager<TypeMismatchException> tmeManager = 
      (ex, req, servletRequest) -> {
         int errorCode = 0;
         // ...
         return errorCode;
      };

   // A simple "factory" for an ExceptionManager
   private Function<? extends Exception, 
          Optional<ExceptionManager<? extends Exception>>> factory = (ex) -> {
      if(ex instanceof TypeMismatchException) {
         return Optional.of(tmeManager);
      }
      /* ... */
      return Optional.empty();
   };

   // global  exception manager
   private ExceptionManager<? extends Exception> defaultExceptionManager =
      (exception, request, servletRequest) -> {

         final Optional<ExceptionManager<? extends Exception>> manager = 
                         factory.apply(exception);

         if(manager.isPresent()) {
            return manager.get()
                    .getErrorCode(exception, request, servletRequest);
         }
         return 1;
      };
}

The following code cannot compile. It's actually complaints about a type incompatibility problem.

Error:(...) java: incompatible types: java.lang.Exception
      cannot be converted to capture#1 of ? extends java.lang.Exception
Error:(...) java: incompatible types: java.lang.Exception
      cannot be converted to capture#2 of ? extends java.lang.Exception

After thinking and reading about the problem, it seems that java perform a type erasure (for jvm backward compatibility) and thus the code:

private ExceptionManager<? extends Exception> defaultExceptionManager = 
                   (exception, request, servletRequest) -> { /* ... */ }

became

private ExceptionManager<Exception> defaultExceptionManager = 
                   (exception, request, servletRequest) -> { /* ... */ }

Which is actually fix the first parameter of the getErrorCode (namely exception) to Exception.

As I understand (not sure about really understand actually), the process should be the same for the generic type. Thus

private interface ExceptionManager<D extends Exception> { /* ... */ }

should became

private interface ExceptionManager<Exception> { /* ... */ }

and consequently also fix the parameter e in the getErrorCode method to Exception. The type incompatibility probleme became a bit more clear after (if I'm right). However, I'm still doubtfully about the capture#xx of ? extends Exception since this means (still according to my comprehension) that the Type Erasure is not effective for the whole part of code.

Can someone point me out of the error in the code (and may be a documentation which I can find some explanation about the internal behavior of the compiler for generics, wildcard and type erasure) ?

Note: the code also complains about an incompatible type.

protected ResponseEntity<Object> handleTypeMismatch(final TypeMismatchException ex,
   final HttpHeaders headers, final HttpStatus status,
   final WebRequest request) {
   /* ... */
   int errorCode = defaultExceptionManager.getErrorCode(ex, request, servletRequest);
}

The result of this call is

Error:(154, 63) java: incompatible types:
      org.springframework.beans.TypeMismatchException
      cannot be converted to capture#3 of ? extends java.lang.Exception

Sorry for the length of this question and thanks for reading and answering to it! Regards

Naman
  • 27,789
  • 26
  • 218
  • 353
ollie314
  • 440
  • 1
  • 10
  • 19
  • 1
    I think the first answer here describes how to find out what's going wrong: https://stackoverflow.com/questions/31227149/why-is-this-type-inference-not-working-with-this-lambda-expression-scenario – JohnB Aug 19 '18 at 08:12
  • 1
    What happens if you cast the lambda explicitly before assigning? `ExceptionManager extends Exception> defaultExceptionManager = (ExceptionManager extends Exception>) ((exception, request, servletRequest) -> { ... )` ? And, btw, wouldn't a simple `ExceptionManager` do the job? – JohnB Aug 19 '18 at 08:25
  • Thanks for both responses @JohnB (the post you point out is pretty interesting and didn't land on it before). For the second point, the cast doesn't change the behavior. For the second point, I wanna be able to keep genericity in the interface so that I can write manager (ex: `ExceptionManager tmeManager`) without casting anything inside the implementation. Note that the structure of the code is mainly for learning how to deal with some features of java8 (function, lambda and generics). One more thanks for your time and your answers. – ollie314 Aug 19 '18 at 08:34

1 Answers1

3

When you declare a function like Function<? extends Exception, …>, you’re saying that the type of the parameter is unknown and consequently, you can not apply this function as you don’t know whether the actual argument is compatible to the unknown parameter type. The same applies to the ExceptionManager<? extends Exception>, which receives an unknown exception type as argument.

This is different from not knowing the return type, as when a function returns ? extends R, you still know that the result is assignable to R or a super type of R.

There is a relationship between the incoming argument and the result type which would make this code usable if it was generic, however, you can’t make a variable (holding a reference to a Function) generic. You can solve this with using ordinary methods, which can declare type parameters. This is almost straight-forward, as you’re overusing functions here anyway:

public class ExceptionHandler {
    // simple internal manager
    @FunctionalInterface
    private interface ExceptionManager<D extends Exception> {
        int getErrorCode(D e, WebRequest request, HttpServletRequest servletRequest);
    }
    // One field, just for the illustration 
    private static ExceptionManager<TypeMismatchException> tmeManager = 
       (ex, req, servletRequest) -> {
          int errorCode = 0;
          // ...
          return errorCode;
       };

    // A simple "factory" for an ExceptionManager
    private static <E extends Exception> Optional<ExceptionManager<E>> factory(E ex) {
        if(ex instanceof TypeMismatchException) {
            // unavoidable unchecked operation
            @SuppressWarnings("unchecked") ExceptionManager<E> em
                                         = (ExceptionManager<E>)tmeManager;
            return Optional.of(em);
        }
        /* ... */
        return Optional.empty();
    }
    // global  exception manager
    private ExceptionManager<Exception> defaultExceptionManager
                                      = ExceptionHandler::handleDefault;

    static <E extends Exception> int handleDefault(E exception, WebRequest request, 
                                                   HttpServletRequest servletRequest) {
        final Optional<ExceptionManager<E>> manager = factory(exception);
        return manager.map(em -> em.getErrorCode(exception, request, servletRequest))
                      .orElse(1);
    }
}

There’s one place where an unchecked operation is unavoidable, when returning a specific handler found to be suitable through an instanceof check. Care must be taken here, as the exception could be a sub-type TypeMismatchException. It’s also possible that the instance is a TypeMismatchException at runtime, but the caller has substituted a super type of it for E. The latter is the more dangerous scenario, as the generic signature would promise to be able to process broader types than it actually can. As long as the method is private, you can easily overview that the caller(s) only pass the same instance as used for the check, so it’ll work.

Holger
  • 285,553
  • 42
  • 434
  • 765