JavaRush /Blog Java /Random-FR /Interfaces fonctionnelles en Java

Interfaces fonctionnelles en Java

Publié dans le groupe Random-FR
Bonjour! Dans la quête Java Syntax Pro, nous avons étudié les expressions lambda et dit qu'elles ne sont rien de plus qu'une implémentation d'une méthode fonctionnelle à partir d'une interface fonctionnelle. En d'autres termes, il s'agit de l'implémentation d'une classe anonyme (inconnue), sa méthode non réalisée. Et si dans les cours du cours nous avons abordé les manipulations avec des expressions lambda, nous allons maintenant considérer, pour ainsi dire, l'autre côté : à savoir ces mêmes interfaces. Interfaces fonctionnelles en Java - 1La huitième version de Java a introduit le concept d' interface fonctionnelle . Qu'est-ce que c'est? Une interface avec une méthode (abstraite) non implémentée est considérée comme fonctionnelle. De nombreuses interfaces prêtes à l'emploi relèvent de cette définition, comme, par exemple, l'interface évoquée précédemment Comparator. Et aussi des interfaces que nous créons nous-mêmes, comme :
@FunctionalInterface
public interface Converter<T, N> {
   N convert(T t);
}
Nous avons une interface dont la tâche est de convertir des objets d'un type en objets d'un autre (une sorte d'adaptateur). L'annotation @FunctionalInterfacen'est pas quelque chose de super complexe ou important, puisque son but est d'indiquer au compilateur que cette interface est fonctionnelle et ne doit pas contenir plus d'une méthode. Si une interface avec cette annotation a plus d'une méthode (abstraite) non implémentée, le compilateur ne sautera pas cette interface, car il la percevra comme du code erroné. Les interfaces sans cette annotation peuvent être considérées comme fonctionnelles et fonctionneront, mais @FunctionalInterfacece n'est rien de plus qu'une assurance supplémentaire. Revenons en classe Comparator. Si vous regardez son code (ou sa documentation ), vous pouvez voir qu'il comporte plusieurs méthodes. Alors vous demandez : comment, alors, peut-elle être considérée comme une interface fonctionnelle ? Les interfaces abstraites peuvent avoir des méthodes qui n'entrent pas dans le cadre d'une seule méthode :
  • statique
Le concept d'interfaces implique qu'une unité de code donnée ne peut avoir aucune méthode implémentée. Mais à partir de Java 8, il est devenu possible d'utiliser des méthodes statiques et par défaut dans les interfaces. Les méthodes statiques sont directement liées à une classe et ne nécessitent pas d'objet spécifique de cette classe pour appeler une telle méthode. Autrement dit, ces méthodes s'intègrent harmonieusement dans le concept d'interfaces. À titre d'exemple, ajoutons une méthode statique pour vérifier la valeur null d'un objet à la classe précédente :
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }
}
Ayant reçu cette méthode, le compilateur ne s'est pas plaint, ce qui signifie que notre interface est toujours fonctionnelle.
  • méthodes par défaut
Avant Java 8, si nous devions créer une méthode dans une interface héritée par d'autres classes, nous ne pouvions créer qu'une méthode abstraite implémentée dans chaque classe spécifique. Mais et si cette méthode était la même pour toutes les classes ? Dans ce cas , les classes abstraites ont été le plus souvent utilisées . Mais à partir de Java 8, il existe une option permettant d'utiliser des interfaces avec des méthodes implémentées - les méthodes par défaut. Lors de l'héritage d'une interface, vous pouvez remplacer ces méthodes ou tout laisser tel quel (laisser la logique par défaut). Lors de la création d'une méthode par défaut, il faut ajouter le mot-clé -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());
   }
}
Encore une fois, nous constatons que le compilateur n'a pas commencé à se plaindre et que nous ne sommes pas allés au-delà des limites de l'interface fonctionnelle.
  • Méthodes de classe d'objet
