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. La 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 Prédicat
Consommateur
Fournisseur
Fonction
Opérateur unaire
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 @FunctionalInterface
n'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 @FunctionalInterface
ce 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
@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
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
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 Converter
est 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 Dog
et que nous devions créer un objet basé sur ses champs Raccoon
. Autrement dit, Converter
il 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 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 Predicate
qui 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 ( ) : Function
String
Integer
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 apply
pour 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 ? Et de telle sorte que de nombreuses méthodesStream
fonctionnent 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 classeStream
- filter
qui prend comme argument Predicate
et renvoie Stream
uniquement 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 true
lorsqu'ils sont utilisés dans une méthode test
d'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 evenNumbers
sera composée d'éléments {2, 4, 6, 8}. Et, comme nous nous en souvenons, collect
il rassemblera tous les éléments dans une certaine collection : dans notre cas, dans List
.
Méthode avec Consommateur
L'une des méthodes deStream
, qui utilise l'interface fonctionnelle Consumer
, est la méthode peek
. Voici à quoi ressemblera notre exemple Consumer
pourStream
:
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 peek
fonctionne avec Consumer
, la modification des chaînes dans Stream
n'aura pas lieu, mais peek
reviendra Stream
avec les éléments d'origine : les mêmes qu'ils y sont parvenus. Par conséquent, la liste peopleGreetings
sera 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éthodeStream
utilisant l'interface fonctionnelle Supplier
est generate
, qui génère une séquence infinie basée sur l'interface fonctionnelle qui lui est transmise. Utilisons notre exemple Supplier
pour 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 avecStream
argument Function
est une méthode map
qui 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 Function
inStream
:
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éeUnaryOperator
comme 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.
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. C'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 !
GO TO FULL VERSION