Selección de firma de método para expresión lambda con múltiples tipos de objetivos coincidentes

11

Estaba respondiendo una pregunta y me encontré con un escenario que no puedo explicar. Considera este código:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

No entiendo por qué escribir explícitamente el parámetro de lambda (A a) -> aList.add(a)hace que el código se compile. Además, ¿por qué se vincula a la sobrecarga en Iterablelugar de a la que está dentro CustomIterable?
¿Hay alguna explicación para esto o un enlace a la sección relevante de la especificación?

Nota: iterable.forEach((A a) -> aList.add(a));solo se compila cuando se CustomIterable<T>extiende Iterable<T>(sobrecargando completamente los métodos en CustomIterableresultados en el error ambiguo)


Conseguir esto en ambos:

  • openjdk versión "13.0.2" 2020-01-14
    Compilador Eclipse
  • Openjdk versión "1.8.0_232"
    compilador Eclipse

Editar : el código anterior no se compila al compilar con maven mientras Eclipse compila la última línea de código con éxito.

ernest_k
fuente
3
Ninguno de los tres compila en Java 8. Ahora no estoy seguro de si se trata de un error que se solucionó en una versión más nueva, o un error / función que se introdujo ... Probablemente debería especificar la versión de Java
Sweeper
@Sweeper Inicialmente obtuve esto usando jdk-13. Las pruebas posteriores en java 8 (jdk8u232) muestran los mismos errores. No estoy seguro de por qué el último tampoco se compila en su máquina.
ernest_k
Tampoco se puede reproducir en dos compiladores en línea ( 1 , 2 ). Estoy usando 1.8.0_221 en mi máquina. Esto se está volviendo más y más extraño ...
Sweeper
1
@ernest_k Eclipse tiene su propia implementación del compilador. Esa podría ser información crucial para la pregunta. Además, el hecho de que un constructor maven limpio cometa errores en la última línea también debe destacarse en la pregunta en mi opinión. Por otro lado, sobre la pregunta vinculada, la suposición de que el OP también está utilizando Eclipse podría aclararse, ya que el código no es reproducible.
Naman
3
Si bien entiendo el valor intelectual de la pregunta, solo puedo desaconsejar la creación de métodos sobrecargados que solo difieran en las interfaces funcionales y esperar que pueda llamarse con seguridad pasar una lambda. No creo que la combinación de inferencia de tipo lambda y sobrecarga sea algo que el programador promedio pueda llegar a comprender. Es una ecuación con muchas variables que los usuarios no controlan. EVITE ESTO, por favor :)
Stephan Herrmann

Respuestas:

8

TL; DR, este es un error del compilador.

No existe una regla que otorgue prioridad a un método aplicable en particular cuando se hereda o un método predeterminado. Curiosamente, cuando cambio el código a

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

La iterable.forEach((A a) -> aList.add(a));declaración produce un error en Eclipse.

Como ninguna propiedad del forEach(Consumer<? super T) c)método desde la Iterable<T>interfaz cambió cuando se declara otra sobrecarga, la decisión de Eclipse de seleccionar este método no puede basarse (de manera consistente) en ninguna propiedad del método. Sigue siendo el único método heredado, sigue siendo el único defaultmétodo, sigue siendo el único método JDK, etc. Ninguna de estas propiedades debería afectar la selección del método de todos modos.

Tenga en cuenta que cambiar la declaración a

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

también produce un error "ambiguo", por lo que el número de métodos sobrecargados aplicables tampoco importa, incluso cuando solo hay dos candidatos, no hay preferencia general hacia los defaultmétodos.

Hasta ahora, el problema parece aparecer cuando hay dos métodos aplicables y un defaultmétodo y una relación de herencia están involucrados, pero este no es el lugar correcto para profundizar.


Pero es comprensible que las construcciones de su ejemplo puedan ser manejadas por diferentes códigos de implementación en el compilador, uno exhibiendo un error mientras que el otro no.
a -> aList.add(a)es una expresión lambda escrita implícitamente , que no se puede usar para la resolución de sobrecarga. Por el contrario, (A a) -> aList.add(a)es una expresión lambda escrita explícitamente que se puede usar para seleccionar un método coincidente de los métodos sobrecargados, pero no ayuda aquí (no debería ayudar aquí), ya que todos los métodos tienen tipos de parámetros con exactamente la misma firma funcional .

