2024-12-03
List<Student> students = Arrays.asList(
new Student("Alice", Arrays.asList(
new Course("Math", 18.5),
new Course("Physics", 17.0)
)),
new Student("Bob", Arrays.asList(
new Course("Math", 12.0),
new Course("Physics", 13.5)
)),
new Student("Charlie", Arrays.asList(
new Course("Math", 15.0),
new Course("Physics", 16.0)
))
);
// Version fonctionnelle : Trouver les noms des étudiants ayant une moyenne > 15 en Math
List<String> highPerformers = students.stream()
.filter(student -> student.courses.stream()
.filter(course -> course.name.equals("Math"))
.mapToDouble(course -> course.grade)
.average()
.orElse(0.0) > 15.0)
.map(student -> student.name)
.collect(Collectors.toList());
highPerformers;
[Alice]
// Version impérative équivalente
List<String> highPerformersImperative = new ArrayList<>();
for (Student student : students) {
double mathSum = 0.0;
int mathCount = 0;
for (Course course : student.courses) {
if (course.name.equals("Math")) {
mathSum += course.grade;
mathCount++;
}
}
if (mathCount > 0 && (mathSum / mathCount) > 15.0) {
highPerformersImperative.add(student.name);
}
}
highPerformersImperative
[Alice]
Définition: Une interface avec une seule méthode abstraite.
https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/function/package-summary.html
Interfaces génériques
Function<T, R>
: Applique une fonction à un argument de type T et retourne un résultat de type R.Consumer<T>
: Consomme un argument de type T.Predicate<T>
: Représente une condition sur un élément de type T.Supplier<T>
: Fournit un élément de type T.BiFunction<T,U>
- like Function but with two parameters.BiConsumer<T,U>
- like Consumer but with two parameters.IntFunction<R>
: Accepte un int et retourne un résultat de type R.IntConsumer
: Consomme un int.IntPredicate
: Représente une condition sur un int.IntSupplier
: Fournit un int.LongFunction<R>
, LongConsumer, LongPredicate, LongSupplierDoubleFunction<R>
, DoubleConsumer, DoublePredicate, DoubleSupplierBooleanSupplier
Elles peuvent être vues comme des instances de classes anonymes n’ayant qu’une seule méthode dont les types de retour et des paramètres sont inférés.
Définition: Une lambda expression est une fonction anonyme qui peut être assignée à une variable. C’est une manière concise d’écrire une instance d’une interface fonctionnelle.
Elles peuvent être vues comme des instances de classes anonymes n’ayant qu’une seule méthode dont les types de retour et des paramètres sont inférés.
Syntaxe:
Une lambda expression est composée :
->
(flèche lambda)return
et les accolades sont facultatifs.import java.util.function.*;
DoubleBinaryOperator add = (x, y) -> x + y; // Additionne deux nombres
DoubleBinaryOperator mul = (x, y) -> x * y; // Additionne deux nombres
void apply(DoubleBinaryOperator operator, double x, double y) {
logger.info("Résultat: " + operator.applyAsDouble(x, y));
}
apply(add, 2.0, 3.0);
apply(mul, 2.0, 3.0);
apply((x, y) -> x * x + y * y, 2.0, 3.0);
18:36:25.889 [IJava-executor-1] INFO notebook -- Résultat: 5.0
18:36:25.906 [IJava-executor-1] INFO notebook -- Résultat: 6.0
18:36:25.924 [IJava-executor-1] INFO notebook -- Résultat: 13.0
Définition: Un record est une classe qui est principalement utilisée pour contenir des données. Il est idéal pour représenter des données immuables.
Pourquoi utiliser des records ?
equals()
, hashCode()
et toString()
, basées sur les champs du record.::
Qu’est-ce que l’opérateur ::
?
C’est un raccourci syntaxique en Java qui permet de créer une référence à une méthode ou un constructeur. Cette référence peut ensuite être utilisée comme si c’était une valeur, par exemple pour l’assigner à une variable ou pour la passer en argument à une méthode.
On peut ainsi définir une méthode qui ajoute des Personnes à une liste qui est créée à partir d’un Supplier qui fournit une liste de Personne en paramètre.
// Define two Supplier<List> lambdas:
Supplier<List> listFactory1 = ArrayList::new; // Creates a Supplier that provides a new ArrayList
Supplier<List> listFactory2 = LinkedList::new; // Creates a Supplier that provides a new LinkedList
// Function to initialize a List of Person objects:
public List<Person> initPersonList(Supplier<List> listFactory) {
// Get a new List instance from the provided Supplier
List<Person> personList = listFactory.get();
// Add some Person objects to the list
personList.add(new Person("Alice", 30));
personList.add(new Person("Bob", 25));
// Return the populated list
return personList;
}
// Demonstrate usage with different Suppliers:
logger.info("List using ArrayList: {}", initPersonList(listFactory1));
logger.info("List using LinkedList: {}", initPersonList(listFactory2));
18:36:35.432 [IJava-executor-1] INFO notebook -- List using ArrayList: [Person[nom=Alice, age=30], Person[nom=Bob, age=25]]
18:36:35.452 [IJava-executor-1] INFO notebook -- List using LinkedList: [Person[nom=Alice, age=30], Person[nom=Bob, age=25]]
Avec une liste d’instances de la classe Person(prenom, age), si la classe redéfini equals
et hashcode
et implante l’interface Comparable
en fonction du prenom on peut trier “canoniquement” la liste par nom avec Collections.sort(personnes);
(impossible ici avec un record).
Comparator
Person
(possédant les attributs ‘prénom’ et ‘âge’)Person
redéfinit les méthodes equals
et hashCode
et implémente l’interface Comparable
en se basant sur le prénomCollections.sort(personnes)
pour trier (pas directement applicable aux records).Pour trier des records ou par un autre critère par exemple l’âge, on peut implanter l’interface Comparator
et en fournir une instance à la méthode de classe sort
.
classe anonyme
Si le comparateur ne doit être utilisé qu’une fois on peut utiliser une classe anonyme.
//Tri par nom, puis age
List<Person> listByNomAge = new ArrayList<Person>(personnes);
Collections.sort(listByNomAge, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.nom()
.equals(o2.nom())
?o1.age()-o2.age()
:o1.nom().compareTo(o2.nom());
}
});
listByNomAge;
[Person[nom=Alice, age=30], Person[nom=Bob, age=25]]
lambda
// Tri par nom en utilisant une lambda expression
List<Person> listByNames = new ArrayList<Person>(personnes);
Collections.sort(listByNames, (o1, o2) -> o1.nom().compareTo(o2.nom()));
logger.info("Tri par nom: {}",listByNames);
// Tri par nom en utilisant une clé de tri
List<Person> listByAges = new ArrayList<Person>(personnes);
listByNames.sort(Comparator.comparing(Person::nom));
logger.info("Tri par age: {}",listByNames);
// Tri par âge
List<Person> listByAges = new ArrayList<Person>(personnes);
listByAges.sort(Comparator.comparingInt(Person::age));
logger.info("{}",listByAges);
18:37:02.832 [IJava-executor-1] INFO notebook -- Tri par nom: [Person[nom=Alice, age=30], Person[nom=Bob, age=25]]
18:37:02.876 [IJava-executor-1] INFO notebook -- Tri par age: [Person[nom=Alice, age=30], Person[nom=Bob, age=25]]
18:37:02.913 [IJava-executor-1] INFO notebook -- [Person[nom=Bob, age=25], Person[nom=Alice, age=30]]
Comparator
L’interface Comparator
fournit de nombreuses méthodes de comparaisons fonctionnelles :
// Sort list of Person objects in reverse order:
// 1. Primary sort by name (nom)
// 2. Secondary sort by age for same names
Collections.sort(listByNomAge, Comparator.comparing(Person::nom)
.thenComparing(Person::age) // Break ties by age
.reversed()); // Reverse the entire ordering
// Return the sorted list (descending by name then age)
listByNomAge;
[Person[nom=Bob, age=25], Person[nom=Alice, age=30]]
Nous allons voir des exemples avec quatre interfaces courament utilisées : Function
, Predicate
, Supplier
et Consumer
.
<T,R>
(apply)L’interface Function applique un traitement sur son unique paramètre.
Le nom de l’unique méthode d’une Function
est apply
.
.andThen()
method
.compose()
executes functions in reverse order:import java.util.function.Function;
// Email formatter functions
// 1. Basic Functions
Function<String, String> addDomain = name -> name + "@company.com";
Function<String, String> makeValidEmail = String::toLowerCase;
// 2. Composition with andThen
Function<String, String> createEmail = makeValidEmail.andThen(addDomain);
// Test with different inputs
System.out.println("Simple: " + addDomain.apply("john.doe"));
System.out.println("Lowercase: " + makeValidEmail.apply("JOHN.DOE"));
System.out.println("andThen: " + createEmail.apply("John.Doe"));
System.out.println("compose: " + addDomain.compose(makeValidEmail).apply("JOHN.DOE"));
Simple: john.doe@company.com
Lowercase: john.doe
andThen: john.doe@company.com
compose: john.doe@company.com
On peut alors définir des méthodes qui prennent en paramètre une instance d’une interface fonctionnelle.
Ici une méthode qui parcourt la liste de personnes, applique la Function sur le prénom et l’affiche.
public class AfficheurDePersonnes {
public static void appliqueEtAfficher(Function<String,String> fonction) {
for (Person p : personnes) System.out.println(fonction.apply(p.nom()));
}
}
AfficheurDePersonnes.appliqueEtAfficher(createEmail);
alice@company.com
bob@company.com
On peut aussi directement utiliser une lambda de types compatibles (noter que les types de retour et du paramètre s
sont inférés à partir des paramètres de l’interface fonctionnelle attendue).
<T>
(get)Un Supplier est une fonction sans paramètre qui produit une valeur et possède donc un seul paramètre générique (le type de l’objet retourné). C’est une sorte de factory.
Ainsi Supplier<String>
fournis des chaines de caratères, Supplier<Integer>
des objets représentant des entiers, … .
Utilisation :
Pour les primitifs, il existe des variantes dédiées de l’interface (donc sans paramètres) comme IntSupplier.
import java.util.function.Supplier;
Random random = new Random();
// Different dice types using method reference
Supplier<Integer> D6 = () -> random.nextInt(6) + 1;
Supplier<Double> RandomTemperature = () -> random.nextDouble()*50 - 19;
System.out.println("D6 Roll: " + D6.get());
System.out.println("Random Temperature: " + RandomTemperature.get());
public class Statistique<T extends Number> {
public static <T extends Number> double average(Supplier<T> supplier, int iterations) {
double sum = 0.0;
for (int i = 0; i < iterations; i++) {
sum += supplier.get().doubleValue();
}
return sum / iterations;
}
}
final int NB = 10;
logger.info("Random dices: {}", Statistique.average(D6, NB)+" (%d rolls)".formatted(NB));
logger.info("Random temperatures: {}", Statistique.average(RandomTemperature, NB)+" (%d temperatures)".formatted(NB));
logger.info("Random 0-100: {}", Statistique.average(()->random.nextInt(100)+1, NB)+" (%d numbers)".formatted(NB));
D6 Roll: 2
Random Temperature: -8.676348852325994
18:44:12.812 [IJava-executor-5] INFO notebook -- Random dices: 4.8 (10 rolls)
18:44:12.823 [IJava-executor-5] INFO notebook -- Random temperatures: 11.28101560265375 (10 temperatures)
18:44:12.837 [IJava-executor-5] INFO notebook -- Random 0-100: 62.3 (10 numbers)
<T>
(accept)void accept(T t)
// Create list of names
List<String> names = List.of("Alice", "Bob", "Charlie");
// Define two consumers
Consumer<String> upperCase = name -> System.out.print(name.toUpperCase());
Consumer<String> greet = name -> System.out.println(" -> Hello " + name);
// Chain consumers with andThen
Consumer<String> process = upperCase.andThen(greet);
// Process each name
// The forEach method provides a functional approach to iteration, making code more concise and focusing on what to do with elements rather than how to iterate.
names.forEach(process);
ALICE -> Hello Alice
BOB -> Hello Bob
CHARLIE -> Hello Charlie
<T>
(test)test(T t)
T
L’interface Predicate
a un paramètre, le type d’objet sur lequel s’applique le prédicat et renvoie un booléen.
// Méthodes par défaut
and(Predicate<T>) // Et logique
or(Predicate<T>) // Ou logique
negate() // Négation
Utilisation typique :
// Name validation predicates
Predicate<String> startsWithCapital = name -> name.matches("[A-Z][a-z]+");
Predicate<String> hasValidLength = name -> name.length() >= 2 && name.length() <= 30;
Predicate<String> noSpecialChars = name -> name.matches("[A-Za-z]+");
// Combine predicates
Predicate<String> isValidName = startsWithCapital
.and(hasValidLength)
.and(noSpecialChars);
// Test
System.out.println("Is 'John' valid? " + isValidName.test("John"));
System.out.println("Is 'j0hn' valid? " + isValidName.test("j0hn"));
Is 'John' valid? true
Is 'j0hn' valid? false
Ces functions sont très utilises pour construire efficacement des traitements sur des séquences de données. Avant le cours, sur les flux de données (Stream
). Voilà une illustration pour construire un traitement générique d’une liste d’élément: filtrage en fonction d’un condition passée en paramètre, transformation des éléments de la liste et application d’un traitement a!ux résultats.
et utiliser une lambda expression pour le prédicat et une référence vers une fonction pour afficher le résultat
List<Person> people = List.of(
new Person("Alice", 25),
new Person("Bob", 17),
new Person("Charlie", 30),
new Person("Alfred", 16)
);
// Multiple predicates
Predicate<Person> isAdult = p -> p.age() >= 18;
Predicate<Person> nameStartsWithA = p -> p.nom().startsWith("A");
// Multiple functions
Function<Person, String> formatName = p -> p.nom().toUpperCase();
Function<String, String> addTitle = p -> "Mr/Ms. " + p;
// Multiple consumers
Consumer<String> print = System.out::println;
Consumer<String> log = s -> logger.info(s);
// Process the data
Processor<Person, String> processor = new Processor<>();
processor.run(people,
isAdult
.and(nameStartsWithA),
formatName
.andThen(addTitle),
print
.andThen(log)
);
Mr/Ms. ALICE
18:46:39.465 [IJava-executor-6] INFO notebook -- Mr/Ms. ALICE
La même classe générique Processor
peut être utilisée pour filtrer des entiers.
// Sample data
List<Integer> numbers = List.of(1, 15, 30, 45, 60, 75, 90);
// Multiple predicates
Predicate<Integer> isGreaterThan30 = n -> n > 30;
Predicate<Integer> isDivisibleBy15 = n -> n % 15 == 0;
// Multiple functions
Function<Integer, String> formatNumber = n -> String.format("Number: %d", n);
Function<String, String> addComment = s -> s + " (multiple of 15)";
// Multiple consumers
Consumer<String> print = System.out::println;
Consumer<String> log = s -> logger.info(s);
// Process the data
Processor<Integer, String> processor = new Processor<>();
processor.run(numbers,
isGreaterThan30
.and(isDivisibleBy15),
formatNumber
.andThen(addComment),
print
.andThen(log)
);
Number: 45 (multiple of 15)
18:46:52.825 [IJava-executor-6] INFO notebook -- Number: 45 (multiple of 15)
Number: 60 (multiple of 15)
18:46:52.825 [IJava-executor-6] INFO notebook -- Number: 60 (multiple of 15)
Number: 75 (multiple of 15)
18:46:52.825 [IJava-executor-6] INFO notebook -- Number: 75 (multiple of 15)
Number: 90 (multiple of 15)
18:46:52.825 [IJava-executor-6] INFO notebook -- Number: 90 (multiple of 15)
E. Bruno