CodeGym/Java Blog/Random-IT/Una spiegazione delle espressioni lambda in Java. Con ese...
John Squirrels
Livello 41
San Francisco

Una spiegazione delle espressioni lambda in Java. Con esempi e compiti. Parte 1

Pubblicato nel gruppo Random-IT
membri
Per chi è questo articolo?
  • È per le persone che pensano di conoscere già bene Java Core, ma non hanno idea delle espressioni lambda in Java. O forse hanno sentito qualcosa sulle espressioni lambda, ma mancano i dettagli
  • È per le persone che hanno una certa comprensione delle espressioni lambda, ma ne sono ancora scoraggiate e non sono abituate a usarle.
Una spiegazione delle espressioni lambda in Java.  Con esempi e compiti.  Parte 1 - 1Se non rientri in una di queste categorie, potresti trovare questo articolo noioso, imperfetto o generalmente non adatto a te. In questo caso, sentiti libero di passare ad altre cose o, se sei esperto in materia, per favore dai suggerimenti nei commenti su come potrei migliorare o integrare l'articolo. Il materiale non pretende di avere alcun valore accademico, figuriamoci novità. Al contrario: cercherò di descrivere le cose complesse (per alcune persone) nel modo più semplice possibile. Una richiesta per spiegare l'API Stream mi ha ispirato a scrivere questo. Ci ho pensato e ho deciso che alcuni dei miei esempi di stream sarebbero stati incomprensibili senza una comprensione delle espressioni lambda. Quindi inizieremo con le espressioni lambda. Cosa devi sapere per capire questo articolo?
  1. Dovresti comprendere la programmazione orientata agli oggetti (OOP), vale a dire:

    • classi, oggetti e la loro differenza;
    • interfacce, come differiscono dalle classi e relazione tra interfacce e classi;
    • metodi, come chiamarli, metodi astratti (cioè metodi senza implementazione), parametri di metodo, argomenti di metodo e come passarli;
    • modificatori di accesso, metodi/variabili statici, metodi/variabili finali;
    • ereditarietà di classi e interfacce, ereditarietà multipla di interfacce.
  2. Conoscenza di Java Core: tipi generici (generics), collezioni (liste), thread.
Bene, andiamo al punto.

Un po' di storia

Le espressioni lambda sono arrivate a Java dalla programmazione funzionale e lì dalla matematica. Negli Stati Uniti a metà del XX secolo, Alonzo Church, che amava molto la matematica e tutti i tipi di astrazioni, lavorava alla Princeton University. Fu Alonzo Church a inventare il lambda calcolo, che inizialmente era un insieme di idee astratte del tutto estranee alla programmazione. Matematici come Alan Turing e John von Neumann lavorarono contemporaneamente alla Princeton University. Tutto si è combinato: Church ha inventato il lambda calcolo. Turing sviluppò la sua macchina informatica astratta, ora nota come "macchina di Turing". E von Neumann ha proposto un'architettura di computer che ha costituito la base dei computer moderni (ora chiamata "architettura di von Neumann"). A quel tempo, Alonzo Chiesa' Le sue idee non divennero così famose come le opere dei suoi colleghi (ad eccezione del campo della matematica pura). Tuttavia, poco dopo John McCarthy (anche lui laureato alla Princeton University e, all'epoca della nostra storia, impiegato del Massachusetts Institute of Technology) si interessò alle idee di Church. Nel 1958 creò il primo linguaggio di programmazione funzionale, LISP, basato su quelle idee. E 58 anni dopo, le idee della programmazione funzionale sono trapelate in Java 8. Non sono passati nemmeno 70 anni... Onestamente, questo non è il tempo che ci vuole perché un'idea matematica venga applicata nella pratica. un impiegato del Massachusetts Institute of Technology) si interessò alle idee di Church. Nel 1958 creò il primo linguaggio di programmazione funzionale, LISP, basato su quelle idee. E 58 anni dopo, le idee della programmazione funzionale sono trapelate in Java 8. Non sono passati nemmeno 70 anni... Onestamente, questo non è il tempo che ci vuole perché un'idea matematica venga applicata nella pratica. un impiegato del Massachusetts Institute of Technology) si interessò alle idee di Church. Nel 1958 creò il primo linguaggio di programmazione funzionale, LISP, basato su quelle idee. E 58 anni dopo, le idee della programmazione funzionale sono trapelate in Java 8. Non sono passati nemmeno 70 anni... Onestamente, questo non è il tempo che ci vuole perché un'idea matematica venga applicata nella pratica.

Il nocciolo della questione

Un'espressione lambda è un tipo di funzione. Puoi considerarlo un normale metodo Java ma con la capacità distintiva di essere passato ad altri metodi come argomento. Giusto. È diventato possibile passare ai metodi non solo numeri, stringhe e gatti, ma anche altri metodi! Quando potremmo averne bisogno? Sarebbe utile, ad esempio, se vogliamo passare qualche metodo di callback. Cioè, se abbiamo bisogno che il metodo che chiamiamo abbia la possibilità di chiamare qualche altro metodo che gli passiamo. In altre parole, abbiamo la possibilità di passare una richiamata in determinate circostanze e una diversa richiamata in altre. E così il nostro metodo che riceve i nostri callback li chiama. L'ordinamento è un semplice esempio. Supponiamo di scrivere un algoritmo di ordinamento intelligente che assomigli a questo:
public void mySuperSort() {
    // We do something here
    if(compare(obj1, obj2) > 0)
    // And then we do something here
}
Nell'istruzione if, chiamiamo il compare()metodo, passando due oggetti da confrontare, e vogliamo sapere quale di questi oggetti è "maggiore". Supponiamo che il "maggiore" venga prima del "minore". Metto "maggiore" tra virgolette, perché stiamo scrivendo un metodo universale che saprà ordinare non solo in ordine crescente, ma anche in ordine decrescente (in questo caso l'oggetto "maggiore" sarà effettivamente l'oggetto "minore" , e viceversa). Per impostare l'algoritmo specifico per il nostro ordinamento, abbiamo bisogno di un meccanismo per passarlo al nostro mySuperSort()metodo. In questo modo saremo in grado di "controllare" il nostro metodo quando viene chiamato. Naturalmente, potremmo scrivere due metodi separati — mySuperSortAscend()emySuperSortDescend()— per l'ordinamento in ordine crescente e decrescente. Oppure potremmo passare qualche argomento al metodo (ad esempio, una variabile booleana; se vero, quindi ordinare in ordine crescente e se falso, quindi in ordine decrescente). Ma cosa succede se vogliamo ordinare qualcosa di complicato come un elenco di array di stringhe? In che modo il nostro mySuperSort()metodo saprà come ordinare questi array di stringhe? Per taglia? Dalla lunghezza cumulativa di tutte le parole? Forse in ordine alfabetico in base alla prima stringa nell'array? E se avessimo bisogno di ordinare l'elenco degli array in base alla dimensione dell'array in alcuni casi e in base alla lunghezza cumulativa di tutte le parole in ogni array in altri casi? Mi aspetto che tu abbia già sentito parlare di comparatori e che in questo caso passeremmo semplicemente al nostro metodo di ordinamento un oggetto comparatore che descrive l'algoritmo di ordinamento desiderato. Perché la normasort()Il metodo è implementato in base allo stesso principio di mySuperSort(), che userò sort()nei miei esempi.
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<;String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByCumulativeWordLength = new Comparator<String[]>() {

    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }

        for (String s : o2) {
            length2 += s.length();
        }

        return length1 - length2;
    }
};

arrays.sort(sortByLength);
Risultato:
Dota GTA5 Halo
if then else
I really love Java
Qui gli array sono ordinati in base al numero di parole in ciascun array. Un array con meno parole è considerato "minore". Ecco perché viene prima. Un array con più parole è considerato "maggiore" e viene posizionato alla fine. Se passiamo un diverso comparatore al sort()metodo, come sortByCumulativeWordLength, otterremo un risultato diverso:
if then else
Dota GTA5 Halo
I really love Java
Ora gli array sono ordinati in base al numero totale di lettere nelle parole dell'array. Nel primo array ci sono 10 lettere, nel secondo - 12 e nel terzo - 15. Se abbiamo un solo comparatore, non dobbiamo dichiarare una variabile separata per esso. Invece, possiamo semplicemente creare una classe anonima proprio al momento della chiamata al sort()metodo. Qualcosa come questo:
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};

List<String[]> arrays = new ArrayList<>();

arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Otterremo lo stesso risultato del primo caso. Attività 1. Riscrivi questo esempio in modo che ordini gli array non in ordine crescente del numero di parole in ogni array, ma in ordine decrescente. Tutto questo lo sappiamo già. Sappiamo come passare oggetti ai metodi. A seconda di ciò di cui abbiamo bisogno al momento, possiamo passare diversi oggetti a un metodo, che invocherà quindi il metodo che abbiamo implementato. Ciò pone la domanda: perché nel mondo abbiamo bisogno di un'espressione lambda qui?  Perché un'espressione lambda è un oggetto che ha esattamente un metodo. Come un "oggetto metodo". Un metodo impacchettato in un oggetto. Ha solo una sintassi leggermente sconosciuta (ma ne parleremo più avanti). Diamo un'altra occhiata a questo codice:
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Qui prendiamo la nostra lista di array e chiamiamo il suo sort()metodo, al quale passiamo un oggetto comparatore con un singolo compare()metodo (il suo nome non ha importanza per noi - dopo tutto, è l'unico metodo di questo oggetto, quindi non possiamo sbagliare). Questo metodo ha due parametri con cui lavoreremo. Se stai lavorando in IntelliJ IDEA, probabilmente hai visto che offre di condensare in modo significativo il codice come segue:
arrays.sort((o1, o2) -> o1.length - o2.length);
Ciò riduce sei righe a una singola breve. 6 righe vengono riscritte come una breve. Qualcosa è scomparso, ma garantisco che non era niente di importante. Questo codice funzionerà esattamente allo stesso modo di una classe anonima. Attività 2. Prova a riscrivere la soluzione dell'attività 1 utilizzando un'espressione lambda (come minimo, chiedi a IntelliJ IDEA di convertire la tua classe anonima in un'espressione lambda).

Parliamo di interfacce

In linea di principio, un'interfaccia è semplicemente un elenco di metodi astratti. Quando creiamo una classe che implementa qualche interfaccia, la nostra classe deve implementare i metodi inclusi nell'interfaccia (oppure dobbiamo rendere la classe astratta). Esistono interfacce con molti metodi diversi (ad esempio,  List) e interfacce con un solo metodo (ad esempio, Comparatoro Runnable). Ci sono interfacce che non hanno un unico metodo (le cosiddette interfacce marker come Serializable). Le interfacce che hanno un solo metodo sono anche chiamate interfacce funzionali . In Java 8, sono persino contrassegnati da un'annotazione speciale:@FunctionalInterface. Sono queste interfacce a metodo singolo adatte come tipi di destinazione per le espressioni lambda. Come ho detto sopra, un'espressione lambda è un metodo racchiuso in un oggetto. E quando passiamo un tale oggetto, stiamo essenzialmente passando questo singolo metodo. Si scopre che non ci interessa come si chiama il metodo. Le uniche cose che ci interessano sono i parametri del metodo e, ovviamente, il corpo del metodo. In sostanza, un'espressione lambda è l'implementazione di un'interfaccia funzionale. Ovunque vediamo un'interfaccia con un singolo metodo, una classe anonima può essere riscritta come lambda. Se l'interfaccia ha più o meno di un metodo, un'espressione lambda non funzionerà e useremo invece una classe anonima o anche un'istanza di una classe ordinaria. Ora è il momento di approfondire un po' le lambda. :)

Sintassi

La sintassi generale è qualcosa del genere:
(parameters) -> {method body}
Ovvero, parentesi che racchiudono i parametri del metodo, una "freccia" (formata da un trattino e segno di maggiore di) e quindi il corpo del metodo tra parentesi graffe, come sempre. I parametri corrispondono a quelli specificati nel metodo di interfaccia. Se i tipi di variabile possono essere determinati in modo inequivocabile dal compilatore (nel nostro caso, sa che stiamo lavorando con array di stringhe, perché il nostro Listoggetto è digitato usando String[]), allora non devi indicarne i tipi.
Se sono ambigui, indicare il tipo. IDEA lo colorerà di grigio se non è necessario.
Puoi leggere di più in questo tutorial Oracle e altrove. Questo si chiama " tipizzazione target ". Puoi nominare le variabili come preferisci, non devi usare gli stessi nomi specificati nell'interfaccia. Se non ci sono parametri, basta indicare parentesi vuote. Se è presente un solo parametro, indicare semplicemente il nome della variabile senza alcuna parentesi. Ora che abbiamo compreso i parametri, è il momento di discutere il corpo dell'espressione lambda. All'interno delle parentesi graffe, scrivi il codice proprio come faresti per un metodo ordinario. Se il tuo codice è costituito da una singola riga, puoi omettere completamente le parentesi graffe (simile alle istruzioni if ​​e ai cicli for). Se il tuo lambda a riga singola restituisce qualcosa, non devi includere areturndichiarazione. Ma se usi le parentesi graffe, devi includere esplicitamente un'istruzione return, proprio come faresti con un metodo ordinario.

Esempi

Esempio 1.
() -> {}
L'esempio più semplice. E il più inutile :), dal momento che non fa nulla. Esempio 2.
() -> ""
Un altro esempio interessante. Non prende nulla e restituisce una stringa vuota ( returnviene omesso perché non è necessario). Ecco la stessa cosa, ma con return:
() -> {
    return "";
}
Esempio 3. "Ciao, mondo!" utilizzando lambda
() -> System.out.println("Hello, World!")
Non richiede nulla e non restituisce nulla (non possiamo anteporre returnla chiamata a System.out.println(), perché il println()tipo restituito dal metodo è void). Mostra semplicemente il saluto. Questo è l'ideale per un'implementazione dell'interfaccia Runnable. Il seguente esempio è più completo:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello, World!")).start();
    }
}
O così:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello, World!"));
        t.start();
    }
}
Oppure possiamo anche salvare l'espressione lambda come Runnableoggetto e poi passarla al Threadcostruttore:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello, World!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Diamo un'occhiata più da vicino al momento in cui un'espressione lambda viene salvata in una variabile. L' Runnableinterfaccia ci dice che i suoi oggetti devono avere un public void run()metodo. Secondo l'interfaccia, il runmetodo non accetta parametri. E non restituisce nulla, cioè il suo tipo di ritorno è void. Di conseguenza, questo codice creerà un oggetto con un metodo che non accetta né restituisce nulla. Questo corrisponde perfettamente al metodo Runnabledell'interfaccia run(). Ecco perché siamo stati in grado di inserire questa espressione lambda in una Runnablevariabile.  Esempio 4.
() -> 42
Ancora una volta, non prende nulla, ma restituisce il numero 42. Tale espressione lambda può essere inserita in una Callablevariabile, perché questa interfaccia ha un solo metodo che assomiglia a questo:
V call(),
dove  V è il tipo restituito (nel nostro caso,  int). Di conseguenza, possiamo salvare un'espressione lambda come segue:
Callable<Integer> c = () -> 42;
Esempio 5. Un'espressione lambda che coinvolge più righe
() -> {
    String[] helloWorld = {"Hello", "World!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Di nuovo, questa è un'espressione lambda senza parametri e un voidtipo restituito (perché non c'è alcuna returnistruzione).  Esempio 6
x -> x
Qui prendiamo una xvariabile e la restituiamo. Tieni presente che se è presente un solo parametro, puoi omettere le parentesi attorno ad esso. Ecco la stessa cosa, ma con parentesi:
(x) -> x
Ed ecco un esempio con un'istruzione return esplicita:
x -> {
    return x;
}
O in questo modo con parentesi e un'istruzione return:
(x) -> {
    return x;
}
Oppure con esplicita indicazione del tipo (e quindi tra parentesi):
(int x) -> x
Esempio 7
x -> ++x
Lo prendiamo xe lo restituiamo, ma solo dopo aver aggiunto 1. Puoi riscrivere quel lambda in questo modo:
x -> x + 1
In entrambi i casi, omettiamo le parentesi attorno al corpo del parametro e del metodo, insieme all'istruzione return, poiché sono facoltative. Le versioni con parentesi e un'istruzione return sono fornite nell'Esempio 6. Esempio 8
(x, y) -> x % y
Prendiamo xe ye restituiamo il resto della divisione di xper y. Le parentesi intorno ai parametri sono obbligatorie qui. Sono opzionali solo quando è presente un solo parametro. Eccolo con esplicita indicazione delle tipologie:
(double x, int y) -> x % y
Esempio 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Prendiamo un Catoggetto, un Stringnome e un'età intera. Nel metodo stesso, usiamo il nome passato e l'età per impostare le variabili sul cat. Poiché il nostro catoggetto è un tipo di riferimento, verrà modificato al di fuori dell'espressione lambda (otterrà il nome e l'età passati). Ecco una versione leggermente più complicata che utilizza un lambda simile:
public class Main {

    public static void main(String[] args) {
        // Create a cat and display it to confirm that it is "empty"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // Create a lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);

        };

        // Call a method to which we pass the cat and lambda
        changeEntity(myCat, s);

        // Display the cat on the screen and see that its state has changed (it has a name and age)
        System.out.println(myCat);

    }

    private static <T extends HasNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Smokey", 3);
    }
}