Como contraejemplo, con

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

las firmas funcionales difieren, y el uso de una expresión lambda de tipo explícito puede ayudar a seleccionar el método correcto, mientras que la expresión lambda escrita implícitamente no ayuda, por lo que forEach(s -> s.isEmpty())produce un error de compilación. Y todos los compiladores de Java están de acuerdo con eso.

Tenga en cuenta que aList::addes una referencia de método ambigua, ya que el addmétodo también está sobrecargado, por lo que tampoco puede ayudar a seleccionar un método, pero las referencias de método pueden ser procesadas por un código diferente de todos modos. El cambio a una ambigua aList::containso cambiar Lista Collection, para que addno ambigua, no cambió el resultado en mi instalación de Eclipse (utilicé 2019-06).

Holger
fuente
1
@howlger tu comentario no tiene sentido. El método predeterminado es el método heredado y el método está sobrecargado. No hay otro método. El hecho de que el método heredado sea un defaultmétodo es solo un punto adicional. Mi respuesta ya muestra un ejemplo donde Eclipse no da prioridad al método predeterminado.
Holger
1
@howlger hay una diferencia fundamental en nuestros comportamientos. Solo descubrió que la eliminación defaultcambia el resultado e inmediatamente asume que encontró la razón del comportamiento observado. Estás tan confiado al respecto que llamas a otras respuestas incorrectas, a pesar de que ni siquiera son contradictorias. Debido a que está proyectando su propio comportamiento sobre otro , nunca alegué que la herencia fuera la razón . Probé que no lo es. Demostré que el comportamiento es inconsistente, ya que Eclipse selecciona el método particular en un escenario pero no en otro, donde existen tres sobrecargas.
Holger
1
@howlger además de eso, ya mencioné otro escenario al final de este comentario , crear una interfaz, sin herencia, dos métodos, uno con defaultel otro abstract, con dos argumentos similares para el consumidor y probarlo. Eclipse dice correctamente que es ambiguo, a pesar de que uno es un defaultmétodo. Aparentemente, la herencia sigue siendo relevante para este error de Eclipse, pero a diferencia de usted, no me estoy volviendo loco y digo que otras respuestas son incorrectas, solo porque no analizaron el error en su totalidad. Ese no es nuestro trabajo aquí.
Holger
1
@howlger no, el punto es que esto es un error. La mayoría de los lectores ni siquiera se preocuparán por los detalles. Eclipse podría tirar dados cada vez que seleccione un método, no importa. Eclipse no debe seleccionar un método cuando es ambiguo, por lo que no importa por qué selecciona uno. Esta respuesta demuestra que el comportamiento es inconsistente, lo que ya es suficiente para indicar fuertemente que es un error. No es necesario señalar la línea del código fuente de Eclipse donde las cosas salen mal. Ese no es el propósito de Stackoverflow. Tal vez, estás confundiendo Stackoverflow con el rastreador de errores de Eclipse.
Holger
1
@howlger, nuevamente estás afirmando incorrectamente que hice una declaración (incorrecta) de por qué Eclipse hizo esa elección incorrecta. Nuevamente, no lo hice, ya que el eclipse no debería hacer una elección en absoluto. El método es ambiguo. Punto. La razón por la que usé el término " heredado " es porque era necesario distinguir métodos con nombres idénticos. Podría haber dicho " método predeterminado " en su lugar, sin cambiar la lógica. Más correctamente, debería haber usado la frase " el mismo método Eclipse seleccionado incorrectamente por cualquier razón ". Puede usar cualquiera de las tres frases indistintamente, y la lógica no cambia.
Holger
2

El compilador de Eclipse resuelve correctamente el defaultmétodo , ya que este es el método más específico de acuerdo con la Especificación de lenguaje Java 15.12.2.5 :

Si exactamente uno de los métodos máximos específicos es concreto (es decir, no abstractpredeterminado o no), es el método más específico.

