Java8: ambigüedad con lambdas y métodos sobrecargados

Estoy jugando con java8 lambdas y me encontré con un error de comstackción que no esperaba.

Digamos que tengo una interface A funcional interface A , una abstract class B y una class C con métodos sobrecargados que toman A o B como argumentos:

 public interface A { void invoke(String arg); } public abstract class B { public abstract void invoke(String arg); } public class C { public void apply(A x) { } public B apply(B x) { return x; } } 

Luego puedo pasar una lambda a c.apply y se resuelve correctamente para c.apply(A) .

 C c = new C(); c.apply(x -> System.out.println(x)); 

Pero cuando cambio la sobrecarga que toma B como argumento para una versión genérica, el comstackdor informa que las dos sobrecargas son ambiguas.

 public class C { public void apply(A x) { } public  T apply(T x) { return x; } } 

Pensé que el comstackdor vería que T tiene que ser una subclase de B que no es una interfaz funcional. ¿Por qué no puede resolver el método correcto?

Existe una gran complejidad en la intersección de la resolución de sobrecarga y la inferencia de tipo. El borrador actual de la especificación lambda tiene todos los detalles sangrientos. Las secciones F y G cubren la resolución de sobrecarga y la inferencia de tipo, respectivamente. No pretendo entenderlo todo. Sin embargo, las secciones de resumen en la introducción son bastante comprensibles, y recomiendo que las personas las lean, particularmente los resúmenes de las secciones F y G, para tener una idea de lo que está sucediendo en esta área.

Para recapitular los problemas brevemente, considere una llamada al método con algunos argumentos en presencia de métodos sobrecargados. La resolución de sobrecarga debe elegir el método correcto para llamar. La “forma” del método (arity, o número de argumentos) es más significativa; Obviamente, una llamada a método con un argumento no puede resolverse a un método que toma dos parámetros. Pero los métodos sobrecargados a menudo tienen la misma cantidad de parámetros de diferentes tipos. En este caso, los tipos comienzan a importar.

Supongamos que hay dos métodos sobrecargados:

  void foo(int i); void foo(String s); 

y algún código tiene la siguiente llamada a método:

  foo("hello"); 

Obviamente, esto se resuelve en el segundo método, según el tipo de argumento que se aprueba. Pero, ¿y si estamos haciendo una resolución de sobrecarga, y el argumento es una lambda? (Especialmente uno cuyos tipos son implícitos, que se basa en la inferencia de tipos para establecer los tipos). Recuerde que el tipo de expresión lambda se deduce del tipo de destino, es decir, el tipo esperado en este contexto. Desafortunadamente, si tenemos métodos sobrecargados, no tenemos un tipo de objective hasta que hayamos resuelto qué método sobrecargado vamos a llamar. Pero como todavía no tenemos un tipo para la expresión lambda, no podemos usar su tipo para ayudarnos durante la resolución de sobrecarga.

Veamos el ejemplo aquí. Considere la interfaz A y la clase abstracta B tal como se define en el ejemplo. Tenemos la clase C que contiene dos sobrecargas, y luego un código llama al método apply y lo pasa a lambda:

  public void apply(A a) public B apply(B b) c.apply(x -> System.out.println(x)); 

Ambas sobrecargas tienen el mismo número de parámetros. El argumento es un lambda, que debe coincidir con una interfaz funcional. A y B son tipos reales, por lo que es manifiesto que A es una interfaz funcional, mientras que B no lo es, por lo tanto, se apply(A) el resultado de la resolución de sobrecarga apply(A) . En este momento, ahora tenemos un tipo de destino A para la lambda, y escriba inferencia para x proceeds.

Ahora la variación:

  public void apply(A a) public  T apply(T t) c.apply(x -> System.out.println(x)); 

En lugar de un tipo real, la segunda sobrecarga de apply es una variable de tipo genérico T No hemos hecho una inferencia de tipo, por lo que no tomamos en cuenta a T , al menos no hasta que se haya completado la resolución de sobrecarga. Por lo tanto, ambas sobrecargas siguen siendo aplicables, ninguna de las dos es más específica y el comstackdor emite un error de que la llamada es ambigua.

Podría argumentar que, dado que sabemos que T tiene un límite de tipo B , que es una clase, no una interfaz funcional, la lambda no puede aplicarse a esta sobrecarga, por lo que debe descartarse durante la resolución de sobrecarga, eliminando el ambigüedad. No soy yo quien tiene esa discusión. 🙂 Esto podría ser un error en el comstackdor o incluso en la especificación.

Sé que esta área sufrió un montón de cambios durante el diseño de Java 8. Las variaciones anteriores intentaron traer más información de verificación e inferencia de tipos a la fase de resolución de sobrecarga, pero fueron más difíciles de implementar, especificar y comprender. (Sí, aún más difícil de entender de lo que es ahora.) Desafortunadamente, los problemas siguieron surgiendo. Se decidió simplificar las cosas al reducir el rango de cosas que pueden sobrecargarse.

La inferencia y sobrecarga de tipos siempre están en oposición; muchos lenguajes con inferencia tipo desde el día 1 prohíben la sobrecarga (excepto tal vez en arity). Por lo tanto, para constructos como lambdas implícitos, que requieren inferencia, parece razonable abandonar algo con sobrecarga para boost el rango de casos donde se pueden usar lambdas implícitos. .

– Brian Goetz, Grupo de expertos de Lambda, 9 de agosto de 2013

(Esta fue una decisión bastante controvertida. Tenga en cuenta que hubo 116 mensajes en este hilo, y hay varios otros temas que tratan este tema).

Una de las consecuencias de esta decisión fue que ciertas API se tuvieron que cambiar para evitar la sobrecarga, por ejemplo, la API de comparación . Anteriormente, el método Comparator.comparing tenía cuatro sobrecargas:

  comparing(Function) comparing(ToDoubleFunction) comparing(ToIntFunction) comparing(ToLongFunction) 

El problema es que estas sobrecargas se diferencian solo por el tipo de retorno lambda, y en realidad nunca obtuvimos la inferencia de tipo para trabajar aquí con lambdas implícitamente tipadas. Para usar estos, uno siempre tendrá que enviar o proporcionar un argumento de tipo explícito para el lambda. Estas API se cambiaron a:

  comparing(Function) comparingDouble(ToDoubleFunction) comparingInt(ToIntFunction) comparingLong(ToLongFunction) 

que es algo torpe, pero no tiene ambigüedades. Una situación similar ocurre con Stream.map , mapToDouble , mapToInt y mapToLong , y en algunos otros lugares alrededor de la API.

La conclusión es que obtener resolución de sobrecarga en presencia de inferencia de tipo es muy difícil en general, y que el lenguaje y los diseñadores del comstackdor intercambiaron energía de la resolución de sobrecarga para hacer que la inferencia de tipo funcione mejor. Por este motivo, las API de Java 8 evitan los métodos sobrecargados en los que se espera que se utilicen lambdas implícitamente tipados.

Creo que la respuesta es que un subtipo T de B podría implementar A, lo que hace que sea ambiguo a qué función enviar un argumento de ese tipo T.

Creo que este caso de prueba expone una situación en la que el comstackdor de javac 8 podría hacer más para tratar de descartar un candidato de sobrecarga inaplicable, segundo método en:

 public class C { public void apply(A x) { } public  T apply(T x) { return x; } } 

Basado en el hecho de que nunca se puede crear una instancia de T en una interfaz funcional. Este caso es muy interesante. @ schenka7 gracias por preguntar esto. Investigaré los pros y los contras de tal propuesta.

En este momento, el principal argumento en contra de implementar esto podría ser cuán frecuente puede ser este código. Supongo que una vez que las personas comiencen a convertir el código Java actual a Java 8, las posibilidades de encontrar este patrón pueden ser mayores.

Otra consideración es que si comenzamos a agregar casos especiales a la especificación / comstackdor puede ser más complicado de entender, explicar y mantener.

He archivado este informe de error: JDK-8046045