Dans la conférence Comparaison d'objets , nous avons parlé du fait que toutes les classes héritent de la classe Object. Cela ne s'applique pas aux interfaces. Mais si nous avons une méthode abstraite dans l'interface qui fait correspondre la signature avec une méthode de la classe Object, une telle méthode (ou méthodes) ne brisera pas notre restriction d'interface fonctionnelle :
@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);
}
Et encore une fois, notre compilateur ne se plaint pas, donc l'interface Converterest toujours considérée comme fonctionnelle. Maintenant, la question est : pourquoi devons-nous nous limiter à une seule méthode non implémentée dans une interface fonctionnelle ? Et puis pour que nous puissions l'implémenter en utilisant des lambdas. Regardons cela avec un exemple Converter. Pour ce faire, créons une 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;
  }
}
Et un similaire Raccoon(raton laveur) :
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;
  }
}
Supposons que nous ayons un objet Doget que nous devions créer un objet basé sur ses champs Raccoon. Autrement dit, Converteril convertit un objet d'un type en un autre. À quoi cela ressemblera-t-il :
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);
}
Lorsque nous l'exécutons, nous obtenons le résultat suivant sur la console :

Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
Et cela signifie que notre méthode a fonctionné correctement.Interfaces fonctionnelles en Java - 2

Interfaces fonctionnelles de base Java 8

Eh bien, examinons maintenant plusieurs interfaces fonctionnelles que Java 8 nous a apportées et qui sont activement utilisées en conjonction avec l'API Stream.

Prédicat

Predicate— une interface fonctionnelle pour vérifier si une certaine condition est remplie. Si la condition est remplie, renvoie true, sinon -false :
@FunctionalInterface
public interface Predicate<T> {
   boolean test(T t);
}
A titre d'exemple, envisagez de créer un Predicatequi vérifiera la parité d'un certain nombre de typesInteger :
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));
}
Sortie de la console :

true
false

Consommateur

Consumer(de l'anglais - « consommateur ») - une interface fonctionnelle qui prend un objet de type T comme argument d'entrée, effectue certaines actions, mais ne renvoie rien :
@FunctionalInterface
public interface Consumer<T> {
   void accept(T t);
}
À titre d'exemple, considérons , dont la tâche est d'afficher un message d'accueil sur la console avec l'argument de chaîne transmis : Consumer
public static void main(String[] args) {
   Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
   greetings.accept("Elena");
}
Sortie de la console :

Hello Elena !!!

Fournisseur

Supplier(de l'anglais - fournisseur) - une interface fonctionnelle qui ne prend aucun argument, mais renvoie un objet de type T :
@FunctionalInterface
public interface Supplier<T> {
   T get();
}
À titre d'exemple, considérons Supplier, qui produira des noms aléatoires à partir d'une liste :
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());
}
Et si nous exécutons ceci, nous verrons des résultats aléatoires à partir d'une liste de noms dans la console.

Fonction

Function— cette interface fonctionnelle prend un argument T et le convertit en un objet de type R, qui est renvoyé comme résultat :
@FunctionalInterface
public interface Function<T, R> {
   R apply(T t);
}
À titre d'exemple, prenons , qui convertit les nombres du format chaîne ( ) au format numérique ( ) : FunctionStringInteger
public static void main(String[] args) {
   Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
   System.out.println(valueConverter.apply("678"));
}
Lorsque nous l'exécutons, nous obtenons le résultat suivant sur la console :

678
PS : si nous transmettons non seulement des nombres, mais également d'autres caractères dans la chaîne, une exception sera levée - NumberFormatException.

Opérateur unaire

UnaryOperator— une interface fonctionnelle qui prend en paramètre un objet de type T, effectue quelques opérations sur celui-ci et renvoie le résultat des opérations sous la forme d'un objet de même type T :
@FunctionalInterface
public interface UnaryOperator<T> {
   T apply(T t);
}
UnaryOperator, qui utilise sa méthode applypour mettre au carré un nombre :
public static void main(String[] args) {
   UnaryOperator<Integer> squareValue = x -> x * x;
   System.out.println(squareValue.apply(9));
}
Sortie de la console :

81
Nous avons examiné cinq interfaces fonctionnelles. Ce n'est pas tout ce qui nous est disponible à partir de Java 8 - ce sont les principales interfaces. Les autres disponibles sont leurs analogues compliqués. La liste complète peut être trouvée dans la documentation officielle d'Oracle .

