Ciao! Nella ricerca Java Syntax Pro, abbiamo studiato le espressioni lambda e abbiamo detto che non sono altro che un'implementazione di un metodo funzionale da un'interfaccia funzionale. In altre parole, questa è l'implementazione di una classe anonima (sconosciuta), il suo metodo non realizzato. E se nelle lezioni del corso abbiamo approfondito le manipolazioni con le espressioni lambda, ora considereremo, per così dire, l'altro lato: cioè proprio queste interfacce. L'ottava versione di Java ha introdotto il concetto di interfacce funzionali . Cos'è questo? Un'interfaccia con un metodo (astratto) non implementato è considerata funzionale. Molte interfacce già pronte rientrano in questa definizione, come ad esempio l'interfaccia discussa in precedenza Predicato
Consumatore
Fornitore
Funzione
UnaryOperator
Comparator
. E anche interfacce che creiamo noi stessi, come:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
Abbiamo un'interfaccia il cui compito è convertire oggetti di un tipo in oggetti di un altro (una sorta di adattatore). L'annotazione @FunctionalInterface
non è qualcosa di super complesso o importante, poiché il suo scopo è dire al compilatore che questa interfaccia è funzionale e non dovrebbe contenere più di un metodo. Se un'interfaccia con questa annotazione ha più di un metodo (astratto) non implementato, il compilatore non salterà questa interfaccia, poiché la percepirà come codice errato. Le interfacce senza questa annotazione possono essere considerate funzionanti e funzioneranno, ma @FunctionalInterface
questa non è altro che un'assicurazione aggiuntiva. Torniamo in classe Comparator
. Se guardi il suo codice (o documentazione ), puoi vedere che ha molti più di un metodo. Allora ti chiedi: come può allora essere considerata un'interfaccia funzionale? Le interfacce astratte possono avere metodi che non rientrano nell'ambito di un singolo metodo:
- statico
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
Avendo ricevuto questo metodo, il compilatore non si è lamentato, il che significa che la nostra interfaccia è ancora funzionante.
- metodi predefiniti
default
:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
}
Ancora una volta, vediamo che il compilatore non ha iniziato a lamentarsi e non siamo andati oltre i limiti dell'interfaccia funzionale.
- Metodi delle classi di oggetti
Object
. Ciò non si applica alle interfacce. Ma se abbiamo un metodo astratto nell'interfaccia che corrisponde alla firma con qualche metodo della classe Object
, tale metodo (o metodi) non infrangerà la restrizione della nostra interfaccia funzionale:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
boolean equals(Object obj);
}
E ancora una volta, il nostro compilatore non si lamenta, quindi l'interfaccia Converter
è ancora considerata funzionale. Ora la domanda è: perché dobbiamo limitarci a un metodo non implementato in un'interfaccia funzionale? E poi in modo che possiamo implementarlo utilizzando lambda. Diamo un'occhiata a questo con un esempio Converter
. Per fare ciò, creiamo una classe Dog
:
public class Dog {
String name;
int age;
int weight;
public Dog(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
E uno simile Raccoon
(procione):
public class Raccoon {
String name;
int age;
int weight;
public Raccoon(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
Supponiamo di avere un oggetto Dog
e di dover creare un oggetto in base ai suoi campi Raccoon
. Cioè, Converter
converte un oggetto di un tipo in un altro. Come apparirà:
public static void main(String[] args) {
Dog dog = new Dog("Bobbie", 5, 3);
Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);
Raccoon raccoon = converter.convert(dog);
System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
Quando lo eseguiamo, otteniamo il seguente output sulla console:
Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
E questo significa che il nostro metodo ha funzionato correttamente.
Interfacce funzionali Java 8 di base
Bene, ora diamo un'occhiata a diverse interfacce funzionali offerte da Java 8 e che vengono utilizzate attivamente insieme all'API Stream.Predicato
Predicate
— un'interfaccia funzionale per verificare se una determinata condizione è soddisfatta. Se la condizione è soddisfatta, restituisce true
, altrimenti - false
:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
Ad esempio, considera la creazione di un file Predicate
che verificherà la parità di un numero di tipo Integer
:
public static void main(String[] args) {
Predicate<Integer> isEvenNumber = x -> x % 2==0;
System.out.println(isEvenNumber.test(4));
System.out.println(isEvenNumber.test(3));
}
Uscita console:
true
false
Consumatore
Consumer
(dall'inglese - "consumatore") - un'interfaccia funzionale che accetta un oggetto di tipo T come argomento di input, esegue alcune azioni, ma non restituisce nulla:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
Ad esempio, consideriamo , il cui compito è inviare un messaggio di saluto alla console con l'argomento stringa passato: Consumer
public static void main(String[] args) {
Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
greetings.accept("Elena");
}
Uscita console:
Hello Elena !!!
Fornitore
Supplier
(dall'inglese - provider) - un'interfaccia funzionale che non accetta argomenti, ma restituisce un oggetto di tipo T:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Ad esempio, considera Supplier
, che produrrà nomi casuali da un elenco:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList .add("Elena");
nameList .add("John");
nameList .add("Alex");
nameList .add("Jim");
nameList .add("Sara");
Supplier<String> randomName = () -> {
int value = (int)(Math.random() * nameList.size());
return nameList.get(value);
};
System.out.println(randomName.get());
}
E se lo eseguiamo, vedremo risultati casuali da un elenco di nomi nella console.
Funzione
Function
— questa interfaccia funzionale prende un argomento T e lo trasmette a un oggetto di tipo R, che viene restituito come risultato:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
Ad esempio, prendiamo , che converte i numeri dal formato stringa ( ) al formato numero ( ): Function
String
Integer
public static void main(String[] args) {
Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
System.out.println(valueConverter.apply("678"));
}
Quando lo eseguiamo, otteniamo il seguente output sulla console:
678
PS: se passiamo non solo numeri, ma anche altri caratteri nella stringa, verrà lanciata un'eccezione - NumberFormatException
.
UnaryOperator
UnaryOperator
— un'interfaccia funzionale che prende un oggetto di tipo T come parametro, esegue alcune operazioni su di esso e restituisce il risultato delle operazioni sotto forma di un oggetto dello stesso tipo T:
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
UnaryOperator
, che utilizza il suo metodo apply
per elevare al quadrato un numero:
public static void main(String[] args) {
UnaryOperator<Integer> squareValue = x -> x * x;
System.out.println(squareValue.apply(9));
}
Uscita console:
81
Abbiamo esaminato cinque interfacce funzionali. Questo non è tutto ciò che è a nostra disposizione a partire da Java 8: queste sono le interfacce principali. Il resto di quelli disponibili sono i loro complicati analoghi. L'elenco completo è reperibile nella documentazione ufficiale Oracle .
Interfacce funzionali in Stream
Come discusso in precedenza, queste interfacce funzionali sono strettamente collegate all'API Stream. Come, chiedi? E tale che molti metodiStream
funzionano specificamente con queste interfacce funzionali. Diamo un'occhiata a come possono essere utilizzate le interfacce funzionali in Stream
.
Metodo con predicato
Ad esempio, prendiamo il metodo classStream
, filter
che accetta come argomento Predicate
e restituisce Stream
solo gli elementi che soddisfano la condizione Predicate
. Nel contesto di Stream
-a, ciò significa che passa solo attraverso quegli elementi che vengono restituiti true
quando utilizzati in un metodo test
di interfaccia Predicate
. Questo è come apparirebbe il nostro esempio Predicate
, ma per un filtro di elementi in Stream
:
public static void main(String[] args) {
List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.filter(x -> x % 2==0)
.collect(Collectors.toList());
}
Di conseguenza, l'elenco evenNumbers
sarà composto dagli elementi {2, 4, 6, 8}. E, come ricordiamo, collect
raccoglierà tutti gli elementi in una determinata raccolta: nel nostro caso, in List
.
Metodo con il Consumatore
Uno dei metodi inStream
, che utilizza l'interfaccia funzionale Consumer
, è peek
. Ecco come apparirà il nostro esempio Consumer
in Stream
:
public static void main(String[] args) {
List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
.peek(x -> System.out.println("Hello " + x + " !!!"))
.collect(Collectors.toList());
}
Uscita console:
Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
Ma poiché il metodo peek
funziona con Consumer
, la modifica delle stringhe in Stream
non avverrà, ma peek
ritornerà Stream
con gli elementi originali: gli stessi come sono arrivati. Pertanto la lista peopleGreetings
sarà composta dagli elementi “Elena”, “John”, “Alex”, “Jim”, “Sara”. Esiste anche un metodo comunemente usato foreach
, che è simile al metodo peek
, ma la differenza è che è final-terminal.
Metodo con il Fornitore
Un esempio di metodoStream
che utilizza l'interfaccia funzionale Supplier
è generate
, che genera una sequenza infinita basata sull'interfaccia funzionale che gli viene passata. Usiamo il nostro esempio Supplier
per stampare cinque nomi casuali sulla console:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList.add("Elena");
nameList.add("John");
nameList.add("Alex");
nameList.add("Jim");
nameList.add("Sara");
Stream.generate(() -> {
int value = (int) (Math.random() * nameList.size());
return nameList.get(value);
}).limit(5).forEach(System.out::println);
}
E questo è l'output che otteniamo nella console:
John
Elena
Elena
Elena
Jim
Qui abbiamo utilizzato il metodo limit(5)
per impostare un limite al metodo generate
, altrimenti il programma stamperebbe nomi casuali sulla console a tempo indeterminato.
Metodo con funzione
Un tipico esempio di metodo conStream
argomento Function
è un metodo map
che prende elementi di un tipo, fa qualcosa con essi e li trasmette, ma questi possono già essere elementi di tipo diverso. Potrebbe apparire un esempio con Function
in Stream
:
public static void main(String[] args) {
List<Integer> values = Stream.of("32", "43", "74", "54", "3")
.map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
Di conseguenza, otteniamo un elenco di numeri, ma in formato Integer
.
Metodo con UnaryOperator
Come metodo che utilizzaUnaryOperator
come argomento, prendiamo un metodo di classe Stream
- iterate
. Questo metodo è simile al metodo generate
: anch'esso genera una sequenza infinita ma ha due argomenti:
- il primo è l'elemento da cui inizia la generazione della sequenza;
- il secondo è
UnaryOperator
, che indica il principio di generare nuovi elementi dal primo elemento.
UnaryOperator
, ma nel metodo iterate
:
public static void main(String[] args) {
Stream.iterate(9, x -> x * x)
.limit(4)
.forEach(System.out::println);
}
Quando lo eseguiamo, otteniamo il seguente output sulla console:
9
81
6561
43046721
Cioè ciascuno dei nostri elementi viene moltiplicato per se stesso e così via per i primi quattro numeri. È tutto! Sarebbe fantastico se dopo aver letto questo articolo fossi un passo avanti verso la comprensione e la padronanza dell'API Stream in Java!
GO TO FULL VERSION