C'è un vantaggio prestazionale nell'usare la sintassi di riferimento del metodo invece della sintassi lambda in Java 8?


56

I riferimenti ai metodi saltano il sovraccarico del wrapper lambda? Potrebbero in futuro?

Secondo il tutorial Java sui riferimenti ai metodi :

A volte ... un'espressione lambda non fa altro che chiamare un metodo esistente. In questi casi, è spesso più chiaro fare riferimento al metodo esistente per nome. I riferimenti ai metodi ti consentono di farlo; sono espressioni lambda compatte e di facile lettura per i metodi che hanno già un nome.

Preferisco la sintassi lambda alla sintassi di riferimento del metodo per diversi motivi:

Le lambda sono più chiare

Nonostante le affermazioni di Oracle, trovo che la sintassi lambda sia più facile da leggere rispetto alla scorciatoia del riferimento al metodo dell'oggetto perché la sintassi del riferimento al metodo è ambigua:

Bar::foo

Stai chiamando un metodo statico a argomento singolo sulla classe di x e passando x?

x -> Bar.foo(x)

O stai chiamando un metodo di istanza zero-topic su x?

x -> x.foo()

La sintassi di riferimento del metodo potrebbe sostituire una delle due. Nasconde ciò che il tuo codice sta effettivamente facendo.

Le lambda sono più sicure

Se si fa riferimento a Bar :: foo come metodo di classe e Bar successivamente aggiunge un metodo di istanza con lo stesso nome (o viceversa), il codice non verrà più compilato.

Puoi usare lambda in modo coerente

