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 soit final, 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 final explicitement, 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.ASCENDING est 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 { ... } avec return explicite
  • 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 :

  1. Boucle for-each classique :
for(String s : l) {
    System.out.println(s);
}
  1. Lambda simple :
l.forEach(s -> System.out.println(s));
  1. Classe anonyme :
l.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});
  1. 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 :

FormeSyntaxeÉquivalent lambda
Méthode statiqueInteger::compare(i1, i2) -> Integer.compare(i1, i2)
ConstructeurArrayList::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"::charAti -> "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 : le forEach des Map prend en argument un BiConsumer<K, V>, interface fonctionnelle à deux arguments (clef, valeur) -> ..., à la différence du Consumer<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égorieRôleExemples
SourcesProduisent un flotstream(), Stream.of(...), IntStream.range(f, t), Stream.iterate(i, f)
IntermédiairesTransforment le flot, retournent un nouveau flotfilter, map, mapToInt, sorted, limit, skip
TerminalesConsomment le flot, retournent autre choseforEach, 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 — entiers int
  • LongStream — entiers long
  • DoubleStream — flottants double
// 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é à reduce doit ê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 p

Mé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}"