Le espressioni lambda hanno un uso diverso dal salvataggio di righe di codice?


120

Le espressioni lambda hanno un uso diverso dal salvataggio di righe di codice?

Ci sono funzioni speciali fornite da lambda che hanno risolto problemi che non erano facili da risolvere? L'utilizzo tipico che ho visto è che invece di scrivere questo:

Comparator<Developer> byName = new Comparator<Developer>() {
  @Override
  public int compare(Developer o1, Developer o2) {
    return o1.getName().compareTo(o2.getName());
  }
};

Possiamo usare un'espressione lambda per abbreviare il codice:

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());

27
Nota che può essere anche più breve: (o1, o2) -> o1.getName().compareTo(o2.getName())
Thorbjørn Ravn Andersen

88
o ancora più breve:Comparator.comparing(Developer::getName)
Thomas Kläger

31
Non sottovaluterei questo vantaggio. Quando le cose sono molto più facili, è più probabile che tu faccia le cose nel modo "giusto". C'è spesso una certa tensione tra lo scrivere codice che è più gestibile a lungo termine e quello più facile da annotare a breve termine. Lambda riduce quella tensione e così ti aiuta a scrivere codice più pulito.
yshavit

5
Le lambda sono chiusure, il che rende il risparmio di spazio ancora maggiore, poiché ora non è necessario aggiungere un costruttore parametrizzato, campi, codice di assegnazione, ecc. Alla classe sostitutiva.
CodesInChaos

Risposte:


116

Le espressioni lambda non cambiano l'insieme di problemi che puoi risolvere con Java in generale, ma sicuramente facilitano la risoluzione di alcuni problemi, proprio per lo stesso motivo per cui non programmiamo più in linguaggio assembly. Rimuovere le attività ridondanti dal lavoro del programmatore semplifica la vita e consente di fare cose che altrimenti non toccheresti nemmeno, solo per la quantità di codice che dovresti produrre (manualmente).

Ma le espressioni lambda non salvano solo righe di codice. Le espressioni lambda ti consentono di definire funzioni , qualcosa per cui potresti usare classi interne anonime come soluzione alternativa, ecco perché puoi sostituire classi interne anonime in questi casi, ma non in generale.

In particolare, le espressioni lambda sono definite indipendentemente dall'interfaccia funzionale a cui verranno convertite, quindi non ci sono membri ereditati a cui potrebbero accedere, inoltre, non possono accedere all'istanza del tipo che implementa l'interfaccia funzionale. All'interno di un'espressione lambda, thise superhanno lo stesso significato del contesto circostante, vedere anche questa risposta . Inoltre, non è possibile creare nuove variabili locali che ombreggiano le variabili locali del contesto circostante. Per il compito previsto di definire una funzione, questo rimuove molte fonti di errore, ma implica anche che per altri casi d'uso, potrebbero esserci classi interne anonime che non possono essere convertite in un'espressione lambda, anche se implementa un'interfaccia funzionale.

Inoltre, il costrutto new Type() { … }garantisce di produrre una nuova istanza distinta (come newsempre fa). Le istanze anonime della classe interna mantengono sempre un riferimento alla loro istanza esterna se create in un non staticcontesto. Al contrario, le espressioni lambda catturano solo un riferimento a thisquando necessario, cioè se accedono thiso un non staticmembro. E producono istanze di un'identità intenzionalmente non specificata, che consente all'implementazione di decidere in fase di runtime se riutilizzare istanze esistenti (vedere anche " Un'espressione lambda crea un oggetto sull'heap ogni volta che viene eseguita? ").

Queste differenze si applicano al tuo esempio. Il tuo costrutto della classe interna anonima produrrà sempre una nuova istanza, inoltre potrebbe catturare un riferimento all'istanza esterna, mentre la tua (Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())è un'espressione lambda non di acquisizione che verrà valutata come un singleton nelle implementazioni tipiche. Inoltre, non produce un .classfile sul tuo disco rigido.

Date le differenze riguardanti sia la semantica che le prestazioni, le espressioni lambda possono cambiare il modo in cui i programmatori risolveranno determinati problemi in futuro, ovviamente, anche a causa delle nuove API che abbracciano idee di programmazione funzionale utilizzando le nuove funzionalità del linguaggio. Vedere anche Espressione lambda Java 8 e valori di prima classe .


1
Essenzialmente, le espressioni lambda sono un tipo di programmazione funzionale. Dato che qualsiasi attività può essere eseguita con la programmazione funzionale o la programmazione imperativa, non vi è alcun vantaggio eccetto la leggibilità e la manutenibilità , che possono superare altre preoccupazioni.
Draco18s non si fida più di SE

