JavaRush /Java Blog /Random-IT /Popolare sulle espressioni lambda in Java. Con esempi e c...
Стас Пасинков
Livello 26
Киев

Popolare sulle espressioni lambda in Java. Con esempi e compiti. Parte 1

Pubblicato nel gruppo Random-IT
A chi è rivolto questo articolo?
  • Per coloro che pensano di conoscere già bene Java Core, ma non hanno idea delle espressioni lambda in Java. O forse hai già sentito parlare di lambda, ma senza dettagli.
  • per coloro che hanno una certa comprensione delle espressioni lambda, ma hanno ancora paura ed è insolito usarle.
Se non rientri in una di queste categorie, potresti trovare questo articolo noioso, errato e generalmente "non interessante". In questo caso, sentiti libero di passare o, se sei esperto dell'argomento, suggerisci nei commenti come potrei migliorare o integrare l'articolo. Il materiale non rivendica alcun valore accademico e ancor meno novità. Anzi, al contrario: in esso cercherò di descrivere le cose complesse (per alcuni) nel modo più semplice possibile. Sono stato ispirato a scrivere da una richiesta di spiegazione dello stream API. Ci ho pensato e ho deciso che senza comprendere le espressioni lambda, alcuni dei miei esempi sui "flussi" sarebbero stati incomprensibili. Quindi iniziamo con lambda. Popolare sulle espressioni lambda in Java.  Con esempi e compiti.  Parte 1 - 1Quali conoscenze sono necessarie per comprendere questo articolo:
  1. Comprensione della programmazione orientata agli oggetti (di seguito denominata OOP), vale a dire:
    • conoscenza di cosa sono le classi e gli oggetti, qual è la differenza tra loro;
    • conoscenza di cosa sono le interfacce, come differiscono dalle classi, qual è la connessione tra loro (interfacce e classi);
    • conoscenza di cos'è un metodo, come chiamarlo, cos'è un metodo astratto (o un metodo senza implementazione), quali sono i parametri/argomenti di un metodo, come passarli lì;
    • modificatori di accesso, metodi/variabili statici, metodi/variabili finali;
    • ereditarietà (classi, interfacce, ereditarietà multipla di interfacce).
  2. Conoscenza di Java Core: generici, raccolte (liste), thread.
Bene, cominciamo.

Un po' di storia

Le espressioni lambda sono arrivate in Java dalla programmazione funzionale e poi dalla matematica. A metà del XX secolo in America, all'Università di Princeton lavorava un certo Alonzo Church, che amava molto la matematica e tutti i tipi di astrazioni. Fu Alonzo Church a inventare il lambda calcolo, che inizialmente era un insieme di idee astratte e non aveva nulla a che fare con la programmazione. Allo stesso tempo, matematici come Alan Turing e John von Neumann lavoravano nella stessa Università di Princeton. Tutto combaciò: Church inventò il sistema del lambda calcolo, Turing sviluppò la sua macchina calcolatrice astratta, ora conosciuta come “macchina di Turing”. Bene, von Neumann propose un diagramma dell'architettura dei computer, che costituì la base dei computer moderni (e ora è chiamato "architettura di von Neumann"). A quel tempo, le idee di Alonzo Church non ottennero la stessa fama del lavoro dei suoi colleghi (ad eccezione del campo della matematica “pura”). Tuttavia, poco dopo, un certo John McCarthy (anche lui laureato all'Università di Princeton, all'epoca della storia - impiegato del Massachusetts Institute of Technology) si interessò alle idee di Church. Sulla base di essi, nel 1958 creò il primo linguaggio di programmazione funzionale, il Lisp. E 58 anni dopo, le idee della programmazione funzionale sono trapelate in Java come numero 8. Non sono passati nemmeno 70 anni... In effetti, questo non è il periodo di tempo più lungo per applicare nella pratica un'idea matematica.

L'essenza

Un'espressione lambda è una di queste funzioni. Puoi considerarlo come un normale metodo in Java, l'unica differenza è che può essere passato ad altri metodi come argomento. Sì, è diventato possibile passare ai metodi non solo numeri, stringhe e gatti, ma anche altri metodi! Quando potremmo averne bisogno? Ad esempio, se vogliamo passare qualche callback. Abbiamo bisogno che il metodo che chiamiamo possa chiamare qualche altro metodo che gli passiamo. Cioè, in modo da avere l'opportunità di trasmettere una richiamata in alcuni casi e un'altra in altri. E il nostro metodo, che accetterebbe i nostri callback, li chiamerebbe. Un semplice esempio è l'ordinamento. Diciamo che scriviamo una sorta di ordinamento complicato che assomiglia a questo:

public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
Dove, ifchiamiamo il metodo compare(), passiamo lì due oggetti che confrontiamo e vogliamo scoprire quale di questi oggetti è “maggiore”. Metteremo quello “più” prima di quello “più piccolo”. Ho scritto “more” tra virgolette perché stiamo scrivendo un metodo universale che sarà in grado di ordinare non solo in ordine ascendente ma anche discendente (in questo caso “more” sarà l'oggetto sostanzialmente più piccolo, e viceversa) . Per impostare la regola esattamente su come vogliamo ordinare, dobbiamo in qualche modo passarla al nostro file mySuperSort(). In questo caso potremo in qualche modo “controllare” il nostro metodo mentre viene chiamato. Naturalmente è possibile scrivere due metodi separati mySuperSortAsc()per mySuperSortDesc()l'ordinamento in ordine ascendente e discendente. Oppure passa qualche parametro all'interno del metodo (ad esempio, booleanif true, ordina in ordine crescente e if falsein ordine decrescente). Ma cosa succede se vogliamo ordinare non una struttura semplice, ma, ad esempio, un elenco di array di stringhe? Come farà il nostro metodo mySuperSort()a sapere come ordinare questi array di stringhe? A misura? Per lunghezza totale delle parole? Forse in ordine alfabetico, a seconda della prima riga dell'array? Ma cosa succede se, in alcuni casi, dobbiamo ordinare un elenco di array in base alla dimensione dell'array e, in un altro caso, in base alla lunghezza totale delle parole nell'array? Penso che tu abbia già sentito parlare di comparatori e che in questi casi passiamo semplicemente un oggetto comparatore al nostro metodo di ordinamento, in cui descriviamo le regole in base alle quali vogliamo ordinare. Poiché il metodo standard sort()è implementato secondo lo stesso principio di , mySuperSort()negli esempi utilizzerò quello standard sort().

String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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[]> sortByWordsLength = 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:
  1. la mamma ha lavato il telaio
  2. pace Il lavoro può
  3. Adoro Java
Qui gli array sono ordinati in base al numero di parole in ciascun array. Un array con meno parole è considerato “più piccolo”. Ecco perché viene all'inizio. Quello in cui ci sono più parole è considerato “more” e finisce alla fine. Se sort()passiamo un altro comparatore al metodo (sortByWordsLength), il risultato sarà diverso:
  1. pace Il lavoro può
  2. la mamma ha lavato il telaio
  3. Adoro Java
Ora gli array vengono ordinati in base al numero totale di lettere nelle parole di tale array. Nel primo caso ci sono 10 lettere, nel secondo 12 e nel terzo 15. Se utilizziamo un solo comparatore, non possiamo creare una variabile separata per esso, ma semplicemente creare un oggetto di una classe anonima proprio nel punto momento della chiamata al metodo sort(). Come quello:

String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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;
    }
});
Il risultato sarà lo stesso del primo caso. Compito 1 . Riscrivi questo esempio in modo che ordini gli array non in ordine crescente in base al numero di parole nell'array, ma in ordine discendente. Tutto questo lo sappiamo già. Sappiamo come passare gli oggetti ai metodi, possiamo passare questo o quell'oggetto a un metodo a seconda di ciò di cui abbiamo bisogno al momento, e all'interno del metodo in cui passiamo tale oggetto, si chiamerà il metodo per il quale abbiamo scritto l'implementazione . La domanda sorge spontanea: cosa c'entrano le espressioni lambda? Dato che un lambda è un oggetto che contiene esattamente un metodo. È come un oggetto metodo. Un metodo racchiuso in un oggetto. Hanno solo una sintassi leggermente insolita (ma ne parleremo più avanti). Diamo un'altra occhiata a questa voce

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Qui prendiamo la nostra lista arrayse chiamiamo il suo metodo sort(), dove passiamo un oggetto comparatore con un unico metodo compare()(non ci importa come si chiama, perché è l'unico in questo oggetto, non ce lo perderemo). Questo metodo richiede due parametri, con i quali lavoreremo in seguito. Se lavori in IntelliJ IDEA , probabilmente hai visto come ti offre questo codice per abbreviare in modo significativo:

arrays.sort((o1, o2) -> o1.length - o2.length);
È così che sei righe si sono trasformate in una breve. 6 righe sono state riscritte in una breve. Qualcosa è scomparso, ma ti garantisco che non è scomparso nulla di importante e questo codice funzionerà esattamente come con una classe anonima. Compito 2 . Scopri come riscrivere la soluzione al problema 1 utilizzando lambda (come ultima risorsa, chiedi a IntelliJ IDEA di trasformare la tua classe anonima in una lambda).

Parliamo di interfacce

Fondamentalmente, un'interfaccia è solo un elenco di metodi astratti. Quando creiamo una classe e diciamo che implementerà un qualche tipo di interfaccia, dobbiamo scrivere nella nostra classe un'implementazione dei metodi elencati nell'interfaccia (o, come ultima risorsa, non scriverla, ma rendere la classe astratta ). Esistono interfacce con molti metodi diversi (ad esempio List), esistono interfacce con un solo metodo (ad esempio, lo stesso Comparator o Runnable). Esistono interfacce senza un unico metodo (le cosiddette interfacce marker, ad esempio Serializable). Quelle interfacce che hanno un solo metodo sono anche chiamate interfacce funzionali . In Java 8 sono addirittura contrassegnati con una speciale annotazione @FunctionalInterface . Si interfaccia con un singolo metodo adatto all'uso da parte delle espressioni lambda. Come ho detto sopra, un'espressione lambda è un metodo racchiuso in un oggetto. E quando passiamo un oggetto del genere da qualche parte, di fatto passiamo questo unico metodo. Si scopre che non ci importa come si chiama questo metodo. Tutto ciò che è importante per noi sono i parametri accettati da questo metodo e, di fatto, il codice del metodo stesso. Un'espressione lambda è, essenzialmente. realizzazione di un'interfaccia funzionale. Laddove vediamo un'interfaccia con un metodo, significa che possiamo riscrivere una classe così anonima utilizzando una lambda. Se l'interfaccia ha più/meno di un metodo, l'espressione lambda non sarà adatta a noi e utilizzeremo una classe anonima, o anche una normale. È ora di approfondire le lambda. :)

Sintassi

La sintassi generale è qualcosa del genere:

(параметры) -> {тело метода}
Cioè, parentesi, al loro interno ci sono i parametri del metodo, una "freccia" (questi sono due caratteri di seguito: meno e maggiore), dopo di che il corpo del metodo è tra parentesi graffe, come sempre. I parametri corrispondono a quelli specificati nell'interfaccia durante la descrizione del metodo. Se il tipo delle variabili può essere definito chiaramente dal compilatore (nel nostro caso è certo che stiamo lavorando con array di stringhe, perché è Listdigitato esattamente da array di stringhe), allora il tipo delle variabili String[]non ha bisogno essere scritto.
Se non sei sicuro specifica la tipologia e IDEA la evidenzierà in grigio se non serve.
Puoi leggere di più nel tutorial di Oracle , ad esempio. Questo si chiama "digitazione target" . Puoi dare qualsiasi nome alle variabili, non necessariamente quelli specificati nell'interfaccia. Se non ci sono parametri, solo parentesi. Se è presente un solo parametro, solo il nome della variabile senza parentesi. Abbiamo sistemato i parametri, ora riguardo al corpo dell'espressione lambda stessa. All'interno delle parentesi graffe, scrivi il codice come per un metodo normale. Se l'intero codice è composto da una sola riga, non è necessario scrivere le parentesi graffe (come con if e loop). Se la tua lambda restituisce qualcosa, ma il suo corpo è costituito da una riga, returnnon è affatto necessario scriverla. Ma se hai le parentesi graffe, allora, come nel solito metodo, devi scrivere esplicitamente return.

Esempi

Esempio 1.
() -> {}
L'opzione più semplice. E quello più insignificante :) Perché non fa nulla. Esempio 2.
() -> ""
Anche un'opzione interessante. Non accetta nulla e restituisce una stringa vuota ( returnomessa perché non necessaria). Lo stesso, ma con return:

() -> {
    return "";
}
Esempio 3. Ciao mondo utilizzando lambda

() -> System.out.println("Hello world!")
Non riceve nulla, non restituisce nulla (non possiamo anteporre returnla chiamata System.out.println(), poiché il tipo restituito nel metodo println() — void), visualizza semplicemente una scritta sullo schermo. Ideale per implementare un'interfaccia Runnable. Lo stesso 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 oggetto di tipo Runnablee poi passarla al costruttore thread’а:

public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Diamo uno sguardo più da vicino al momento del salvataggio di un'espressione lambda in una variabile. L'interfaccia Runnableci dice che i suoi oggetti devono avere un metodo public void run(). Secondo l'interfaccia, il metodo run non accetta nulla come parametro. E non restituisce nulla (void). Pertanto, scrivendo in questo modo, verrà creato un oggetto con un metodo che non accetta né restituisce nulla. Il che è abbastanza coerente con il metodo run()nell'interfaccia Runnable. Ecco perché siamo riusciti a inserire questa espressione lambda in una variabile come Runnable. Esempio 4

() -> 42
Ancora una volta, non accetta nulla, ma restituisce il numero 42. Questa espressione lambda può essere inserita in una variabile di tipo Callable, perché questa interfaccia definisce solo un metodo, che assomiglia a questo:

V call(),
dove Vè il tipo del valore restituito (nel nostro caso int). Di conseguenza, possiamo memorizzare tale espressione lambda come segue:

Callable<Integer> c = () -> 42;
Esempio 5. Lambda su più righe

() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);    
}
Ancora una volta, questa è un'espressione lambda senza parametri e il relativo tipo restituito void(poiché non esiste return). Esempio 6

x -> x
Qui prendiamo qualcosa in una variabile хe lo restituiamo. Tieni presente che se viene accettato un solo parametro, non è necessario scrivere le parentesi attorno ad esso. Lo stesso, ma con parentesi:

(x) -> x
Ed ecco l'opzione con una esplicita return:

x -> {
    return x;
}
O così, con parentesi e return:

(x) -> {
    return x;
}
Oppure con l'indicazione esplicita della tipologia (e, di conseguenza, con parentesi):

(int x) -> x
Esempio 7
x -> ++x
Lo accettiamo хe lo restituiamo, ma per 1di più. Puoi anche riscriverlo in questo modo:
x -> x + 1
In entrambi i casi, non indichiamo parentesi attorno al parametro, al corpo del metodo e alla parola return, poiché ciò non è necessario. Le opzioni con parentesi e invio sono descritte nell'esempio 6. Esempio 8
(x, y) -> x % y
Ne accettiamo alcuni хe уrestituiamo il resto della divisione xcon y. Le parentesi attorno ai parametri sono già necessarie qui. Sono facoltativi solo quando è presente un solo parametro. In questo modo con indicazione esplicita dei tipi:
(double x, int y) -> x % y
Esempio 9

(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Accettiamo un oggetto Cat, una stringa con un nome e un'età intera. Nel metodo stesso, impostiamo il nome e l'età passati al Gatto. catPoiché la nostra variabile è un tipo di riferimento, l'oggetto Cat all'esterno dell'espressione lambda cambierà (riceverà il nome e l'età passati all'interno). Una versione leggermente più complicata che utilizza un lambda simile:

public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

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

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

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

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

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

class Cat implements WithNameAndAge {
    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='Murzik', age=3} Come puoi vedere, inizialmente l'oggetto Cat aveva uno stato, ma dopo aver utilizzato l'espressione lambda, lo stato è cambiato . Le espressioni Lambda funzionano bene con i generici. E se dobbiamo creare una classe Dog, ad esempio, che implementerà anche WithNameAndAge, allora nel metodo main()possiamo fare le stesse operazioni con Dog, senza modificare affatto l'espressione lambda stessa. Compito 3 . Scrivi un'interfaccia funzionale con un metodo che accetta un numero e restituisce un valore booleano. Scrivere un'implementazione di tale interfaccia sotto forma di un'espressione lambda che restituisca truese il numero passato è divisibile per 13 senza resto.Attività 4 . Scrivi un'interfaccia funzionale con un metodo che accetta due stringhe e restituisce la stessa stringa. Scrivi un'implementazione di tale interfaccia sotto forma di lambda che restituisca la stringa più lunga. Compito 5 . Scrivere un'interfaccia funzionale con un metodo che accetta tre numeri frazionari: a, be crestituisce lo stesso numero frazionario. Scrivere un'implementazione di tale interfaccia sotto forma di espressione lambda che restituisca un discriminante. Chi ha dimenticato, D = b^2 - 4ac . Compito 6 . Utilizzando l'interfaccia funzionale dell'attività 5, scrivi un'espressione lambda che restituisca il risultato dell'operazione a * b^c. Popolare sulle espressioni lambda in Java. Con esempi e compiti. Parte 2.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION