JavaRush /Java Blog /Random-IT /Interfacce funzionali in Java

Interfacce funzionali in Java

Pubblicato nel gruppo Random-IT
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. Interfacce funzionali in Java - 1L'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 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 @FunctionalInterfacenon è 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 @FunctionalInterfacequesta 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
Il concetto di interfacce implica che una determinata unità di codice non può avere alcun metodo implementato. Ma a partire da Java 8 è diventato possibile utilizzare metodi statici e predefiniti nelle interfacce. I metodi statici sono legati direttamente a una classe e non richiedono che un oggetto specifico di quella classe chiami tale metodo. Cioè, questi metodi si inseriscono armoniosamente nel concetto di interfacce. Ad esempio, aggiungiamo un metodo statico per verificare la nullità di un oggetto alla classe precedente:
@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
Prima di Java 8, se avevamo bisogno di creare un metodo in un'interfaccia ereditata da altre classi, potevamo solo creare un metodo astratto che veniva implementato in ciascuna classe specifica. Ma cosa succederebbe se questo metodo fosse lo stesso per tutte le classi? In questo caso , venivano usate più spesso le classi astratte . Ma a partire da Java 8, esiste la possibilità di utilizzare interfacce con metodi implementati, i metodi predefiniti. Quando si eredita un'interfaccia, è possibile sovrascrivere questi metodi o lasciare tutto così com'è (lasciare la logica predefinita). Quando creiamo un metodo predefinito, dobbiamo aggiungere la parola chiave - 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
Nella lezione Confronto di oggetti , abbiamo parlato del fatto che tutte le classi ereditano dalla classe 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 Doge di dover creare un oggetto in base ai suoi campi Raccoon. Cioè, Converterconverte 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 in Java - 2

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 Predicateche 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 ( ): FunctionStringInteger
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 applyper 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? Interfacce funzionali in Java - 3E tale che molti metodi Streamfunzionano 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 class Stream, filterche accetta come argomento Predicatee restituisce Streamsolo gli elementi che soddisfano la condizione Predicate. Nel contesto di Stream-a, ciò significa che passa solo attraverso quegli elementi che vengono restituiti truequando utilizzati in un metodo testdi 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 evenNumberssarà composto dagli elementi {2, 4, 6, 8}. E, come ricordiamo, collectraccoglierà tutti gli elementi in una determinata raccolta: nel nostro caso, in List.

Metodo con il Consumatore

Uno dei metodi in Stream, che utilizza l'interfaccia funzionale Consumer, è peek. Ecco come apparirà il nostro esempio Consumerin 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 peekfunziona con Consumer, la modifica delle stringhe in Streamnon avverrà, ma peekritornerà Streamcon gli elementi originali: gli stessi come sono arrivati. Pertanto la lista peopleGreetingssarà 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 metodo Streamche utilizza l'interfaccia funzionale Supplierè generate, che genera una sequenza infinita basata sull'interfaccia funzionale che gli viene passata. Usiamo il nostro esempio Supplierper 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 con Streamargomento Functionè un metodo mapche 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 Functionin 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 utilizza UnaryOperatorcome 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.
Questo è come apparirà il nostro esempio 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. Interfacce funzionali in Java - 4È tutto! Sarebbe fantastico se dopo aver letto questo articolo fossi un passo avanti verso la comprensione e la padronanza dell'API Stream in Java!
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION