Seleção de assinatura de método para expressão lambda com vários tipos de destino correspondentes

11

Eu estava respondendo uma pergunta e me deparei com um cenário que não consigo explicar. Considere 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
    }
}

Eu não entendo por que digitar explicitamente o parâmetro do lambda (A a) -> aList.add(a)faz o código compilar. Além disso, por que ele está vinculado à sobrecarga em Iterablevez da que está dentro CustomIterable?
Existe alguma explicação para isso ou um link para a seção relevante da especificação?

Nota: iterable.forEach((A a) -> aList.add(a));somente compila quando CustomIterable<T>estende Iterable<T>(sobrecarregar os métodos de forma plana CustomIterableresulta em erro ambíguo)


Obtendo isso em ambos:

  • openjdk versão "13.0.2" 2020-01-14
    Compilador Eclipse
  • openjdk versão "1.8.0_232"
    Compilador Eclipse

Edit : O código acima falha ao compilar na construção com o maven, enquanto o Eclipse compila a última linha do código com êxito.

ernest_k
fonte
3
Nenhum dos três compila no Java 8. Agora não tenho certeza se este é um bug corrigido em uma versão mais recente ou um bug / recurso que foi introduzido ... Você provavelmente deve especificar a versão Java
Sweeper
@ Sweeper Inicialmente consegui isso usando o jdk-13. O teste subsequente no java 8 (jdk8u232) mostra os mesmos erros. Não sei por que o último também não é compilado em sua máquina.
ernest_k 22/04
Também não é possível reproduzir em dois compiladores online ( 1 , 2 ). Estou usando 1.8.0_221 na minha máquina. Isso está ficando cada vez mais estranho ...
Vassoura
11
@ernest_k O Eclipse tem sua própria implementação de compilador. Essa poderia ser uma informação crucial para a pergunta. Além disso, o fato de um maven limpo criar erros na última linha também deve ser destacado na pergunta em minha opinião. Por outro lado, sobre a questão vinculada, a suposição de que o OP também esteja usando o Eclipse poderia ser esclarecida, uma vez que o código não é reproduzível.
Naman
3
Embora eu compreenda o valor intelectual da questão, só posso desaconselhar a criação de métodos sobrecarregados que diferem apenas nas interfaces funcionais e na expectativa de que ele possa ser chamado com segurança na transmissão de um lambda. Não acredito que a combinação de inferência e sobrecarga do tipo lambda seja algo que o programador médio chegará perto de entender. É uma equação com muitas variáveis ​​que os usuários não controlam. EVITAR ISSO, por favor :)
Stephan Herrmann

Respostas:

8

TL; DR, este é um bug do compilador.

Não há uma regra que daria precedência a um método aplicável específico quando ele é herdado ou um método padrão. Curiosamente, quando eu mudo o código para

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
}

a iterable.forEach((A a) -> aList.add(a));instrução produz um erro no Eclipse.

Como nenhuma propriedade do forEach(Consumer<? super T) c)método da Iterable<T>interface foi alterada ao declarar outra sobrecarga, a decisão do Eclipse de selecionar esse método não pode ser (consistentemente) baseada em nenhuma propriedade do método. Ainda é o único método herdado, ainda é o único defaultmétodo, ainda é o único método JDK e assim por diante. Nenhuma dessas propriedades deve afetar a seleção do método de qualquer maneira.

Observe que alterar a declaração para

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

também produz um erro "ambíguo"; portanto, o número de métodos sobrecarregados aplicáveis ​​também não importa, mesmo quando há apenas dois candidatos, não há preferência geral pelos defaultmétodos.

Até agora, o problema parece surgir quando há dois métodos aplicáveis ​​e um defaultmétodo e uma relação de herança estão envolvidos, mas esse não é o lugar certo para aprofundar.


Mas é compreensível que as construções do seu exemplo possam ser tratadas por diferentes códigos de implementação no compilador, um exibindo um bug enquanto o outro não.
a -> aList.add(a)é uma expressão lambda de tipo implícito , que não pode ser usada para a resolução de sobrecarga. Por outro lado, (A a) -> aList.add(a)é uma expressão lambda de tipo explícito que pode ser usada para selecionar um método correspondente dos métodos sobrecarregados, mas não ajuda aqui (não deve ajudar aqui), pois todos os métodos têm tipos de parâmetros com exatamente a mesma assinatura funcional .

Como um contra-exemplo, com

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

as assinaturas funcionais diferem e o uso de uma expressão lambda do tipo explicitamente pode realmente ajudar a selecionar o método certo, enquanto a expressão lambda implicitamente digitada não ajuda, portanto forEach(s -> s.isEmpty())produz um erro do compilador. E todos os compiladores Java concordam com isso.

Observe que aList::addé uma referência de método ambígua, pois o addmétodo também está sobrecarregado, por isso também não pode ajudar na seleção de um método, mas as referências a métodos podem ser processadas por código diferente de qualquer maneira. Mudar para um não ambíguo aList::containsou mudar Listpara Collection, para tornar addnão ambíguo, não alterou o resultado na minha instalação do Eclipse (eu usei 2019-06).

Holger
fonte
11
@ howlger seu comentário não faz sentido. O método padrão é o método herdado e o método está sobrecarregado. Não existe outro método. O fato de o método herdado ser um defaultmétodo é apenas um ponto adicional. Minha resposta já mostra um exemplo em que o Eclipse não dá precedência ao método padrão.
Holger
11
@ howlger, existe uma diferença fundamental em nossos comportamentos. Você só descobriu que a remoção defaultaltera o resultado e imediatamente assume que encontrou o motivo do comportamento observado. Você é tão confiante demais que chama outras respostas erradas, apesar de elas nem contradizerem. Como você está projetando seu próprio comportamento em detrimento de outro , nunca reivindiquei que a herança fosse a razão . Eu provei que não é. Eu demonstrei que o comportamento é inconsistente, pois o Eclipse seleciona o método específico em um cenário, mas não em outro, onde existem três sobrecargas.
Holger
11
@ howlger além disso, eu já nomeei outro cenário no final deste comentário , crie uma interface, sem herança, dois métodos, um com defaulto outro abstract, com dois argumentos de consumidor e experimente. O Eclipse diz corretamente que é ambíguo, apesar de um ser um defaultmétodo. Aparentemente, a herança ainda é relevante para esse bug do Eclipse, mas, diferentemente de você, não estou enlouquecendo e chamo outras respostas de erradas, apenas porque elas não analisaram o bug por completo. Esse não é o nosso trabalho aqui.
Holger
11
@ howlger não, o ponto é que isso é um bug. A maioria dos leitores nem se importa com os detalhes. O Eclipse pode rolar dados toda vez que seleciona um método, não importa. O Eclipse não deve selecionar um método quando é ambíguo, portanto, não importa por que ele seleciona um. Esta resposta prova que o comportamento é inconsistente, o que já é suficiente para indicar fortemente que é um bug. Não é necessário apontar para a linha no código-fonte do Eclipse onde as coisas dão errado. Esse não é o objetivo do Stackoverflow. Talvez você esteja confundindo o Stackoverflow com o rastreador de erros do Eclipse.
Holger
11
@ howlger, você está novamente afirmando incorretamente que eu fiz uma declaração (errada) do porquê o Eclipse fez essa escolha errada. Novamente, não fiz, pois o eclipse não deveria fazer nenhuma escolha. O método é ambíguo. Ponto. A razão pela qual usei o termo " herdado " é porque era necessário distinguir métodos nomeados de forma idêntica. Eu poderia ter dito “ método padrão ” em vez disso, sem alterar a lógica. Mais corretamente, eu deveria ter usado a frase " o próprio método que o Eclipse selecionou incorretamente por qualquer motivo ". Você pode usar qualquer uma das três frases alternadamente, e a lógica não muda.
Holger
2

O compilador Eclipse resolve corretamente o defaultmétodo , pois esse é o método mais específico de acordo com a Java Language Specification 15.12.2.5 :

Se exatamente um dos métodos maximamente específicos é concreto (ou seja, não abstractou padrão), é o método mais específico.

javac(usado por Maven e IntelliJ por padrão) informa que a chamada do método é ambígua aqui. Mas, de acordo com a Java Language Specification, não é ambíguo, pois um dos dois métodos é o método mais específico aqui.

Expressões lambda de tipo implícito são tratadas de maneira diferente das expressões lambda de tipo explícito em Java. Digitar implicitamente em contraste com expressões lambda explicitamente digitadas passa pela primeira fase para identificar os métodos de chamada estrita (consulte Java Language Specification jls-15.12.2.2 , primeiro ponto). Portanto, a chamada de método aqui é ambígua para expressões lambda implicitamente digitadas .

No seu caso, a solução alternativa para esse javacerro é especificar o tipo da interface funcional em vez de usar uma expressão lambda explicitamente digitada da seguinte maneira:

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

ou

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

Aqui está o seu exemplo ainda mais minimizado para testes:

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)
    }

}
howlger
fonte
4
Você perdeu a pré-condição imediatamente antes da sentença citada: “ Se todos os métodos maximamente específicos tiverem assinaturas equivalentes a substituição ” É claro que dois métodos cujos argumentos são interfaces totalmente independentes não terão assinaturas equivalentes a substituição. Além disso, isso não explicaria por que o Eclipse para de selecionar o defaultmétodo quando há três métodos candidatos ou quando os dois métodos são declarados na mesma interface.
Holger
11
@Holger Sua resposta afirma: "Não existe uma regra que daria precedência a um método aplicável específico quando ele é herdado ou um método padrão". Entendi corretamente que você está dizendo que a pré-condição para essa regra inexistente não se aplica aqui? Observe que o parâmetro aqui é uma interface funcional (consulte JLS 9.8).
howlger 27/04
11
Você rasgou uma frase fora de contexto. A frase descreve a seleção de métodos equivalentes de substituição , em outras palavras, uma escolha entre declarações que todos invocariam o mesmo método em tempo de execução, porque haverá apenas um método concreto na classe concreta. Isso é irrelevante para o caso de métodos distintos como forEach(Consumer)e forEach(Consumer2)que nunca podem terminar no mesmo método de implementação.
Holger
2
@StephanHerrmann Eu não conheço um JEP ou JSR, mas a mudança parece uma correção para estar alinhada com o significado de “concreto”, ou seja, compare com JLS§9.4 : “ Métodos padrão são distintos dos métodos concretos (§8.4. 3.1), que são declarados em classes. ”, Que nunca mudou.
Holger
2
@StephanHerrmann sim, parece que selecionar um candidato a partir de métodos equivalentes a substituições se tornou mais complicado e seria interessante conhecer a lógica por trás disso, mas não é relevante para a questão em questão. Deveria haver outro documento explicando mudanças e motivações. No passado, havia, mas com essa política "uma nova versão a cada ½ ano", manter a qualidade parece impossível ...
Holger
2

O código no qual o Eclipse implementa o JLS §15.12.2.5 não encontra nenhum método como mais específico que o outro, mesmo no caso da lambda explicitamente digitada.

Então, idealmente, o Eclipse para aqui e informa ambigüidade. Infelizmente, a implementação da resolução de sobrecarga possui código não trivial, além de implementar o JLS. Pelo meu entendimento, esse código (que data da época em que o Java 5 era novo) deve ser mantido para preencher algumas lacunas no JLS.

Eu enviei https://bugs.eclipse.org/562538 para rastrear isso.

Independente desse bug em particular, só posso aconselhar fortemente contra esse estilo de código. Sobrecarregar é bom para um bom número de surpresas em Java, multiplicado pela inferência do tipo lambda, a complexidade é desproporcional ao ganho percebido.

Stephan Herrmann
fonte
Obrigado. Já havia registrado bugs.eclipse.org/bugs/show_bug.cgi?id=562507 , talvez você possa ajudar a vinculá-los ou fechar um como duplicado ...
ernest_k 28/04