1
@ Trilarion: potresti riempire interi libri con la definizione di funzione . Dovresti anche decidere se concentrarti sull'obiettivo o sugli aspetti supportati dalle espressioni lambda di Java. L'ultimo collegamento della mia risposta ( questa ) discute già alcuni di questi aspetti nel contesto di Java. Ma per capire la mia risposta, è già sufficiente capire che le funzioni sono un concetto diverso dalle classi. Per maggiori dettagli, ci sono già risorse esistenti e domande e risposte ...
Holger

1
@ Trilarion: entrambe le funzioni non consentono di risolvere più problemi, a meno che non si consideri inaccettabile la quantità di dimensione / complessità del codice o soluzioni alternative necessarie per altre soluzioni (come detto nel primo paragrafo), e c'era già un modo per definire le funzioni , come detto, le classi interne potrebbero essere utilizzate come soluzione alternativa per l'assenza di un modo migliore per definire le funzioni (ma le classi interne non sono funzioni, poiché possono servire a molti più scopi). È lo stesso di OOP: non consente di fare nulla che non potresti fare con l'assembly, ma comunque, puoi immaginare un'applicazione come Eclipse scritta in assembly?
Holger

3
@EricDuminil: per me, la completezza di Turing è troppo teorica. Ad esempio, indipendentemente dal fatto che Java sia completo o meno di Turing, non è possibile scrivere un driver di dispositivo Windows in Java. (O in PostScript, che è anche Turing completo) L'aggiunta di funzionalità a Java che consentono di farlo, cambierebbe la serie di problemi che puoi risolvere con esso, tuttavia, ciò non accadrà. Quindi preferisco il punto di riunione; poiché tutto ciò che puoi fare è implementato in istruzioni CPU eseguibili, non c'è niente che non potresti fare in Assembly che supporta ogni istruzione CPU (anche più facile da capire rispetto al formalismo della macchina di Turing).
Holger

2
@abelito è comune a tutti i costrutti di programmazione di livello superiore, per fornire meno "visibilità su ciò che sta realmente accadendo", poiché è di questo che si tratta, meno dettagli, più attenzione al problema di programmazione reale. Puoi usarlo per migliorare o peggiorare il codice; è solo uno strumento. Come la funzione di voto; è uno strumento che puoi utilizzare nel modo previsto, per fornire un giudizio fondato sull'effettiva qualità di un post. Oppure lo usi per svalutare una risposta elaborata e ragionevole, solo perché odi i lambda.
Holger

52

I linguaggi di programmazione non sono per le macchine da eseguire.

Sono per i programmatori su cui riflettere .

Le lingue sono una conversazione con un compilatore per trasformare i nostri pensieri in qualcosa che una macchina può eseguire. Una delle principali lamentele su Java da parte di persone che vengono ad esso da altre lingue (o lasciano che per altre lingue) usato per essere che costringe un certo modello mentale sul programmatore (cioè tutto è una classe).

Non ho intenzione di valutare se sia un bene o un male: tutto è un compromesso. Ma i lambda Java 8 consentono ai programmatori di pensare in termini di funzioni , cosa che prima non si poteva fare in Java.

È la stessa cosa di un programmatore procedurale che impara a pensare in termini di classi quando arriva a Java: li vedi spostarsi gradualmente da classi che sono strutture glorificate e hanno classi `` helper '' con un mucchio di metodi statici e passare a qualcosa che assomiglia più da vicino a un design OO razionale (mea culpa).

Se li consideri solo un modo più breve per esprimere classi interne anonime, probabilmente non li troverai molto impressionanti nello stesso modo in cui il programmatore procedurale sopra probabilmente non pensava che le classi fossero un grande miglioramento.


5
Fai attenzione però, pensavo che l'OOP fosse tutto, e poi mi sono orribilmente confuso con le architetture del sistema di componenti di entità (whaddaya significa che non hai una classe per ogni tipo di oggetto di gioco ?! e ogni oggetto di gioco non è un oggetto OOP ?!)
user253751

3
In altre parole, pensare in OOP, piuttosto che pensare solo alle classi * o f * come un altro strumento, limitava il mio pensiero in modo negativo.
user253751

2
@immibis: ci sono molti modi diversi per "pensare in OOP", quindi oltre al comune errore di confondere "pensare in OOP" con "pensare in classe", ci sono molti modi per pensare in modo controproducente. Sfortunatamente, ciò accade troppo spesso fin dall'inizio, poiché anche libri e tutorial insegnano quelli inferiori, mostrano anti-schemi negli esempi e diffondono quel pensiero. La presunta necessità di "avere una classe per ogni tipo di" qualunque cosa, è solo un esempio, contraddicendo il concetto di astrazione, allo stesso modo, sembra esserci il mito "OOP significa scrivere quanto più boilerplate possibile", ecc
Holger

