Selezione della firma del metodo per l'espressione lambda con più tipi di destinazione corrispondenti


11

Stavo rispondendo a una domanda e mi sono imbattuto in uno scenario che non posso spiegare. Considera questo codice:

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

Non capisco perché digitando esplicitamente il parametro del lambda si (A a) -> aList.add(a)compili il codice. Inoltre, perché si collega al sovraccarico Iterablepiuttosto che a quello in CustomIterable?
C'è qualche spiegazione a questo o un link alla sezione pertinente delle specifiche?

Nota: iterable.forEach((A a) -> aList.add(a));compila solo quando si CustomIterable<T>estende Iterable<T>(sovraccaricando completamente i metodi nei CustomIterablerisultati nell'errore ambiguo)


Ottenere questo su entrambi:


  • compilatore Eclipse versione "13.0.2" 2020-01-14 di openjdk

  • compilatore Eclipse versione openjdk "1.8.0_232"

Modifica : il codice sopra non riesce a compilare su building with maven mentre Eclipse compila correttamente l'ultima riga di codice.


3
Nessuna delle tre compilazioni su Java 8. Ora non sono sicuro che si tratti di un bug che è stato corretto in una versione più recente o di un bug / funzionalità che è stato introdotto ... Probabilmente dovresti specificare la versione di Java
Sweeper

@Sweeper Inizialmente ho ottenuto questo usando jdk-13. I test successivi in ​​java 8 (jdk8u232) mostrano gli stessi errori. Non sono sicuro del motivo per cui l'ultimo non viene compilato sul tuo computer.
ernest_k

Non è possibile riprodurre su due compilatori online ( 1 , 2 ). Sto usando 1.8.0_221 sulla mia macchina. Questo sta diventando sempre più strano ...
Sweeper

1
@ernest_k Eclipse ha una propria implementazione di compilatore. Potrebbe essere un'informazione cruciale per la domanda. Inoltre, a mio avviso , il fatto che un'arma pulita crei errori anche nell'ultima riga dovrebbe essere evidenziato nella domanda. D'altra parte, riguardo alla domanda collegata, si potrebbe chiarire che anche il PO utilizza Eclipse, poiché il codice non è riproducibile.
Naman,

3
Mentre comprendo quel valore intellettuale della domanda, posso solo sconsigliare di creare metodi sovraccarichi che differiscono solo nelle interfacce funzionali e mi aspetto che possa essere tranquillamente chiamato passando una lambda. Non credo che la combinazione di inferenza di tipo lambda e sovraccarico sia qualcosa che il programmatore medio si avvicinerà mai alla comprensione. È un'equazione con moltissime variabili che gli utenti non controllano. EVITARE QUESTO, per favore :)
Stephan Herrmann il

Risposte:


8

TL; DR, questo è un bug del compilatore.

Non esiste una regola che dia la precedenza a un particolare metodo applicabile quando viene ereditato o un metodo predefinito. È interessante notare che quando cambio il codice in

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
}

l' iterable.forEach((A a) -> aList.add(a));istruzione produce un errore in Eclipse.

Poiché nessuna proprietà del forEach(Consumer<? super T) c)metodo Iterable<T>dall'interfaccia è cambiata quando si dichiara un altro sovraccarico, la decisione di Eclipse di selezionare questo metodo non può essere (coerentemente) basata su alcuna proprietà del metodo. È ancora l'unico metodo ereditato, ancora l'unico defaultmetodo, ancora l'unico metodo JDK e così via. Nessuna di queste proprietà dovrebbe comunque influire sulla selezione del metodo.

Si noti che cambiando la dichiarazione in

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

produce anche un errore "ambiguo", quindi anche il numero di metodi sovraccarichi applicabili non ha importanza, anche quando ci sono solo due candidati, non esiste una preferenza generale verso i defaultmetodi.

Finora, il problema sembra apparire quando ci sono due metodi applicabili e defaultsono coinvolti un metodo e una relazione di ereditarietà, ma questo non è il posto giusto per approfondire.


Ma è comprensibile che i costrutti del tuo esempio possano essere gestiti da diversi codici di implementazione nel compilatore, uno che mostra un bug mentre l'altro no.
a -> aList.add(a)è un'espressione lambda tipizzata in modo implicito , che non può essere utilizzata per la risoluzione del sovraccarico. Al contrario, (A a) -> aList.add(a)è un un'espressione lambda tipizzata in modo esplicito che può essere utilizzata per selezionare un metodo corrispondente dai metodi sovraccarichi, ma non aiuta qui (non dovrebbe aiutare qui), poiché tutti i metodi hanno tipi di parametri con esattamente la stessa firma funzionale .

Come controesempio, con

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

le firme funzionali differiscono e l'uso di un'espressione lambda di tipo esplicito può effettivamente aiutare a selezionare il metodo giusto mentre l'espressione lambda tipicamente implicita non aiuta, quindi forEach(s -> s.isEmpty()) produce un errore del compilatore. E tutti i compilatori Java sono d'accordo su questo.

Si noti che si aList::addtratta di un riferimento al metodo ambiguo, poiché anche il addmetodo è sovraccarico, quindi non può essere di aiuto nella selezione di un metodo, ma i riferimenti al metodo potrebbero comunque essere elaborati da un codice diverso. Passare a un non ambiguo aList::containso passare Lista Collection, per renderlo addnon ambiguo, non ha modificato il risultato nella mia installazione di Eclipse (ho usato 2019-06).


1
@howlger il tuo commento non ha senso. Il metodo predefinito è il metodo ereditato e il metodo è sovraccarico. Non esiste altro metodo. Il fatto che il metodo ereditato sia un defaultmetodo è solo un punto aggiuntivo. La mia risposta mostra già un esempio in cui Eclipse non ha la precedenza sul metodo predefinito.
Holger,

1
@howlger c'è una differenza fondamentale nei nostri comportamenti. Hai scoperto solo che la rimozione defaultmodifica il risultato e supponi immediatamente di aver trovato il motivo del comportamento osservato. Sei così fiducioso al riguardo da chiamare erroneamente altre risposte, nonostante non siano nemmeno in contraddizione. Dato che stai proiettando il tuo comportamento sugli altri, non ho mai sostenuto che l'ereditarietà fosse la ragione . Ho dimostrato che non lo è. Ho dimostrato che il comportamento è incoerente, poiché Eclipse seleziona il metodo particolare in uno scenario ma non in un altro, in cui esistono tre sovraccarichi.
Holger,

1
@howlger oltre a questo, ho già nominato un altro scenario alla fine di questo commento , creare un'interfaccia, nessuna eredità, due metodi, uno con defaultl'altro abstract, con due argomenti simili ai consumatori e provarlo. Eclipse afferma correttamente che è ambiguo, nonostante uno sia un defaultmetodo. Apparentemente l'eredità è ancora rilevante per questo bug di Eclipse, ma a differenza di te, non sto impazzendo e definendo errate altre risposte, solo perché non hanno analizzato il bug nella sua interezza. Questo non è il nostro lavoro qui.
Holger,

1
@howlger no, il punto è che si tratta di un bug. La maggior parte dei lettori non si preoccuperà nemmeno dei dettagli. Eclipse può tirare i dadi ogni volta che seleziona un metodo, non importa. Eclipse non dovrebbe selezionare un metodo quando è ambiguo, quindi non importa perché ne selezioni uno. Questa risposta dimostra che il comportamento è incoerente, il che è già sufficiente per indicare con forza che si tratta di un bug. Non è necessario indicare la riga nel codice sorgente di Eclipse in cui le cose vanno male. Questo non è lo scopo di StackOverflow. Forse stai confondendo Stackoverflow con il tracker dei bug di Eclipse.
Holger,

1
@howlger stai di nuovo affermando erroneamente che ho fatto una dichiarazione (sbagliata) del perché Eclipse abbia fatto quella scelta sbagliata. Ancora una volta, non l'ho fatto, poiché l'eclissi non dovrebbe fare alcuna scelta. Il metodo è ambiguo. Punto. Il motivo per cui ho usato il termine " ereditato " è perché era necessario distinguere metodi identici. Avrei potuto invece dire " metodo predefinito ", senza cambiare la logica. Più correttamente, avrei dovuto usare la frase " il metodo Eclipse stesso selezionato in modo errato per qualsiasi motivo ". È possibile utilizzare una delle tre frasi in modo intercambiabile e la logica non cambia.
Holger,

2

Il compilatore Eclipse si risolve correttamente nel defaultmetodo , poiché questo è il metodo più specifico in base alla specifica del linguaggio Java 15.12.2.5 :

Se esattamente uno dei metodi massimamente specifici è concreto (cioè, non abstracto predefinito), è il metodo più specifico.

javac(utilizzato da Maven e IntelliJ per impostazione predefinita) indica che la chiamata al metodo è ambigua qui. Ma secondo Java Language Specification non è ambiguo poiché uno dei due metodi è il metodo più specifico qui.

Le espressioni lambda tipizzate in modo implicito sono gestite in modo diverso rispetto alle espressioni lambda tipizzate in modo esplicito in Java. Digitato implicitamente in contrasto con le espressioni lambda esplicitamente digitate, passa attraverso la prima fase per identificare i metodi di invocazione rigorosa (vedi Java Language Specification jls-15.12.2.2 , primo punto). Quindi, la chiamata del metodo qui è ambigua per i caratteri impliciti le espressioni lambda .

Nel tuo caso, la soluzione alternativa per questo javacerrore consiste nello specificare il tipo di interfaccia funzionale invece di utilizzare un'espressione lambda esplicitamente digitata come segue:

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

o

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

Ecco il tuo esempio ulteriormente minimizzato per i test:

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

}

4
Hai perso la condizione preliminare proprio prima della frase citata: " Se tutti i metodi massimamente specifici hanno firme equivalenti a override " Naturalmente, due metodi i cui argomenti sono interfacce completamente indipendenti non hanno firme equivalenti a override. Oltre a ciò, questo non spiegherebbe perché Eclipse smette di selezionare il defaultmetodo quando ci sono tre metodi candidati o quando entrambi i metodi sono dichiarati nella stessa interfaccia.
Holger,

1
@Holger La tua risposta afferma: "Non esiste alcuna regola che dia la precedenza a un particolare metodo applicabile quando viene ereditato o un metodo predefinito". Ti capisco correttamente che stai dicendo che il prerequisito per questa regola inesistente non si applica qui? Si noti che il parametro qui è un'interfaccia funzionale (vedere JLS 9.8).
ululato il

1
Hai strappato una frase dal contesto. La frase descrive la selezione di metodi equivalenti all'override, in altre parole, una scelta tra dichiarazioni che tutte invocherebbero lo stesso metodo in fase di esecuzione, perché ci sarà un solo metodo concreto nella classe concrete. Ciò è irrilevante nel caso di metodi distinti come forEach(Consumer)e forEach(Consumer2)che non possono mai finire con lo stesso metodo di implementazione.
Holger,

2
@StephanHerrmann Non conosco un JEP o un JSR, ma la modifica sembra una correzione per essere in linea con il significato di "concreto", ovvero confrontare con JLS§9.4 : "I metodi predefiniti sono distinti dai metodi concreti (§8.4. 3.1), che sono dichiarati in classi. ", Che non è mai cambiato.
Holger

2
@StephanHerrmann sì, sembra che selezionare un candidato tra metodi equivalenti alla sostituzione sia diventato più complicato e sarebbe interessante conoscere la logica alla base, ma non è rilevante per la domanda in corso. Dovrebbe esserci un altro documento che spiega i cambiamenti e le motivazioni. In passato, c'era, ma con questa politica "una nuova versione ogni ½ anno", mantenere la qualità sembra impossibile ...
Holger

2

Il codice in cui Eclipse implementa JLS §15.12.2.5 non trova nessuno dei due metodi più specifico dell'altro, anche nel caso del lambda esplicitamente digitato.

Quindi idealmente Eclipse si fermerebbe qui e riferirebbe ambiguità. Sfortunatamente, l'implementazione della risoluzione del sovraccarico ha un codice non banale oltre all'implementazione di JLS. Da quanto ho capito, questo codice (che risale al momento in cui Java 5 era nuovo) deve essere mantenuto per colmare alcune lacune in JLS.

Ho archiviato https://bugs.eclipse.org/562538 per tracciare questo.

Indipendentemente da questo particolare bug, posso solo sconsigliare vivamente questo stile di codice. Il sovraccarico è buono per un buon numero di sorprese in Java, moltiplicato per l'inferenza di tipo lambda, la complessità è abbastanza sproporzionata rispetto al guadagno percepito.


Grazie. Avevo già registrato bugs.eclipse.org/bugs/show_bug.cgi?id=562507 , forse potresti aiutare a collegarli o chiuderne uno come duplicato ...
ernest_k
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.