Puoi racchiudere qualsiasi funzione in una lambda, in modo da poter utilizzare la stessa sintassi in modo coerente ovunque. La sintassi di riferimento del metodo non funziona sui metodi che accettano o restituiscono array primitivi, generano eccezioni verificate o hanno lo stesso nome di metodo usato come istanza e metodo statico (poiché la sintassi di riferimento del metodo è ambigua su quale metodo sarebbe chiamato) . Non funzionano quando hai metodi sovraccaricati con lo stesso numero di argomenti, ma non dovresti farlo comunque (vedi l'articolo 41 di Josh Bloch), quindi non possiamo tenerlo contro i riferimenti al metodo.

Conclusione

Se non ci sono penalità di prestazione per farlo, sono tentato di disattivare l'avviso nel mio IDE e utilizzare la sintassi lambda in modo coerente senza aggiungere il riferimento al metodo occasionale nel mio codice.

PS

Né qui né lì, ma nei miei sogni, i riferimenti al metodo dell'oggetto sembrano più simili a questo e applicano invoke-dynamic contro il metodo direttamente sull'oggetto senza un wrapper lambda:

_.foo()

2
Questo non risponde alla tua domanda, ma ci sono altri motivi per usare le chiusure. A mio avviso, molti di loro superano di gran lunga il piccolo sovraccarico di una chiamata di funzione aggiuntiva.

9
Penso che ti preoccupi delle prestazioni prematuramente. A mio avviso, le prestazioni dovrebbero essere una considerazione secondaria per l'utilizzo di un metodo di riferimento; si tratta solo di esprimere il tuo intento. Se hai bisogno di passare una funzione da qualche parte, perché non passare semplicemente la funzione invece di un'altra funzione che la delega? Mi sembra strano; come se non comprassi del tutto che le funzioni sono cose di prima classe, hanno bisogno di un accompagnatore anonimo per spostarle. Ma per rispondere alla tua domanda più direttamente, non sarei sorpreso se un riferimento al metodo può essere implementato come una chiamata statica.
Doval,

7
.map(_.acceptValue()): Se non sbaglio, assomiglia molto alla sintassi della Scala. Forse stai solo cercando di usare la lingua sbagliata.
Giorgio,

2
"La sintassi di riferimento del metodo non funziona sui metodi che restituiscono il vuoto" - In che modo? Sto passando System.out::printlnper forEach()tutto il tempo ...?
Lukas Eder,

3
"La sintassi di riferimento del metodo non funziona sui metodi che accettano o restituiscono array primitivi, generano eccezioni verificate ...." Non è corretto. È possibile utilizzare riferimenti a metodi e espressioni lambda per tali casi, a condizione che l'interfaccia funzionale in questione lo consenta.
Stuart Marks,

Risposte:


12

In molti scenari, penso che lambda e il metodo di riferimento siano equivalenti. Ma lambda avvolgerà il target di invocazione in base al tipo di interfaccia dichiarante.

Per esempio

public class InvokeTest {

    private static void invoke(final Runnable r) {
        r.run();
    }

    private static void target() {
        new Exception().printStackTrace();
    }

    @Test
    public void lambda() throws Exception {
        invoke(() -> target());
    }

    @Test
    public void methodReference() throws Exception {
        invoke(InvokeTest::target);
    }
}

Vedrai la console emettere lo stacktrace.

In lambda(), il metodo di chiamata target()è lambda$lambda$0(InvokeTest.java:20), che ha informazioni sulla linea tracciabile. Ovviamente, questo è il lambda che scrivi, il compilatore genera un metodo anonimo per te. E quindi, il chiamante del metodo lambda è qualcosa di simile InvokeTest$$Lambda$2/1617791695.run(Unknown Source), cioè la invokedynamicchiamata in JVM, significa che la chiamata è collegata al metodo generato .

In methodReference(), la chiamata del metodo target()è direttamente la InvokeTest$$Lambda$1/758529971.run(Unknown Source), significa che la chiamata è direttamente collegata al InvokeTest::targetmetodo .

Conclusione

Soprattutto, il confronto con il metodo di riferimento, l'uso dell'espressione lambda causerà solo un'altra chiamata di metodo al metodo di generazione da lambda.


1
La risposta qui sotto sul metafactory è un po 'migliore. Ho aggiunto alcuni commenti utili che sono rilevanti per esso (vale a dire come implementazioni come Oracle / OpenJDK usano ASM per generare gli oggetti della classe wrapper necessari per creare istanze di lambda / method.
Ajax,

29

Riguarda il metafactory

Innanzitutto, la maggior parte dei riferimenti ai metodi non necessita di desugaring da parte della metafora lambda, ma vengono semplicemente utilizzati come metodo di riferimento. Nella sezione "Zuccheraggio del corpo Lambda" dell'articolo Traduzione delle espressioni lambda ("TLE"):

A parità di condizioni, i metodi privati ​​sono preferibili ai metodi non privati, statici preferibili ai metodi di istanza, è meglio se i corpi lambda vengono scruginati nella classe più interna in cui appare l'espressione lambda, le firme devono corrispondere alla firma del corpo della lambda, extra gli argomenti dovrebbero essere anteposti nella parte anteriore dell'elenco degli argomenti per i valori acquisiti e non desiderebbero affatto i riferimenti al metodo. Tuttavia, ci sono casi di eccezione in cui potremmo dover discostarci da questa strategia di base.

Ciò è ulteriormente evidenziato più in basso in "The Lambda Metafactory" di TLE:

metaFactory(MethodHandles.Lookup caller, // provided by VM
            String invokedName,          // provided by VM
            MethodType invokedType,      // provided by VM
            MethodHandle descriptor,     // lambda descriptor
            MethodHandle impl)           // lambda body

L' implargomento identifica il metodo lambda, un corpo lambda desugared o il metodo indicato in un riferimento di metodo.

I riferimenti a un Integer::summetodo di istanza statico ( ) o illimitato ( Integer::intValue) sono i "più semplici" o i più "convenienti", nel senso che possono essere gestiti in modo ottimale da una variante metafattiva "a percorso rapido" senza desugaring . Questo vantaggio è utilmente sottolineato nelle "varianti metafattive" di TLE:

Eliminando gli argomenti in cui non sono necessari, i file di classe diventano più piccoli. E l'opzione di percorso rapido abbassa la barra affinché la VM possa intrinsecare l'operazione di conversione lambda, consentendole di essere trattata come un'operazione di "boxing" e facilitando le ottimizzazioni di unbox.

Naturalmente, un riferimento al metodo ( obj::myMethod) che cattura l'istanza deve fornire l'istanza limitata come argomento all'handle del metodo per l'invocazione, il che può significare la necessità di desugaring usando i metodi "bridge".

Conclusione

Non sono esattamente sicuro di quale sia il "wrapper" lambda a cui stai accennando, ma anche se il risultato finale dell'utilizzo dei lambda definiti dall'utente o dei metodi di riferimento sono gli stessi, il modo in cui viene raggiunto sembra essere abbastanza diverso, e può essere diverso in futuro se non è così ora. Quindi, suppongo sia più probabile che i riferimenti al metodo possano essere gestiti in modo ottimale dal metafactory.


1
Questa è la risposta più pertinente, in quanto tocca i macchinari interni necessari per creare un lambda. Come qualcuno che ha effettivamente eseguito il debug del processo di creazione lambda (al fine di capire come generare ID stabili per un dato riferimento lambda / metodo in modo che possano essere rimossi dalle mappe), ho trovato abbastanza sorprendente quanto lavoro sia stato fatto per creare un istanza di un lambda. Oracle / OpenJDK usano ASM per generare dinamicamente classi che, se necessario, si chiudono su qualsiasi variabile a cui fa riferimento il tuo lambda (o il qualificatore di istanza di un riferimento al metodo) ...
Ajax

1
... Oltre a chiudere le variabili, lambda in particolare crea anche un metodo statico che viene iniettato nella classe dichiarante in modo che la lambda creata abbia qualcosa da chiamare per mappare le funzioni che si desidera richiamare. Un riferimento al metodo con un qualificatore di istanza prenderà quell'istanza come parametro per questo metodo; Non ho testato se un riferimento this :: method in una classe privata salta questa chiusura, ma ho testato che i metodi statici in realtà saltano il metodo intermedio (e creano semplicemente un oggetto per conformarsi alla destinazione di invocazione sul chiama il sito).
Ajax,

1
Presumibilmente anche un metodo privato non avrà bisogno dell'invio virtuale, mentre qualcosa di più pubblico dovrà chiudere l'istanza (tramite un metodo statico generato) in modo che possa invocare virtuale sull'istanza effettiva per assicurarsi che le notifiche abbiano la precedenza. TL; DR: i riferimenti ai metodi si chiudono su meno argomenti, quindi perdono meno e richiedono meno generazione di codice dinamico.
Ajax,

3
Ultimo commento spammy ... La generazione di classe dinamica è piuttosto pesante. Probabilmente è ancora meglio che caricare una classe anonima nel programma di caricamento classi (poiché le parti più pesanti vengono eseguite una sola volta), ma rimarrai davvero sorpreso da quanto lavoro ci vorrà per creare un'istanza lambda. Sarebbe interessante vedere veri e propri parametri di riferimento di lambda rispetto a riferimenti a metodi rispetto a classi anonime. Si noti che due lambda o riferimenti a metodi che fanno riferimento alle stesse cose, ma in luoghi diversi creano classi diverse in fase di esecuzione (anche all'interno dello stesso metodo, subito dopo l'altro).
Ajax,


0

Vi è una conseguenza abbastanza seria quando si usano espressioni lambda che possono influire sulle prestazioni.

Quando si dichiara un'espressione lambda, si sta creando una chiusura sopra l'ambito locale.

Cosa significa e in che modo influisce sulle prestazioni?

Beh, sono contento che tu l'abbia chiesto. Significa che ognuna di quelle piccole espressioni lambda è una piccola classe interna anonima e ciò significa che porta con sé un riferimento a tutte le variabili che si trovano nello stesso ambito dell'espressione lambda.

Questo significa anche thisriferimento all'istanza dell'oggetto e tutti i suoi campi. A seconda della tempistica dell'effettiva invocazione della lambda, ciò può comportare una perdita di risorse piuttosto significativa poiché il garbage collector non è in grado di lasciar andare questi riferimenti finché l'oggetto che tiene in una lambda è ancora vivo ...


Lo stesso vale per i riferimenti a metodi non statici.
Ajax,

12
Le lambda di Java non sono rigorosamente chiusure. Né sono classi interne anonime. NON riportano un riferimento a tutte le variabili nell'ambito in cui sono dichiarate. Portano SOLO un riferimento alle variabili a cui effettivamente fanno riferimento. Questo vale anche per l' thisoggetto a cui potrebbe essere fatto riferimento. Un lambda, quindi, non perde risorse semplicemente avendo spazio per loro; si aggrappa solo agli oggetti di cui ha bisogno. D'altra parte, una classe interna anonima può perdere risorse, ma non sempre lo fa. Vedi questo codice per un esempio: a.blmq.us/2mmrL6v
squid314

Quella risposta è semplicemente sbagliata
Stefan Reich,

Grazie @StefanReich, è stato molto utile!
Roland Tepp,
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.