@immibis sebbene in quel caso non ti stesse aiutando, quell'aneddoto in realtà supporta il punto: il linguaggio e il paradigma forniscono una struttura per strutturare i tuoi pensieri e trasformarli in un design. Nel tuo caso ti sei imbattuto contro i bordi della struttura, ma in un altro contesto farlo potrebbe averti aiutato a smarrirti in una grande palla di fango e produrre un brutto disegno. Quindi, presumo, "tutto è un compromesso". Mantenere il secondo vantaggio evitando il primo problema è una domanda chiave quando si decide quanto "grande" per fare una lingua.
Leushenko

@immibis Ecco perché è importante per imparare un po 'di paradigma bene : ciascuno ti raggiunge circa l'inadeguatezza degli altri.
Jared Smith

36

Il salvataggio di righe di codice può essere visto come una nuova funzionalità, se consente di scrivere una parte sostanziale di logica in un modo più breve e più chiaro, che richiede meno tempo per la lettura e la comprensione da parte degli altri.

Senza espressioni lambda (e / o riferimenti a metodi) le Streampipeline sarebbero state molto meno leggibili.

Pensa, ad esempio, come Streamsarebbe stata la seguente pipeline se sostituissi ogni espressione lambda con un'istanza di classe anonima.

List<String> names =
    people.stream()
          .filter(p -> p.getAge() > 21)
          .map(p -> p.getName())
          .sorted((n1,n2) -> n1.compareToIgnoreCase(n2))
          .collect(Collectors.toList());

Sarebbe:

List<String> names =
    people.stream()
          .filter(new Predicate<Person>() {
              @Override
              public boolean test(Person p) {
                  return p.getAge() > 21;
              }
          })
          .map(new Function<Person,String>() {
              @Override
              public String apply(Person p) {
                  return p.getName();
              }
          })
          .sorted(new Comparator<String>() {
              @Override
              public int compare(String n1, String n2) {
                  return n1.compareToIgnoreCase(n2);
              }
          })
          .collect(Collectors.toList());

È molto più difficile da scrivere rispetto alla versione con espressioni lambda ed è molto più soggetta a errori. È anche più difficile da capire.

E questa è una pipeline relativamente breve.

Per renderlo leggibile senza espressioni lambda e riferimenti al metodo, avresti dovuto definire variabili che contengano le varie istanze dell'interfaccia funzionale utilizzate qui, il che avrebbe diviso la logica della pipeline, rendendola più difficile da capire.


17
Penso che questa sia una visione chiave. Si potrebbe sostenere che qualcosa è praticamente impossibile da fare in un linguaggio di programmazione se l'overhead sintattico e cognitivo è troppo grande per essere utile, quindi altri approcci saranno superiori. Pertanto, riducendo l'overhead sintattico e cognitivo (come fanno i lambda rispetto alle classi anonime), diventano possibili pipeline come quelle sopra. Più ironico, se scrivere un pezzo di codice ti fa licenziare dal tuo lavoro, si potrebbe dire che non è possibile.
Sebastian Redl

Nitpick: In realtà non hai bisogno del Comparatorper sorteddato che si confronta comunque in ordine naturale. Forse cambiarlo per confrontare la lunghezza delle stringhe o simili, per rendere l'esempio ancora migliore?
tobias_k

@tobias_k nessun problema. L'ho cambiato in compareToIgnoreCaseper farlo comportare in modo diverso da sorted().
Eran

1
compareToIgnoreCaseè ancora un cattivo esempio, poiché potresti semplicemente usare il String.CASE_INSENSITIVE_ORDERcomparatore esistente . Il suggerimento di @ tobias_k di confrontare per lunghezza (o qualsiasi altra proprietà che non abbia un comparatore integrato) si adatta meglio. A giudicare dagli esempi di codice di domande e risposte SO, lambda ha causato molte implementazioni di comparatori non necessarie ...
Holger

Sono l'unico che pensa che il secondo esempio sia più facile da capire?
Battaglia di Clark,

8

Iterazione interna

Durante l'iterazione delle raccolte Java, la maggior parte degli sviluppatori tende a ottenere un elemento e quindi a elaborarlo . Cioè, togli quell'elemento e poi usalo, o reinseriscilo, ecc. Con le versioni precedenti alla 8 di Java, puoi implementare una classe interna e fare qualcosa come:

numbers.forEach(new Consumer<Integer>() {
    public void accept(Integer value) {
        System.out.println(value);
    }
});

Ora con Java 8 puoi fare di meglio e meno prolisso con:

numbers.forEach((Integer value) -> System.out.println(value));

o meglio

numbers.forEach(System.out::println);

Comportamenti come argomenti

Indovina il seguente caso:

public int sumAllEven(List<Integer> numbers) {
    int total = 0;

    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    } 
    return total;
}

Con l' interfaccia Java 8 Predicate puoi fare di meglio in questo modo:

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;

    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

Chiamandolo come:

sumAll(numbers, n -> n % 2 == 0);

Fonte: DZone - Perché abbiamo bisogno delle espressioni Lambda in Java


Probabilmente il primo caso è un modo per salvare alcune righe di codice, ma rende il codice più facile da leggere
anche

4
In che modo l'iterazione interna è una caratteristica lambda? Il semplice fatto è che tecnicamente i lambda non ti consentono di fare nulla che non potresti fare prima di java-8. È solo un modo più conciso per passare il codice.
Ousmane D.

@ Aominè In questo caso numbers.forEach((Integer value) -> System.out.println(value));l'espressione lambda è composta da due parti quella a sinistra del simbolo della freccia (->) che ne elenca i parametri e quella a destra che ne contiene il corpo . Il compilatore capisce automaticamente che l'espressione lambda ha la stessa firma dell'unico metodo non implementato dell'interfaccia Consumer.
alseether

5
Non ho chiesto cos'è un'espressione lambda, sto solo dicendo che l'iterazione interna non ha nulla a che fare con le espressioni lambda.
Ousmane D.

1
@ Aominè Capisco il tuo punto, non ha nulla con i lambda di per sé , ma come fai notare, è più un modo di scrivere più pulito. Penso che in termini di efficienza sia buono come le versioni precedenti alla 8
anche

4

Ci sono molti vantaggi nell'usare lambda invece della classe interna come di seguito:

  • Rendi il codice più compatto ed espressivo senza introdurre più semantica della sintassi del linguaggio. hai già fornito un esempio nella tua domanda.

  • Usando le espressioni lambda sei felice di programmare con operazioni in stile funzionale sui flussi di elementi, come le trasformazioni di riduzione della mappa nelle raccolte. vedere la documentazione dei pacchetti java.util.function e java.util.stream .

  • Non esiste un file di classi fisiche generato per lambda dal compilatore. Pertanto, riduce le dimensioni delle applicazioni fornite. Come la memoria assegna a lambda?

  • Il compilatore ottimizzerà la creazione di lambda se lambda non accede a variabili al di fuori del suo ambito, il che significa che l'istanza lambda viene creata solo una volta dalla JVM. per maggiori dettagli puoi vedere la risposta di @ Holger alla domanda La memorizzazione nella cache dei riferimenti al metodo è una buona idea in Java 8? .

  • Lambdas può implementare interfacce multi marker oltre all'interfaccia funzionale, ma le classi interne anonime non possono implementare più interfacce, ad esempio:

    //                 v--- create the lambda locally.
    Consumer<Integer> action = (Consumer<Integer> & Serializable) it -> {/*TODO*/};

2

I lambda sono solo zucchero sintattico per classi anonime.

Prima di lambda, le classi anonime possono essere utilizzate per ottenere lo stesso risultato. Ogni espressione lambda può essere convertita in una classe anonima.

Se stai usando IntelliJ IDEA, può eseguire la conversione per te:

  • Posiziona il cursore nel lambda
  • Premi ALT / OPZIONE + INVIO

inserisci qui la descrizione dell'immagine


3
... pensavo che @StuartMarks avesse già chiarito la zuccherosità sintattica (sp? Gr?) Di lambdas? ( stackoverflow.com/a/15241404/3475600 , softwareengineering.stackexchange.com/a/181743 )
srborlongan

2

Per rispondere alla tua domanda, il fatto è che lambda non ti consente di fare nulla che non potresti fare prima di java-8, piuttosto ti consente di scrivere codice più conciso . Il vantaggio di questo è che il tuo codice sarà più chiaro e più flessibile.


Chiaramente @Holger ha dissipato ogni dubbio in termini di efficienza lambda. Non è solo un modo per rendere il codice più pulito, ma se lambda non acquisisce alcun valore, sarà una classe Singleton (riutilizzabile)
se

Se scavi in ​​profondità, ogni caratteristica della lingua ha una differenza. La domanda in questione è di alto livello, quindi ho scritto la mia risposta ad alto livello.
Ousmane D.

2

Una cosa che non vedo ancora menzionata è che un lambda ti consente di definire la funzionalità dove viene utilizzato .

Quindi, se hai qualche semplice funzione di selezione non è necessario metterla in un posto separato con un mucchio di boilerplate, scrivi semplicemente un lambda che sia conciso e localmente rilevante.


1

Sì, ci sono molti vantaggi.

  • Non è necessario definire l'intera classe, possiamo passare l'implementazione della funzione stessa come riferimento.
    • La creazione interna della classe creerà il file .class mentre se usi lambda, la creazione della classe viene evitata dal compilatore perché in lambda stai passando l'implementazione della funzione invece della classe.
  • La riutilizzabilità del codice è maggiore rispetto a prima
  • E come hai detto, il codice è più breve della normale implementazione.
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.