Interfaces fonctionnelles dans Stream

Comme indiqué ci-dessus, ces interfaces fonctionnelles sont étroitement couplées à l'API Stream. Comment, demandez-vous ? Interfaces fonctionnelles en Java - 3Et de telle sorte que de nombreuses méthodes Streamfonctionnent spécifiquement avec ces interfaces fonctionnelles. Voyons comment les interfaces fonctionnelles peuvent être utilisées dans Stream.

Méthode avec prédicat

Par exemple, prenons la méthode de classe Stream- filterqui prend comme argument Predicateet renvoie Streamuniquement les éléments qui satisfont à la condition Predicate. Dans le contexte de Stream-a, cela signifie qu'il ne passe que par les éléments renvoyés truelorsqu'ils sont utilisés dans une méthode testd'interface Predicate. Voici à quoi ressemblerait notre exemple Predicate, mais pour un filtre d'éléments dansStream :
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());
}
En conséquence, la liste evenNumberssera composée d'éléments {2, 4, 6, 8}. Et, comme nous nous en souvenons, collectil rassemblera tous les éléments dans une certaine collection : dans notre cas, dans List.

Méthode avec Consommateur

L'une des méthodes de Stream, qui utilise l'interface fonctionnelle Consumer, est la méthode peek. Voici à quoi ressemblera notre exemple ConsumerpourStream :
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());
}
Sortie de la console :

Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
Mais comme la méthode peekfonctionne avec Consumer, la modification des chaînes dans Streamn'aura pas lieu, mais peekreviendra Streamavec les éléments d'origine : les mêmes qu'ils y sont parvenus. Par conséquent, la liste peopleGreetingssera composée des éléments "Elena", "John", "Alex", "Jim", "Sara". Il existe également une méthode couramment utilisée foreach, qui est similaire à la méthode peek, mais la différence est qu'elle est finale - terminale.

Méthode avec Fournisseur

Un exemple de méthode Streamutilisant l'interface fonctionnelle Supplierest generate, qui génère une séquence infinie basée sur l'interface fonctionnelle qui lui est transmise. Utilisons notre exemple Supplierpour imprimer cinq noms aléatoires sur la 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);
}
Et voici le résultat que nous obtenons dans la console :

John
Elena
Elena
Elena
Jim
Ici, nous avons utilisé la méthode limit(5)pour définir une limite sur la méthode generate, sinon le programme imprimerait indéfiniment des noms aléatoires sur la console.

Méthode avec fonction

Un exemple typique de méthode avec Streamargument Functionest une méthode mapqui prend des éléments d’un type, en fait quelque chose et les transmet, mais il peut déjà s’agir d’éléments d’un type différent. À quoi pourrait ressembler un exemple avec FunctioninStream :
public static void main(String[] args) {
   List<Integer> values = Stream.of("32", "43", "74", "54", "3")
           .map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
En conséquence, nous obtenons une liste de nombres, mais en Integer.

Méthode avec UnaryOperator

En tant que méthode utilisée UnaryOperatorcomme argument, prenons une méthode de classe Stream- iterate. Cette méthode est similaire à la méthode generate: elle génère également une séquence infinie mais possède deux arguments :
  • le premier est l’élément à partir duquel commence la génération de séquence ;
  • le second est UnaryOperator, qui indique le principe de génération de nouveaux éléments à partir du premier élément.
Voici à quoi ressemblera notre exemple UnaryOperator, mais dans la méthode iterate:
public static void main(String[] args) {
   Stream.iterate(9, x -> x * x)
           .limit(4)
           .forEach(System.out::println);
}
Lorsque nous l'exécutons, nous obtenons le résultat suivant sur la console :

9
81
6561
43046721
Autrement dit, chacun de nos éléments est multiplié par lui-même, et ainsi de suite pour les quatre premiers nombres. Interfaces fonctionnelles en Java - 4C'est tout! Ce serait formidable si, après avoir lu cet article, vous étiez sur le point de comprendre et de maîtriser l'API Stream en Java !
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION