1. Interfacce
Per capire cosa sono le funzioni lambda, devi prima capire cosa sono le interfacce. Quindi, ricordiamo i punti principali.
Un'interfaccia è una variazione del concetto di classe . Una classe pesantemente troncata, diciamo. A differenza di una classe, un'interfaccia non può avere le proprie variabili (eccetto quelle statiche). Inoltre, non puoi creare oggetti il cui tipo è un'interfaccia:
- Non puoi dichiarare variabili della classe
- Non puoi creare oggetti
Esempio:
interface Runnable
{
void run();
}
Utilizzando un'interfaccia
Allora perché è necessaria un'interfaccia? Le interfacce vengono utilizzate solo insieme all'ereditarietà. La stessa interfaccia può essere ereditata da classi diverse o, come si dice anche, le classi implementano l' interfaccia .
Se una classe implementa un'interfaccia, deve implementare i metodi dichiarati ma non implementati dall'interfaccia. Esempio:
interface Runnable
{
void run();
}
class Timer implements Runnable
{
void run()
{
System.out.println(LocalTime.now());
}
}
class Calendar implements Runnable
{
void run()
{
var date = LocalDate.now();
System.out.println("Today: " + date.getDayOfWeek());
}
}
La Timer
classe implementa l' Runnable
interfaccia, quindi deve dichiarare al suo interno tutti i metodi che sono nell'interfaccia Runnable
e implementarli, cioè scrivere codice nel corpo di un metodo. Lo stesso vale per la Calendar
classe.
Ma ora Runnable
le variabili possono memorizzare riferimenti a oggetti che implementano l' Runnable
interfaccia.
Esempio:
Codice | Nota |
---|---|
|
Verrà chiamato il run() metodo nella classe Verrà chiamato il metodo nella classe Verrà chiamato il metodo nella classeTimer run() Timer run() Calendar |
È sempre possibile assegnare un riferimento a un oggetto a una variabile di qualsiasi tipo, purché tale tipo sia una delle classi antenate dell'oggetto. Per le classi Timer
e Calendar
esistono due tipi di questo tipo: Object
e Runnable
.
Se assegni un riferimento a un oggetto a una Object
variabile, puoi chiamare solo i metodi dichiarati nella Object
classe. E se assegni un riferimento a un oggetto a una Runnable
variabile, puoi chiamare i metodi del Runnable
tipo.
Esempio 2:
ArrayList<Runnable> list = new ArrayList<Runnable>();
list.add (new Timer());
list.add (new Calendar());
for (Runnable element: list)
element.run();
Questo codice funzionerà, perché gli oggetti Timer
e Calendar
hanno metodi di esecuzione che funzionano perfettamente. Quindi, chiamarli non è un problema. Se avessimo appena aggiunto un metodo run() a entrambe le classi, non saremmo in grado di chiamarle in un modo così semplice.
Fondamentalmente, l' Runnable
interfaccia viene utilizzata solo come luogo in cui inserire il metodo run.
2. Ordinamento
Passiamo a qualcosa di più pratico. Ad esempio, diamo un'occhiata all'ordinamento delle stringhe.
Per ordinare una raccolta di stringhe in ordine alfabetico, Java ha un ottimo metodo chiamatoCollections.sort(collection);
Questo metodo statico ordina la raccolta passata. E nel processo di ordinamento, esegue confronti a coppie dei suoi elementi per capire se gli elementi devono essere scambiati.
Durante l'ordinamento, questi confronti vengono eseguiti utilizzando il compareTo
metodo (), che hanno tutte le classi standard: Integer
, String
, ...
Il metodo compareTo() della classe Integer confronta i valori di due numeri, mentre il metodo compareTo() della classe String esamina l'ordine alfabetico delle stringhe.
Quindi una raccolta di numeri verrà ordinata in ordine crescente, mentre una raccolta di stringhe verrà ordinata alfabeticamente.
Algoritmi alternativi di ordinamento
Ma cosa succede se vogliamo ordinare le stringhe non in ordine alfabetico, ma in base alla loro lunghezza? E se volessimo ordinare i numeri in ordine decrescente? Cosa fai in questo caso?
Per gestire tali situazioni, la Collections
classe ha un altro sort()
metodo che ha due parametri:
Collections.sort(collection, comparator);
Dove comparator è un oggetto speciale che sa come confrontare gli oggetti in una raccolta durante un'operazione di ordinamento . Il termine comparator deriva dalla parola inglese comparator , che a sua volta deriva da compare , che significa "confrontare".
Allora, qual è questo oggetto speciale?
Comparator
interfaccia
Beh, è tutto molto semplice. Il tipo del sort()
secondo parametro del metodo èComparator<T>
Dove T è un parametro di tipo che indica il tipo degli elementi nella raccolta ed Comparator
è un'interfaccia con un solo metodoint compare(T obj1, T obj2);
In altre parole, un oggetto comparatore è qualsiasi oggetto di qualsiasi classe che implementa l'interfaccia Comparator. L'interfaccia di Comparator sembra molto semplice:
public interface Comparator<Type>
{
public int compare(Type obj1, Type obj2);
}
Il compare()
metodo confronta i due argomenti che gli vengono passati.
Se il metodo restituisce un numero negativo, significa obj1 < obj2
. Se il metodo restituisce un numero positivo, significa obj1 > obj2
. Se il metodo restituisce 0, significa obj1 == obj2
.
Ecco un esempio di un oggetto comparatore che confronta le stringhe in base alla loro lunghezza:
public class StringLengthComparator implements Comparator<String>
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
}
Per confrontare le lunghezze delle stringhe, sottrarre semplicemente una lunghezza dall'altra.
Il codice completo per un programma che ordina le stringhe per lunghezza sarebbe simile a questo:
public class Solution
{
public static void main(String[] args)
{
ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list, "Hello", "how's", "life?");
Collections.sort(list, new StringLengthComparator());
}
}
class StringLengthComparator implements Comparator<String>
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
}
3. Zucchero sintattico
Cosa ne pensi, questo codice può essere scritto in modo più compatto? Fondamentalmente, c'è solo una riga che contiene informazioni utili: obj1.length() - obj2.length();
.
Ma il codice non può esistere al di fuori di un metodo, quindi abbiamo dovuto aggiungere un compare()
metodo e per memorizzare il metodo abbiamo dovuto aggiungere una nuova classe — StringLengthComparator
. E dobbiamo anche specificare i tipi delle variabili... Tutto sembra essere corretto.
Ma ci sono modi per rendere questo codice più breve. Abbiamo un po' di zucchero sintattico per te. Due scoop!
Classe interna anonima
Puoi scrivere il codice del comparatore direttamente all'interno del main()
metodo e il compilatore farà il resto. Esempio:
public class Solution
{
public static void main(String[] args)
{
ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list, "Hello", "how's", "life?");
Comparator<String> comparator = new Comparator<String>()
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
};
Collections.sort(list, comparator);
}
}
Puoi creare un oggetto che implementa l' Comparator
interfaccia senza creare esplicitamente una classe! Il compilatore lo creerà automaticamente e gli darà un nome temporaneo. Confrontiamo:
Comparator<String> comparator = new Comparator<String>()
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
};
Comparator<String> comparator = new StringLengthComparator();
class StringLengthComparator implements Comparator<String>
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
}
Lo stesso colore viene utilizzato per indicare blocchi di codice identici nei due diversi casi. Le differenze sono abbastanza piccole in pratica.
Quando il compilatore incontra il primo blocco di codice, genera semplicemente un secondo blocco di codice corrispondente e assegna alla classe un nome casuale.
4. Espressioni lambda in Java
Supponiamo che tu decida di utilizzare una classe interna anonima nel tuo codice. In questo caso, avrai un blocco di codice come questo:
Comparator<String> comparator = new Comparator<String>()
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
};
Qui combiniamo la dichiarazione di una variabile con la creazione di una classe anonima. Ma c'è un modo per accorciare questo codice. Ad esempio, in questo modo:
Comparator<String> comparator = (String obj1, String obj2) ->
{
return obj1.length() – obj2.length();
};
Il punto e virgola è necessario perché qui non abbiamo solo una dichiarazione di classe implicita, ma anche la creazione di una variabile.
Una notazione come questa è chiamata espressione lambda.
Se il compilatore incontra una notazione come questa nel tuo codice, genera semplicemente la versione dettagliata del codice (con una classe interna anonima).
Si noti che durante la scrittura dell'espressione lambda, abbiamo omesso non solo il nome della classe, ma anche il nome del metodo.Comparator<String>
int compare()
La compilazione non avrà problemi a determinare il metodo , perché un'espressione lambda può essere scritta solo per le interfacce che hanno un solo metodo . A proposito, c'è un modo per aggirare questa regola, ma lo imparerai quando inizierai a studiare OOP in modo più approfondito (stiamo parlando di metodi predefiniti).
Diamo un'occhiata di nuovo alla versione dettagliata del codice, ma elimineremo in grigio la parte che può essere omessa durante la scrittura di un'espressione lambda:
Comparator<String> comparator = new Comparator<String>()
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
};
Sembra che non sia stato omesso nulla di importante. Infatti, se l' Comparator
interfaccia ha un solo compare()
metodo, il compilatore può recuperare interamente il codice disattivato dal codice rimanente.
Ordinamento
A proposito, ora possiamo scrivere il codice di ordinamento in questo modo:
Comparator<String> comparator = (String obj1, String obj2) ->
{
return obj1.length() – obj2.length();
};
Collections.sort(list, comparator);
O anche così:
Collections.sort(list, (String obj1, String obj2) ->
{
return obj1.length() – obj2.length();
}
);
Abbiamo semplicemente sostituito immediatamente la comparator
variabile con il valore assegnato alla comparator
variabile.
Tipo di inferenza
Ma non è tutto. Il codice in questi esempi può essere scritto in modo ancora più compatto. Innanzitutto, il compilatore può determinare da sé che le variabili obj1
e obj2
sono Strings
. E in secondo luogo, le parentesi graffe e l'istruzione return possono anche essere omesse se si dispone di un solo comando nel codice del metodo.
La versione abbreviata sarebbe così:
Comparator<String> comparator = (obj1, obj2) ->
obj1.length() – obj2.length();
Collections.sort(list, comparator);
E se invece di usare la comparator
variabile, usiamo immediatamente il suo valore, otteniamo la seguente versione:
Collections.sort(list, (obj1, obj2) -> obj1.length() — obj2.length() );
Beh, cosa ne pensi? Solo una riga di codice senza informazioni superflue, solo variabili e codice. Non c'è modo di accorciarlo! O c'è?
5. Come funziona
In effetti, il codice può essere scritto in modo ancora più compatto. Ma ne parleremo più avanti.
Puoi scrivere un'espressione lambda in cui utilizzeresti un tipo di interfaccia con un singolo metodo.
Ad esempio, nel codice , puoi scrivere un'espressione lambda perché la firma del metodo è così:Collections.sort(list, (obj1, obj2) -> obj1.length() - obj2.length());
sort()
sort(Collection<T> colls, Comparator<T> comp)
Quando abbiamo passato la ArrayList<String>
raccolta come primo argomento al metodo sort, il compilatore è stato in grado di determinare che il tipo del secondo argomento è . E da questo, ha concluso che questa interfaccia ha un unico metodo. Tutto il resto è un tecnicismo.Comparator<String>
int compare(String obj1, String obj2)
GO TO FULL VERSION