Per chi è questo articolo?
Se 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?
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 a
- È 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 - 1](https://cdn.codegym.cc/images/article/7c4222bd-fcde-4993-a190-a7ab1836b4d3/800.jpeg)
-
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.
- Conoscenza di Java Core: tipi generici (generics), collezioni (liste), thread.
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, Comparator
o 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 List
oggetto è digitato usando String[
]), allora non devi indicarne i tipi.
Se sono ambigui, indicare il tipo. IDEA lo colorerà di grigio se non è necessario. |
return
dichiarazione. 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 ( return
viene 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 return
la 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 Runnable
oggetto e poi passarla al Thread
costruttore:
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' Runnable
interfaccia ci dice che i suoi oggetti devono avere un public void run()
metodo. Secondo l'interfaccia, il run
metodo 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 Runnable
dell'interfaccia run()
. Ecco perché siamo stati in grado di inserire questa espressione lambda in una Runnable
variabile. Esempio 4.
() -> 42
Ancora una volta, non prende nulla, ma restituisce il numero 42. Tale espressione lambda può essere inserita in una Callable
variabile, 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 void
tipo restituito (perché non c'è alcuna return
istruzione). Esempio 6
x -> x
Qui prendiamo una x
variabile 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 x
e 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 x
e y
e restituiamo il resto della divisione di x
per 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 Cat
oggetto, un String
nome e un'età intera. Nel metodo stesso, usiamo il nome passato e l'età per impostare le variabili sul cat. Poiché il nostro cat
oggetto è 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' Cat
oggetto 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 Dog
classe che implementi anche HasNameAndAge
, allora possiamo eseguire le stesse operazioni nel Dog
metodo 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
GO TO FULL VERSION