tipos - ¿Por qué la inferencia de tipo Java 8 no considera las excepciones lanzadas por Lambdas en la selección de sobrecarga?
listado de excepciones java (3)
Tengo una pregunta sobre la inferencia de Java 8 con respecto a lambdas y sus firmas de excepción relacionadas.
Si defino algún método foo:
public static <T> void foo(Supplier<T> supplier) {
//some logic
...
}
luego obtengo la semántica agradable y concisa de poder escribir foo(() -> getTheT());
en la mayoría de los casos para un T
dado. Sin embargo, en este ejemplo, si mi operación getTheT
declara que throws Exception
, mi método foo
que toma un Proveedor ya no se compila: la firma del método Supplier para get
no arroja excepciones.
Parece que una forma decente de evitar esto sería sobrecargar a foo para aceptar cualquiera de las opciones, con la definición sobrecargada de:
public static <T> void foo(ThrowingSupplier<T> supplier) {
//same logic as other one
...
}
donde ThrowingSupplier se define como
public interface ThrowingSupplier<T> {
public T get() throws Exception;
}
De esta forma, tenemos un tipo de Proveedor que arroja excepciones y otro que no. La sintaxis deseada sería algo como esto:
foo(() -> operationWhichDoesntThrow()); //Doesn''t throw, handled by Supplier
foo(() -> operationWhichThrows()); //Does throw, handled by ThrowingSupplier
Sin embargo, esto causa problemas debido a que el tipo de lambda es ambiguo (presumiblemente no se puede resolver entre el proveedor y ThrowingSupplier). Hacer un lanzamiento explícito a la foo((ThrowingSupplier)(() -> operationWhichThrows()));
funcionaría, pero elimina la mayor parte de la concisión de la sintaxis deseada.
Supongo que la pregunta subyacente es: si el compilador de Java es capaz de resolver el hecho de que una de mis lambdas es incompatible debido a que arroja una excepción en el caso de solo proveedor, ¿por qué no puede usar esa misma información para derivar? el tipo de lambda en el caso secundario de tipografía?
También agradecería cualquier información o recurso que cualquier persona pueda señalarme, ya que no estoy muy seguro de dónde buscar más información sobre el asunto.
¡Gracias!
Cualquier lambda que pueda ser aceptado como un Supplier<T>
también puede ser aceptado como un ThrowingSupplier<T>
. Las siguientes compilaciones:
public static interface ThrowingSupplier<T>{
public T get() throws Exception;
}
public static <T> void foo(ThrowingSupplier<T> supplier) {
}
public static String getAString(){
return "Hello";
}
public static String getAnotherString() throws Exception{
return "World";
}
public static void main(String[] args) {
foo(()->getAString());
foo(()->getAnotherString());
}
Dado lo anterior, probablemente no lo necesite, pero si foo
debe aceptar un Supplier<T>
que no arroje, siempre puede ajustar el método Exception-throwing en un método que lo lave en una Excepción no verificada:
public static <T> void foo(Supplier<T> supplier) {
}
public static String getAString(){
return "Hello";
}
public static String getAnotherString() throws Exception{
return "World";
}
public static String getAnotherStringUnchecked(){
try{
return getAnotherString();
} catch(Exception e){
throw new RuntimeException("Error getting another string",e);
}
}
public static void main(String[] args) throws Exception{
foo(()->getAString());
foo(()->getAnotherStringUnchecked());
}
En primer lugar, no tiene que sobrecargar: D - la sobrecarga nunca es una necesidad; usa 2 nombres de método diferentes, por ejemplo, foo
y fooX
En segundo lugar, no veo por qué necesita 2 métodos aquí. Si desea controlar excepciones marcadas y no marcadas de manera diferente, se puede hacer en tiempo de ejecución. Para lograr "transparencia de excepción", puede hacer
interface SupplierX<T, X extends Throwable>
{
T get() throws X;
}
<T, X extends Throwable> void foo(Supplier<T, X> supplier) throws X { .. }
foo( ()->"" ); // throws RuntimeException
foo( ()->{ throw new IOException(); } ); // X=IOException
Finalmente, la desambigüedad se puede lograr lanzando el tipo de retorno lambda; el compilador usa el tipo de devolución como si usara un tipo de argumento para elegir el método más específico. Esto nos da la idea de ajustar el valor junto con el tipo de excepción, como Result<T,X>
, una "mónada" como dicen.
interface Result<T, X extends Throwable>
{
T get() throws X;
}
// error type embedded in return type, not in `throws` clause
static Result<String, Exception> m1(){ return ()->{ throw new Exception();}; }
static Result<String, RuntimeException> m2(){ return ()->{ return "str"; }; }
// better to have some factory method, e.g. return Result.success("str");
public static void main(String[] args)
{
foo(()->m1()); // foo#2 is not applicable
foo(()->m2()); // both applicable; foo#2 is more specific
}
interface S1<T> { T get(); }
static <T> void foo(S1<Result<T, ? extends Exception>> s)
{
System.out.println("s1");}
}
interface S2<T> { T get(); } // can''t have two foo(S1) due to erasure
static <T> void foo(S2<Result<T, ? extends RuntimeException>> s)
{
System.out.println("s2");
}
Si te hace sentir mejor, este tema fue cuidadosamente considerado durante el proceso de diseño de JSR-335.
La pregunta no es "por qué no es capaz", sino "por qué elegimos no hacerlo". Cuando encontramos múltiples sobrecargas potencialmente aplicables, ciertamente podríamos haber elegido atribuir especulativamente el cuerpo lambda bajo cada conjunto de firmas, y podar aquellos candidatos para los cuales el cuerpo lambda no pudo verificar el tipo.
Sin embargo, llegamos a la conclusión de que hacerlo probablemente haría más daño que bien; significa, por ejemplo, que pequeños cambios en el cuerpo del método, según esta regla, podrían provocar que algunas decisiones de selección de sobrecarga de método cambien silenciosamente sin que el usuario tenga la intención de hacerlo. Al final, llegamos a la conclusión de que usar la presencia de errores en el cuerpo del método para descartar un candidato potencialmente aplicable causaría más confusión que beneficio, especialmente dado que hay una solución alternativa simple y segura: proporcionar un tipo de objetivo. Sentimos que la confiabilidad y la previsibilidad aquí superaban la concisión óptima.