javac(utilizado por Maven e IntelliJ por defecto) indica que la llamada al método es ambigua aquí. Pero de acuerdo con la especificación del lenguaje Java, no es ambiguo ya que uno de los dos métodos es el método más específico aquí.

Las expresiones lambda escritas implícitamente se manejan de manera diferente a las expresiones lambda escritas explícitamente en Java. Tipeado implícitamente en contraste con las expresiones lambda tipeadas explícitamente pasan por la primera fase para identificar los métodos de invocación estricta (consulte la Especificación del lenguaje Java jls-15.12.2.2 , primer punto). Por lo tanto, la llamada al método aquí es ambigua para expresiones lambda escritas implícitamente .

En su caso, la solución para este javacerror es especificar el tipo de la interfaz funcional en lugar de usar una expresión lambda escrita explícitamente de la siguiente manera:

iterable.forEach((ConsumerOne<A>) aList::add);

o

iterable.forEach((Consumer<A>) aList::add);

Aquí está su ejemplo minimizado para pruebas:

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}
aullador
fuente
44
Se perdió la condición previa justo antes de la oración citada: " Si todos los métodos máximos específicos tienen firmas equivalentes de anulación " Por supuesto, dos métodos cuyos argumentos son interfaces completamente no relacionadas no tienen firmas equivalentes de anulación. Además de eso, esto no explicaría por qué Eclipse deja de seleccionar el defaultmétodo cuando hay tres métodos candidatos o cuando ambos métodos se declaran en la misma interfaz.
Holger
1
@Holger Su respuesta afirma: "No existe una regla que otorgue prioridad a un método aplicable en particular cuando se hereda o un método predeterminado". ¿Te entiendo correctamente que estás diciendo que la condición previa para esta regla inexistente no se aplica aquí? Tenga en cuenta que el parámetro aquí es una interfaz funcional (consulte JLS 9.8).
aullador
1
Rasgaste una oración fuera de contexto. La oración describe la selección de métodos equivalentes de anulación , en otras palabras, una elección entre declaraciones que invocarían el mismo método en tiempo de ejecución, porque solo habrá un método concreto en la clase concreta. Esto es irrelevante para el caso de métodos distintos como forEach(Consumer)y forEach(Consumer2)que nunca pueden terminar en el mismo método de implementación.
Holger
2
@StephanHerrmann No conozco un JEP o JSR, pero el cambio parece una solución para estar en línea con el significado de "concreto", es decir, comparar con JLS§9.4 : " Los métodos predeterminados son distintos de los métodos concretos (§8.4. 3.1), que se declaran en clases. ", Que nunca cambió.
Holger
2
@StephanHerrmann sí, parece que seleccionar un candidato de los métodos equivalentes de anulación se ha vuelto más complicado y sería interesante conocer la razón detrás de esto, pero no es relevante para la pregunta en cuestión. Debería haber otro documento que explique los cambios y las motivaciones. En el pasado, existía, pero con esta política de "una nueva versión cada medio año", mantener la calidad parece imposible ...
Holger
2

El código donde Eclipse implementa JLS §15.12.2.5 no encuentra ninguno de los métodos como más específico que el otro, incluso para el caso de la lambda escrita explícitamente.

Así que, idealmente, Eclipse se detendría aquí e informaría de ambigüedad. Desafortunadamente, la implementación de la resolución de sobrecarga tiene un código no trivial además de implementar JLS. Según tengo entendido, este código (que data del momento en que Java 5 era nuevo) debe mantenerse para llenar algunos vacíos en JLS.

He presentado https://bugs.eclipse.org/562538 para rastrear esto.

Independientemente de este error en particular, solo puedo desaconsejar este estilo de código. La sobrecarga es buena para una buena cantidad de sorpresas en Java, multiplicada por la inferencia de tipo lambda, la complejidad es bastante desproporcionada con la ganancia percibida.

Stephan Herrmann
fuente
Gracias. Ya había registrado bugs.eclipse.org/bugs/show_bug.cgi?id=562507 , tal vez podría ayudar a vincularlos o cerrar uno como duplicado ...
ernest_k