Notes S5
1. Les Lambdas
Tri avec un Enum (Ascending/Descending)
Il existe plusieurs façons de trier une liste avec un enum Order : ASCENDING, DESCENDING.
Version initiale
void sort(List<Integer> maListe, Order order) {
Collections.sort(maListe, (i1, i2) -> order == Order.ASCENDING
? Integer.compare(i1, i2)
: Integer.compare(i2, i1));
}Note importante : Si
order = null, cela provoque une erreur dans le lambda car la variable doit être soitfinal, soit effectively final (pas de variable qui peut changer dans un lambda).
Effectively final : une variable locale n’a pas besoin d’être déclarée
finalexplicitement, mais elle ne doit jamais être réassignée après sa création. Si elle l’est, Java refuse de la capturer dans une lambda. C’est ce que la Java Language Specification appelle effectively final (§4.12.4).
Optimisation
Il est préférable de stocker le comparateur dans une variable avant de l’utiliser.
void sort(List<Integer> list, Order order) {
// Note : on ne peut pas utiliser Object c ici, il faut utiliser Comparator<Integer>
Comparator<Integer> c = order == Order.ASCENDING
? (i1, i2) -> Integer.compare(i1, i2)
: (i1, i2) -> Integer.compare(i2, i1);
Collections.sort(list, c);
}Pourquoi stocker le comparateur ? Le test
order == Order.ASCENDINGest ainsi effectué une seule fois avant le tri, et non à chaque comparaison de deux éléments lors du tri. Cela améliore l’efficacité.
Interface fonctionnelle
Une interface fonctionnelle est une interface qui possède exactement une méthode abstraite. Elle peut en revanche posséder n’importe quel nombre de méthodes static ou default. L’annotation optionnelle @FunctionalInterface garantit à la compilation qu’une interface est bien fonctionnelle.
@FunctionalInterface
public interface RealFunction {
public double valueAt(double x);
}Une lambda ne peut apparaître que dans un contexte attendant une interface fonctionnelle — Java doit pouvoir déterminer sans ambiguïté quelle méthode abstraite la lambda implémente.
Syntaxe des Lambdas
Arguments abrégés
- Le type des arguments peut être omis (inféré par Java)
- Si la lambda ne prend qu’un seul argument, les parenthèses peuvent être omises :
s -> System.out.println(s)
Corps abrégé
- Si le corps est un bloc multi-lignes, on utilise
{ ... }avecreturnexplicite - Si le corps est une expression unique, on peut supprimer les accolades et le
return
// Corps bloc (multi-lignes)
Comparator<String> c = (s1, s2) -> {
int lc = Integer.compare(s1.length(), s2.length());
return lc != 0 ? lc : s1.compareTo(s2);
};
// Corps expression (une ligne)
Comparator<String> c = (s1, s2) -> Integer.compare(s1.length(), s2.length());L’Interface fonctionnelle Consumer
L’argument des lambdas est souvent ce que Java appelle un consommateur (Consumer), décrit par l’interface fonctionnelle Consumer du paquetage java.util.function.
public interface Consumer<T> {
void accept(T t);
}
public interface Iterable<T> {
default void forEach(Consumer<T> action) { /* ... */ }
}Exemple d’utilisation
Soit une liste l contenant "un", "deux", "trois", "quatre".
Différentes façons de l’afficher :
- Boucle for-each classique :
for(String s : l) {
System.out.println(s);
}- Lambda simple :
l.forEach(s -> System.out.println(s));- Classe anonyme :
l.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});- Référence de méthode :
l.forEach(System.out::println);Implémentation personnalisée de forEach
Il n’y a rien de “magique” avec les lambdas, c’est juste un raccourci de code.
private static void myForEach(List<String> l, Consumer<String> c) {
for (String s : l) {
c.accept(s);
}
}
// Appel avec une classe externe :
myForEach(l, new PrintConsumer());Références de méthodes
Java offre une syntaxe :: pour écrire des lambdas qui ne font que déléguer à une méthode existante. Il en existe 4 formes :
| Forme | Syntaxe | Équivalent lambda |
|---|---|---|
| Méthode statique | Integer::compare | (i1, i2) -> Integer.compare(i1, i2) |
| Constructeur | ArrayList::new | () -> new ArrayList<>() |
| Méthode non statique (récepteur inféré) | String::compareTo | (s1, s2) -> s1.compareTo(s2) |
| Méthode non statique (récepteur fixé) | "abc"::charAt | i -> "abc".charAt(i) |
Attention (forme 3) : avec une référence à une méthode non statique sans récepteur fixé, la lambda a un argument de plus que la méthode, le premier argument devenant le récepteur.
Comparator.comparing
Pour trier par un critère spécifique, la méthode statique Comparator.comparing est plus concise qu’une lambda manuelle :
// Trier des chaînes par longueur
List<String> l = Arrays.asList("bas", "bras", "as", "a", "sabre");
l.sort(Comparator.comparing(s -> s.length()));
// Ou encore plus court avec une référence de méthode :
l.sort(Comparator.comparing(String::length));2. Utilisation des Maps
Exemple : Compter le nombre de mots par longueur
Map<Integer, Integer> counts = new HashMap<>(); // <Longueur, Nombre de mots>
for (String word : frWords) {
int length = word.length();
// La méthode getOrDefault simplifie grandement le code.
counts.put(length, counts.getOrDefault(length, 0) + 1);
}
// Pour afficher les résultats :
// On itère sur le entrySet pour avoir accès à la clef et la valeur en même temps.
for (Map.Entry<Integer, Integer> entry : counts.entrySet()) {
System.out.printf("Longueur %2d : %5d mots\n", entry.getKey(), entry.getValue());
}Raccourcir avec forEach
Map<Integer, Integer> h = new HashMap<>();
// ... remplissage ...
h.forEach((length, count) -> {
System.out.printf("Longueur %2d : %5d mots\n", length, count);
});Utilisation de merge
La méthode merge permet de simplifier encore plus l’incrémentation.
Map<Integer, Integer> h = new HashMap<>();
for (String word : frWords) {
int length = word.length();
// 1 est la valeur pour la première fois qu'on rencontre cette longueur
h.merge(length, 1, (oldValue, newValue) -> oldValue + newValue);
// Ou avec Integer::sum
h.merge(length, 1, Integer::sum);
}Version ultra-compacte
Map<Integer, Integer> h = new HashMap<>();
frWords.forEach(w -> h.merge(w.length(), 1, Integer::sum));
h.forEach((length, count) -> {
System.out.printf("Longueur %2d : %5d mots\n", length, count);
});
BiConsumer: leforEachdesMapprend en argument unBiConsumer<K, V>, interface fonctionnelle à deux arguments(clef, valeur) -> ..., à la différence duConsumer<T>à un seul argument des listes.
3. Programmation par Flots (Streams)
Approche classique (Impérative)
Conversion de températures de Fahrenheit en Celsius.
List<String> tempF = List.of("0", "", "100");
List<String> tempC = new ArrayList<>();
for (String fS : tempF) {
if (fS.isEmpty()) continue;
double f = Double.parseDouble(fS);
double c = (f - 32.0) * (5.0 / 9.0);
String cS = String.valueOf(c);
tempC.add(cS);
}
System.out.println(tempC);Approche avec les Streams
Un Stream ne se réutilise pas :
List<String> tempsF = List.of("0", "", "100");
tempsF.stream().forEach(System.out::println);
Stream<String> stream = tempsF.stream(); Pipeline de transformation
List<String> tempC = tempsF.stream()
.filter(s -> !s.isEmpty()) // Filtre la chaîne vide
.map(Double::parseDouble) // fS -> Double.parseDouble(fS)
.map(f -> (f - 32.0) * (5.0 / 9.0)) // Calcul Celsius
.map(String::valueOf) // Conversion en String
.toList(); // En faire une liste
System.out.println(tempC); // Il ne se passe rien si on ne consomme pas le flot (opération terminale)Trois catégories de méthodes
Toutes les méthodes travaillant sur les flots appartiennent à exactement une des trois catégories suivantes :
| Catégorie | Rôle | Exemples |
|---|---|---|
| Sources | Produisent un flot | stream(), Stream.of(...), IntStream.range(f, t), Stream.iterate(i, f) |
| Intermédiaires | Transforment le flot, retournent un nouveau flot | filter, map, mapToInt, sorted, limit, skip |
| Terminales | Consomment le flot, retournent autre chose | forEach, collect, reduce, count, min, max, allMatch, anyMatch, noneMatch |
Flots spécialisés
Pour éviter le coût des objets Integer, Double, etc., Java propose des flots spécialisés pour les types primitifs :
IntStream— entiersintLongStream— entierslongDoubleStream— flottantsdouble
// Flot d'entiers non spécialisé (crée des objets Integer)
Stream<Integer> s1 = Stream.iterate(1, i -> i + 1);
// Flot spécialisé (plus efficace)
IntStream s2 = IntStream.iterate(1, i -> i + 1);
// Plage d'entiers
IntStream range = IntStream.range(0, 10); // [0, 9]
IntStream rangeClosed = IntStream.rangeClosed(1, 10); // [1, 10]Méthodes terminales importantes
Réduction (reduce)
// Factorielle avec reduce
int fact(int x) {
return IntStream.rangeClosed(2, x)
.reduce(1, (a, b) -> a * b);
}L’opérateur passé à
reducedoit être associatif car l’ordre d’application n’est pas garanti.
Méthodes logiques
stream.allMatch(p) // vrai si TOUS les éléments satisfont p
stream.anyMatch(p) // vrai si AU MOINS UN élément satisfont p
stream.noneMatch(p) // vrai si AUCUN élément ne satisfait pMéthodes statistiques
stream.count() // nombre d'éléments
stream.max(Comparator.naturalOrder()) // plus grand élément (Optional)
stream.min(Comparator.naturalOrder()) // plus petit élément (Optional)
// Sur IntStream/DoubleStream uniquement :
intStream.average() // moyenne (OptionalDouble)Collecteurs (Collectors)
La méthode terminale collect s’utilise avec des collecteurs prédéfinis de la classe Collectors :
// Collecter en liste
.collect(Collectors.toList()) // ou simplement .toList()
// Collecter en ensemble
.collect(Collectors.toSet())
// Collecter en Map
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()))
// Joindre des chaînes
Stream.of("un", "deux", "trois")
.collect(Collectors.joining(", ", "{", "}"));
// → "{un, deux, trois}"