Dadas las siguientes funciones de identidad:
<T> Consumer<T> f(Consumer<T> c) { return c; } // (1) <T,R> Function<T,R> f(Function<T, R> c) { return c; } // (2)Observo el siguiente comportamiento en JDK 11 y JDK 17:
void _void() {} f(x -> {}); // okay, dispatches to (1) f(x -> { return; }); // okay, dispatches to (1) f(x -> { _void(); }); // okay, dispatches to (1) f(x -> _void()); // should dispatch to (1) | Error: | reference to f is ambiguous | both method f(java.util.function.Function<java.lang.Object,java.lang.Object>) in and method f(java.util.function.Consumer<java.lang.Object>) in match int _one() { return 1; } f(x -> 1); // okay, dispatches to (2) f(x -> { return 1; }); // okay, dispatches to (2) f(x -> { return _one(); }); // okay, dispatches to (2) f(x -> _one()); // should dispatch to (2) | Error: | reference to f is ambiguous | both method <T,R>f(java.util.function.Function<T,R>) in and method <T>f(java.util.function.Consumer<T>) in match¿Por qué el compilador no puede resolver estos símbolos utilizando el tipo de retorno de la expresión? Las versiones de llaves funcionan bien, y hubiera pensado que serían los casos más difíciles. Entiendo que puede lanzar explícitamente la función lambda, pero eso anula el propósito de lo que estoy tratando de lograr.
Se espera que x -> _void() y x -> one() sean compatibles con Consumer<T> (con el resultado de one() descartado).
Cuando el cuerpo lambda es de tipo bloque, el compilador comprueba adicionalmente la compatibilidad de "retorno". El JLS es bastante explícito acerca de la compatibilidad de vacío/valor para cuerpos de bloque:
Un cuerpo lambda de bloque es compatible con vacíos si cada declaración de devolución en el bloque tiene la forma de devolución; . Un cuerpo lambda de bloque es compatible con valores si no puede completarse normalmente (§14.21) y cada declaración de devolución en el bloque tiene la forma expresión de devolución; .
Si bien eso no dice por qué fallan los cuerpos de expresión única, dice exactamente por qué se compilan los cuerpos de bloque: el compilador observa los formularios de return para juzgar la compatibilidad de esos cuerpos con Consumer o Function (en este caso).
Para las expresiones de invocación de métodos, el hecho de que esto esté permitido:
Consumer<Integer> c = x -> one(); //discarded result Function<T, Integer> f = x -> one(); //returned resultno permite que el compilador resuelva el conflicto que observó. Puede reescribir la misma expresión lambda con cuerpos de bloque para resolver el conflicto, y eso es simplemente porque los cuerpos de bloque se verifican de manera diferente, por especificación.
Supongo que estoy tratando de decir que la pregunta más natural es "¿por qué los cuerpos de bloque se compilan en este caso" , dado que normalmente no esperamos que los tipos de retorno (¿formularios?) participen en la resolución de sobrecarga. Pero la congruencia de las expresiones lambda con los tipos es otra cosa, ¿no es así? Creo que esto (que el tipo de bloque ayuda a la inferencia de tipo objetivo) es el comportamiento especial.