interface HasNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends HasNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements HasNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
Risultato:
Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Come puoi vedere, l' Catoggetto aveva uno stato e poi lo stato è cambiato dopo aver usato l'espressione lambda. Le espressioni lambda si combinano perfettamente con i generici. E se abbiamo bisogno di creare una Dogclasse che implementi anche HasNameAndAge, allora possiamo eseguire le stesse operazioni nel Dogmetodo main() senza modificare l'espressione lambda. Attività 3. Scrivete un'interfaccia funzionale con un metodo che prenda un numero e restituisca un valore booleano. Scrivi un'implementazione di tale interfaccia come un'espressione lambda che restituisca vero se il numero passato è divisibile per 13. Compito 4.Scrivere un'interfaccia funzionale con un metodo che accetti due stringhe e restituisca anche una stringa. Scrivi un'implementazione di tale interfaccia come un'espressione lambda che restituisce la stringa più lunga. Attività 5. Scrivete un'interfaccia funzionale con un metodo che accetti tre numeri in virgola mobile: a, bec e restituisca anche un numero in virgola mobile. Scrivere un'implementazione di tale interfaccia come un'espressione lambda che restituisce il discriminante. Nel caso te ne fossi dimenticato, questo è D = b^2 — 4ac. Attività 6. Utilizzando l'interfaccia funzionale dell'attività 5, scrivi un'espressione lambda che restituisca il risultato di a * b^c. Una spiegazione delle espressioni lambda in Java. Con esempi e compiti. Parte 2
Commenti
  • Popolari
  • Nuovi
  • Vecchi
Devi avere effettuato l'accesso per lasciare un commento
Questa pagina non ha ancora commenti