interfaces - java funcional
Lambdas e interfaces funcionales con cláusulas genéricas. (1)
Considere este fragmento de código de Java 8:
public class Generics {
public static <V, E extends Exception> V f(CheckedCallable1<V, E> callable) throws E {
return callable.call();
}
public static <V, E extends Exception> V g(CheckedCallable2<V, E> callable) throws E {
return callable.call();
}
public static void main(String[] args) {
f(() -> 1);
g(() -> 1);
}
}
interface Callable<V> {
V call() throws Exception;
}
interface CheckedCallable1<V, E extends Exception> {
V call() throws E;
}
interface CheckedCallable2<V, E extends Exception> extends Callable<V> {
@Override V call() throws E;
}
La lambda en la llamada a f
compila bien, mientras que la lambda en la llamada a g
no compila, sino que da este error de compilación:
Error:(10, 7) java: call() in <anonymous Generics$> cannot implement call() in CheckedCallable2
overridden method does not throw java.lang.Exception
¿Por qué es esto?
Me parece que los métodos CheckedCallable1.call
y CheckedCallable2.call
son equivalentes: según las reglas de borrado de tipo, V
convierte en Object
como es ilimitado y E
convierte en Exception
, ya que ese es el límite de tipo superior. Entonces, ¿por qué el compilador piensa que el método anulado no lanza la excepción java.lang.Exception?
Incluso ignorar el borrado de tipos, que probablemente no sea relevante aquí porque todo esto está sucediendo en tiempo de compilación, todavía no tiene sentido para mí: no veo una razón por la cual este patrón, si se permite, resultaría, por ejemplo, erróneo. código java.
Entonces, ¿puede alguien aclararme por qué esto no está permitido?
Actualizar:
Así que encontré algo que quizás sea aún más interesante. Tome el archivo anterior, cambie cada ocurrencia de Exception
a IOException
y agregue la cláusula throws a main
. Compilar obras! Cambia de nuevo a Exception
: compila los descansos!
Esto compila bien:
import java.io.IOException;
public class Generics {
public static <V, E extends IOException> V f(CheckedCallable1<V, E> callable) throws E {
return callable.call();
}
public static <V, E extends IOException> V g(CheckedCallable2<V, E> callable) throws E {
return callable.call();
}
public static void main(String[] args) throws IOException {
f(() -> 1);
g(() -> 1);
}
}
interface Callable<V> {
V call() throws IOException;
}
interface CheckedCallable1<V, E extends IOException> {
V call() throws E;
}
interface CheckedCallable2<V, E extends IOException> extends Callable<V> {
@Override V call() throws E;
}
En este punto, está empezando a parecerse más y más a un error de Java ...
No creo que haya una regla que prohíba este patrón. Es bastante probable que hayas encontrado un error en el compilador.
Es fácil mostrar que este patrón no da como resultado un código erróneo simplemente escribiendo el código de clase interno equivalente de g(() -> 1);
:
g(new CheckedCallable2<Integer, RuntimeException>() {
public Integer call() {
return 1;
}
});
Esto se compila y ejecuta sin problemas, incluso bajo Java 6 (supongo que incluso funcionaría en Java 5 pero no tenía JDK para probarlo) y no hay ninguna razón por la que no debería funcionar cuando se hace lo mismo con un lambda. Escribir este código en Netbeans da como resultado incluso la recomendación de convertirlo en un lambda.
Tampoco hay restricción de tiempo de ejecución que prohibiría tal construcción. Además del hecho de que bajo el capó no hay reglas de excepción aplicadas y todo depende de las verificaciones en tiempo de compilación, incluso podemos probar que funcionaría si el compilador aceptara nuestro código creando el código manualmente que el compilador crearía:
CheckedCallable2<Integer,RuntimeException> c;
try
{
MethodHandles.Lookup l = MethodHandles.lookup();
c=(CheckedCallable2)
LambdaMetafactory.metafactory(l, "call",
MethodType.methodType(CheckedCallable2.class),
MethodType.methodType(Object.class),
l.findStatic(Generics.class, "lambda$1", MethodType.methodType(int.class)),
MethodType.methodType(Integer.class)).getTarget().invokeExact();
} catch(Throwable t) { throw new AssertionError(t); }
int i=g(c);
System.out.println(i);
// verify that the inheritance is sound:
Callable<Integer> x=c;
try { System.out.println(x.call()); }// throws Exception
catch(Exception ex) { throw new AssertionError(ex); }
…
static int lambda$1() { return 1; }// the synthetic method for ()->1
Este código se ejecuta y produce 1
como se esperaba, independientemente de la interface
que usamos para call()
. Sólo las excepciones que tenemos que atrapar difieren. Pero como se dijo, eso es un artefacto en tiempo de compilación.