Una característica peculiar de la inferencia de tipos de excepción en Java 8

85

Mientras escribía código para otra respuesta en este sitio, encontré esta peculiaridad:

static void testSneaky() {
  final Exception e = new Exception();
  sneakyThrow(e);    //no problems here
  nonSneakyThrow(e); //ERRROR: Unhandled exception: java.lang.Exception
}

@SuppressWarnings("unchecked")
static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
  throw (T) t;
}

static <T extends Throwable> void nonSneakyThrow(T t) throws T {
  throw t;
}

Primero, estoy bastante confundido por qué la sneakyThrowllamada está bien para el compilador. ¿Qué tipo posible dedujo Tcuando no se menciona en ninguna parte un tipo de excepción sin marcar?

En segundo lugar, aceptando que esto funciona, ¿por qué entonces el compilador se queja en la nonSneakyThrowllamada? Se parecen mucho.

Marko Topolnik
fuente

Respuestas:

66

Se sneakyThrowinfiere que la T de es RuntimeException. Esto se puede seguir de la especificación de idioma sobre inferencia de tipos ( http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html )

En primer lugar, hay una nota en la sección 18.1.3:

Un límite del formulario throws αes puramente informativo: dirige la resolución para optimizar la instanciación de α para que, si es posible, no sea un tipo de excepción comprobado.

Esto no afecta nada, pero nos apunta a la sección Resolución (18.4), que tiene más información sobre los tipos de excepción inferidos con un caso especial:

... De lo contrario, si el conjunto encuadernado contiene throws αi, y los límites superiores adecuadas de αi son, a lo sumo, Exception, Throwable, y Object, a continuación, Ti = RuntimeException.

Este caso se aplica a sneakyThrow: el único límite superior es Throwable, por lo que Tse infiere que es RuntimeExceptionsegún la especificación, por lo que se compila. El cuerpo del método es irrelevante: la conversión no verificada tiene éxito en el tiempo de ejecución porque en realidad no sucede, lo que deja un método que puede anular el sistema de excepciones verificado en tiempo de compilación.

nonSneakyThrowno compila porque ese método Ttiene un límite inferior de Exception(es decir, Tdebe ser un supertipo de Exception, o él Exceptionmismo), que es una excepción comprobada, debido al tipo con el que se llama, por lo que Tse infiere como Exception.

thecoop
fuente
1
@Maksym Debes decir la sneakyThrowllamada. Las regulaciones especiales sobre la inferencia de throws Tformas no existían en la especificación de Java 7.
Marko Topolnik
1
Pequeño detalle: En nonSneakyThrow, Tdebe ser Exception, no "un supertipo de" Exception, porque es exactamente el tipo de argumento declarado en tiempo de compilación en el sitio de llamada.
llogiq
1
@llogiq Si he leído la especificación correctamente, tiene un límite inferior Exceptiony un límite superior Throwable, por lo que el límite superior mínimo, que es el tipo inferido resultante, es Exception.
thecoop
1
@llogiq Tenga en cuenta que el tipo del argumento solo establece un límite de tipo inferior porque cualquier supertipo del argumento también es aceptable.
Marko Topolnik
2
la Exceptionfrase "o sí mismo" puede ser útil para el lector, pero en general, debe tenerse en cuenta que la especificación siempre usa los términos "subtipo" y "supertipo" en el sentido de "incluirse a sí mismo" ...
Holger
17

Si la inferencia de tipo produce un único límite superior para una variable de tipo, normalmente se elige el límite superior como solución. Por ejemplo, si T<<Numberla solución es T=Number. Aunque Integer, Floatetc. también podrían satisfacer la restricción, no hay una buena razón para elegirlos Number.

Ese fue también el caso para throws Ten Java 5-7: T<<Throwable => T=Throwable. (Todas las soluciones de lanzamiento furtivo tenían <RuntimeException>argumentos de tipo explícitos , de lo contrario <Throwable>se infiere).

En java8, con la introducción de lambda, esto se vuelve problemático. Considere este caso

interface Action<T extends Throwable>
{
    void doIt() throws T;
}

<T extends Throwable> void invoke(Action<T> action) throws T
{
    action.doIt(); // throws T
}    

Si invocamos con una lambda vacía, ¿cómo se Tinferiría?

    invoke( ()->{} ); 

La única restricción Tes un límite superior Throwable. En una etapa anterior de java8, T=Throwablese inferiría . Vea este informe que presenté.

Pero eso es bastante tonto, inferir Throwable, una excepción marcada, de un bloque vacío. En el informe se propuso una solución (que aparentemente fue adoptada por JLS):

If E has not been inferred from previous steps, and E is in the throw clause, 
and E has an upper constraint E<<X,
    if X:>RuntimeException, infer E=RuntimeException
    otherwise, infer E=X. (X is an Error or a checked exception)

es decir, si el límite superior es Exceptiono Throwable, elija RuntimeExceptioncomo solución. En este caso, no es una buena razón para elegir un subtipo particular de la cota superior.

ZhongYu
fuente
¿Cuál es el significado de X:>RuntimeExceptionen su último fragmento de ejemplo?
marsouf
1

Con sneakyThrow, el tipo Tes una variable de tipo genérico acotado sin un tipo específico (porque no hay de dónde podría provenir el tipo).

Con nonSneakyThrow, el tipo Tes del mismo tipo que el argumento, por lo que en su ejemplo, Tde nonSneakyThrow(e);es Exception. Como testSneaky()no declara un arrojado Exception, se muestra un error.

Tenga en cuenta que se trata de una interferencia conocida de los genéricos con las excepciones marcadas.

llogiq
fuente
Entonces sneakyThrow, ¿no se infiere realmente de ningún tipo específico, y el "elenco" es de un tipo tan indefinido? Me pregunto qué pasa realmente con esto.
Marko Topolnik