MFormations
Modern Java Engineering

Chapitre 3

Chapitre 03 : Java 8-17-21

Chapitre 03 : Java 8-17-21

Cours : Java 8-17-21

1. Lambdas et Interfaces Fonctionnelles

1.1 Syntaxe des Lambdas

Une lambda est une fonction anonyme qui peut être traitée comme une valeur.

// Syntaxe complète
(paramètres) -> { corps }

// Exemples
(int a, int b) -> { return a + b; }
(String s) -> { System.out.println(s); }
() -> { return 42; }

// Syntaxe simplifiée
(a, b) -> a + b                   // Types inférés, return implicite
s -> System.out.println(s)        // Un seul paramètre : parenthèses optionnelles
() -> 42                           // Aucun paramètre
x -> x * 2                         // Expression simple

1.2 Interfaces Fonctionnelles

Une interface fonctionnelle a exactement une méthode abstraite. L'annotation @FunctionalInterface est optionnelle mais recommandée.

@FunctionalInterface
interface Calcul {
    int apply(int a, int b);
}

// Utilisation
Calcul addition = (a, b) -> a + b;
Calcul multiplication = (a, b) -> a * b;

1.3 Les 4 Interfaces Fonctionnelles Principales

// 1. Predicate<T> → boolean test(T t)
Predicate<String> nonVide = s -> !s.isEmpty();
Predicate<Integer> pair = n -> n % 2 == 0;

// 2. Function<T, R> → R apply(T t)
Function<String, Integer> longueur = String::length;
Function<Integer, String> toString = Object::toString;

// 3. Consumer<T> → void accept(T t)
Consumer<String> afficher = System.out::println;
Consumer<List<String>> vider = List::clear;

// 4. Supplier<T> → T get()
Supplier<Double> random = Math::random;
Supplier<LocalDate> aujourdHui = LocalDate::now;

1.4 Interfaces Spécialisées

// Primitives (évitent l'autoboxing)
IntFunction<String> intToString = String::valueOf;
ToIntFunction<String> toLength = String::length;
IntPredicate estPair = n -> n % 2 == 0;
IntConsumer afficherInt = System.out::println;
IntSupplier supplierInt = () -> 42;

// Binaires
BinaryOperator<Integer> sum = Integer::sum;
BiFunction<String, String, String> concat = String::concat;
BiPredicate<String, String> egales = String::equals;
BiConsumer<String, Integer> printNTimes = (s, n) -> {
    for (int i = 0; i < n; i++) System.out.println(s);
};

1.5 Method References

// Static method : Class::staticMethod
Function<String, Integer> parser = Integer::parseInt;  // s -> Integer.parseInt(s)

// Instance method of object : object::instanceMethod
var list = List.of("a", "b", "c");
Consumer<String> printer = System.out::println;

// Instance method of class : Class::instanceMethod
Function<String, Integer> length = String::length;

// Constructor : Class::new
Supplier<List<String>> listCreator = ArrayList::new;
Function<String, StringBuilder> sbCreator = StringBuilder::new;

2. Streams API

2.1 Pipeline Pattern

Un Stream a 3 phases :

  1. Source : collection, tableau, générateur
  2. Opérations intermédiaires : filter, map, sorted (lazy)
  3. Opération terminale : collect, forEach, count (déclenche le traitement)
List<String> result = list.stream()       // 1. Source
    .filter(s -> !s.isEmpty())            // 2. Intermédiaire (lazy)
    .map(String::toUpperCase)             // 2. Intermédiaire (lazy)
    .sorted()                             // 2. Intermédiaire (lazy)
    .collect(Collectors.toList());        // 3. Terminale (déclenche)

2.2 Création de Streams

// Depuis une collection
list.stream();
list.parallelStream();

// Depuis un tableau
Arrays.stream(array);
Stream.of("a", "b", "c");

// Depuis des valeurs
Stream.of(1, 2, 3, 4, 5);
Stream.concat(stream1, stream2);

// Streams infinis
Stream.generate(Math::random)              // infini
      .limit(10);                          // limité

Stream.iterate(0, n -> n + 2)              // infini
      .limit(10);

// Java 9+ : iterate avec condition
Stream.iterate(0, n -> n < 100, n -> n + 2);

// Java 9+ : ofNullable
Stream.ofNullable(getValue());              // Stream vide ou 1 élément

// Primitives
IntStream.range(0, 10);                     // 0..9
IntStream.rangeClosed(1, 10);               // 1..10
IntStream.iterate(0, i -> i + 1).limit(100);
LongStream.range(0L, 1_000_000L);
DoubleStream.generate(Math::random).limit(10);

2.3 Opérations Intermédiaires

// filter : garde les éléments correspondant au prédicat
stream.filter(s -> s.length() > 5)

// map : transforme chaque élément
stream.map(String::toUpperCase)
stream.mapToInt(String::length)
stream.mapToLong(...)
stream.mapToDouble(...)

// flatMap : aplatit les Streams imbriqués
List<List<String>> listOfLists = ...;
listOfLists.stream()
    .flatMap(Collection::stream)          // Stream<String>
    .toList();

// flatMap pour Optionals (Java 16+)
stream.flatMap(Optional::stream)

// distinct : éléments uniques (basé sur equals)
stream.distinct()

// sorted : tri (naturel ou Comparator)
stream.sorted()
stream.sorted(Comparator.reverseOrder())
stream.sorted(Comparator.comparing(Person::age))

// peek : debug (ne pas utiliser pour modifier)
stream.peek(System.out::println)

// limit / skip
stream.limit(10)                          // 10 premiers
stream.skip(5)                            // ignore 5 premiers

// takeWhile / dropWhile (Java 9+)
stream.takeWhile(n -> n < 10)             // prend tant que condition vraie
stream.dropWhile(n -> n < 10)             // ignore tant que condition vraie

// mapMulti (Java 16+) : alternative à flatMap
stream.mapMulti((String s, Consumer<String> downstream) -> {
    downstream.accept(s.toLowerCase());
    downstream.accept(s.toUpperCase());
});

2.4 Opérations Terminales

// forEach / forEachOrdered
stream.forEach(System.out::println);          // ordre non garanti
stream.forEachOrdered(System.out::println);   // ordre garanti

// collect
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
Map<Integer, String> map = stream.collect(Collectors.toMap(String::length, Function.identity()));

// toList (Java 16+) : immuable
List<String> list = stream.toList();

// toArray
String[] array = stream.toArray(String[]::new);

// reduce
Optional<Integer> sum = stream.reduce(Integer::sum);
int sum = stream.reduce(0, Integer::sum);                  // avec valeur initiale

// count
long count = stream.count();

// anyMatch / allMatch / noneMatch
boolean hasLong = stream.anyMatch(s -> s.length() > 10);
boolean allLong = stream.allMatch(s -> s.length() > 10);
boolean noneLong = stream.noneMatch(s -> s.length() > 10);

// findFirst / findAny
Optional<String> first = stream.findFirst();               // premier élément
Optional<String> any = stream.findAny();                   // n'importe lequel (parallel)

// min / max
Optional<String> min = stream.min(Comparator.naturalOrder());
Optional<String> max = stream.max(String::compareTo);

2.5 flatMap Approfondi

// flatMap avec Optional (Java 9+)
public List<String> getEmails(List<User> users) {
    return users.stream()
        .map(User::getEmail)               // Stream<Optional<String>>
        .flatMap(Optional::stream)         // Java 9+ : Stream<String>
        .toList();
}

// flatMap avec objets imbriqués
List<String> allTags = posts.stream()       // Stream<Post>
    .map(Post::getTags)                     // Stream<List<String>>
    .flatMap(List::stream)                  // Stream<String>
    .distinct()
    .toList();

// flatMap avec validation
List<String> validItems = items.stream()
    .flatMap(item -> {
        if (item.isValid()) return Stream.of(item);
        else return Stream.empty();
    })
    .map(Item::getName)
    .toList();

3. Collectors et Opérations Terminales

3.1 Collectors Essentiels

// Collections
.toList()                               // Java 16+ : liste immuable
Collectors.toList()                     // liste mutable
Collectors.toSet()                      // set
Collectors.toCollection(LinkedList::new) // collection spécifique

// Map
Collectors.toMap(Function.identity(), String::length)
Collectors.toMap(k -> k, v -> v, (v1, v2) -> v1)  // gestion doublons
Collectors.toMap(k -> k, v -> v, (v1, v2) -> v1, TreeMap::new)

// Regroupement
Collectors.groupingBy(String::length)               // Map<Integer, List<String>>
Collectors.groupingBy(String::length, Collectors.toSet())
Collectors.groupingBy(String::length, TreeMap::new, Collectors.toList())

// Partitionnement (2 groupes)
Collectors.partitioningBy(s -> s.length() > 5)      // Map<Boolean, List<String>>
Collectors.partitioningBy(s -> s.length() > 5, Collectors.counting())

// Jointure
Collectors.joining()                                 // "abc"
Collectors.joining(", ")                             // "a, b, c"
Collectors.joining(", ", "[", "]")                   // "[a, b, c]"

// Statistiques
Collectors.summarizingInt(String::length)             // IntSummaryStatistics
Collectors.summingInt(String::length)                 // int
Collectors.averagingInt(String::length)               // double
Collectors.counting()                                 // long

// Mapping et Reducing
Collectors.mapping(String::toUpperCase, Collectors.toList())
Collectors.filtering(s -> s.length() > 5, Collectors.toList())  // Java 9+
Collectors.flatMapping(s -> s.chars().boxed(), Collectors.toList()) // Java 9+
Collectors.reducing(0, String::length, Integer::sum)

3.2 Collectors Personnalisés

// Collecteur personnalisé simple
List<String> result = stream.collect(
    ArrayList::new,           // supplier
    ArrayList::add,           // accumulator
    ArrayList::addAll         // combiner (parallel)
);

// Collecteur avec statistiques
public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitionCollector(
        Predicate<? super T> predicate) {
    return Collectors.partitioningBy(predicate);
}

4. Parallel Streams et Performance

4.1 Quand utiliser parallelStream ?

// ✅ Bon candidat : calculs intensifs, grandes collections, indépendance
long sum = LongStream.range(0, 10_000_000)
    .parallel()
    .sum();

// ❌ Mauvais candidat : petites collections, blocage I/O, état partagé
list.stream().parallel().collect(toList());  // Séquentiel est plus rapide

// ⚠️ Opérations avec état partagé (non thread-safe)
List<Integer> result = new ArrayList<>();  // ❌ Pas thread-safe!
IntStream.range(0, 100).parallel()
    .forEach(result::add);  // Résultat indéterministe

4.2 Facteurs de Performance

// Taille de la collection
// Le parallélisme a un overhead. Pas rentable pour < 10 000 éléments.

// Type d'opération
// CPU-bound (calcul) : bon candidat pour parallélisme
// I/O-bound (réseau/fichier) : mieux avec CompletableFuture

// ForkJoinPool commun
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4");
// Par défaut : nombre de processeurs - 1

4.3 Opérations de l'ordre

// Opérations qui brisent l'ordre
List<Integer> unordered = stream.parallel()
    .unordered()  // Indique que l'ordre n'importe pas → meilleures performances
    .distinct()
    .toList();

4.4 Bonnes Pratiques Performance

// ✅ Utiliser les primitives (évite l'autoboxing)
IntStream.range(0, 1000).sum();

// ❌ Autoboxing coûteux
Stream.iterate(0, i -> i + 1).limit(1000).mapToInt(i -> i).sum();

// ✅ flatMap coûteux → mapMulti (Java 16+)
stream.<String>mapMulti((s, consumer) -> {
    for (String word : s.split(" ")) consumer.accept(word);
});

// ✅ Utiliser findFirst → findAny en parallèle
stream.parallel().findAny();  // plus rapide que findFirst()

// ⚠️ Les opérations avec état (sorted, distinct, limit) sont plus coûteuses en parallèle

5. Optional Patterns Avancés

5.1 Composition d'Optionals

// or (Java 9+) : Optional alternatif
Optional<String> result = findPrimaryEmail(user)
    .or(() -> findSecondaryEmail(user))
    .or(() -> Optional.of("default@email.com"));

// Chaînage avec flatMap
Optional<String> city = findUser(userId)
    .flatMap(User::getAddress)
    .map(Address::getCity);

// ifPresentOrElse (Java 9+)
opt.ifPresentOrElse(
    value -> System.out.println("Présent: " + value),
    () -> System.out.println("Absent")
);

5.2 Optional dans les Streams

// Java 9+ : Optional::stream dans flatMap
List<String> emails = users.stream()
    .map(User::getEmail)            // Stream<Optional<String>>
    .flatMap(Optional::stream)      // Stream<String>
    .toList();

// Java 16+ : mapMulti équivalent
List<String> emails = users.stream()
    .<String>mapMulti((user, consumer) -> 
        user.getEmail().ifPresent(consumer))
    .toList();

5.3 Combinators avec Optional

// Combiner deux Optionals
public static <T, U, R> Optional<R> combine(
        Optional<T> opt1, 
        Optional<U> opt2,
        BiFunction<T, U, R> combiner) {
    return opt1.flatMap(t -> opt2.map(u -> combiner.apply(t, u)));
}

// Usage
var result = combine(
    Optional.of("Hello"),
    Optional.of(5),
    (s, n) -> s + " " + n
);  // Optional["Hello 5"]

// Optional avec API stream
public static <T> Optional<T> findFirst(List<Optional<T>> optionals) {
    return optionals.stream()
        .flatMap(Optional::stream)
        .findFirst();
}

6. Switch Moderne (Arrow, Pattern Matching)

6.1 Arrow Switch (Java 14+)

// Expression switch avec ->
String result = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> "6h de sommeil";
    case TUESDAY -> "7h de sommeil";
    case THURSDAY, SATURDAY -> "8h de sommeil";
    case WEDNESDAY -> "9h de sommeil";
    default -> "Pas un jour valide";
};

// Avec yield pour les blocs
String result = switch (day) {
    case MONDAY -> {
        System.out.println("Lundi!");
        yield "Début de semaine";
    }
    case FRIDAY -> {
        System.out.println("Vendredi!");
        yield "Fin de semaine";
    }
    default -> "Autre jour";
};

6.2 Pattern Matching pour Switch (Java 21+)

// Avant Java 21 : if/else chaîne
String formatValue(Object obj) {
    if (obj instanceof Integer i) return "int: " + i;
    else if (obj instanceof String s) return "String: " + s;
    else if (obj instanceof Long l) return "long: " + l;
    else return "inconnu";
}

// Java 21+ : pattern matching for switch
String formatValue(Object obj) {
    return switch (obj) {
        case Integer i -> "int: " + i;
        case String s -> "String: " + s;
        case Long l -> "long: " + l;
        case null -> "Null!";
        default -> "inconnu";
    };
}

// Guards (clause when)
String describe(Object obj) {
    return switch (obj) {
        case Integer i when i < 0 -> "entier négatif";
        case Integer i when i == 0 -> "zéro";
        case Integer i -> "entier positif: " + i;
        case String s when s.length() > 10 -> "longue chaîne";
        case String s -> "courte chaîne: " + s;
        case null, default -> "autre";
    };
}

6.3 Record Patterns (Java 21+)

record Point(int x, int y) {}
record Line(Point start, Point end) {}

// Déconstruction de records dans switch
void print(Object obj) {
    switch (obj) {
        case Point(int x, int y) -> System.out.println("Point: " + x + "," + y);
        case Line(Point s, Point e) -> System.out.println("Line: " + s + " → " + e);
        default -> System.out.println("autre");
    }
}

// Avec guards
void analyze(Point p) {
    switch (p) {
        case Point(int x, int y) when x == y -> System.out.println("Sur la diagonale");
        case Point(int x, int y) when x > 0 && y > 0 -> System.out.println("Quadrant 1");
        case Point(var x, var y) -> System.out.println("Autre point");
    }
}

7. Text Blocks (Java 15+)

7.1 Syntaxe

// Multi-lignes automatique, échappement minimal
String json = """
    {
        "name": "Java",
        "version": 21,
        "features": [
            "Records",
            "Sealed Classes",
            "Pattern Matching"
        ]
    }
    """;

String html = """
    <html>
        <body>
            <h1>%s</h1>
            <p>%s</p>
        </body>
    </html>
    """.formatted("Titre", "Contenu");

// SQL
String query = """
    SELECT u.name, u.email, o.total
    FROM users u
    JOIN orders o ON u.id = o.user_id
    WHERE o.status = 'ACTIVE'
    ORDER BY o.total DESC
    LIMIT 10
    """;

7.2 Formatage et Échappement

// formattage avec String.formatted()
String message = """
    Bonjour %s !
    Votre commande #%d est confirmée.
    Montant total : %.2f €
    """.formatted("Alice", 12345, 99.99);

// Échappement : \s = espace insécable
// \ = fin de ligne (permet de continuer)
String longText = """
    Ceci est un texte très long que je \
    souhaite écrire sur plusieurs lignes \
    dans le code source mais sur une seule \
    ligne dans l'exécution.
    """;

// Indentation automatique
String code = """
        public void hello() {
            System.out.println("Hello!");
        }
        """;
// L'indentation est déterminée par la position du """ fermant

8. Records et Sealed Classes

8.1 Records : Rappels et Usage Avancé

// Record avec constructeur compact et validation
public record Email(String value) {
    // Validé avant affectation
    public Email {
        if (value == null || !value.contains("@")) {
            throw new IllegalArgumentException("Email invalide");
        }
    }
    
    // Méthode statique
    public static Email of(String value) {
        return new Email(value);
    }
}

// Record avec méthode
public record Range(int min, int max) {
    public Range {
        if (min > max) throw new IllegalArgumentException("min > max");
    }
    
    public boolean contains(int value) {
        return value >= min && value <= max;
    }
}

// Record local (Java 16+)
public List<String> process(List<Person> people) {
    record NameCount(String name, long count) {}
    
    return people.stream()
        .collect(Collectors.groupingBy(Person::name, Collectors.counting()))
        .entrySet().stream()
        .map(e -> new NameCount(e.getKey(), e.getValue()))
        .sorted(Comparator.comparingLong(NameCount::count).reversed())
        .map(NameCount::name)
        .toList();
}

8.2 Sealed Classes

// Hiérarchie d'expressions mathématiques
public sealed interface Expr 
    permits Const, Add, Mul, Neg, Div {}

public record Const(double value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}
public record Div(Expr left, Expr right) implements Expr {
    public Div {
        if (right instanceof Const(double v) && v == 0) {
            throw new ArithmeticException("Division by zero");
        }
    }
}
public record Neg(Expr expr) implements Expr {}

// Évaluation avec pattern matching exhaustif
public static double eval(Expr e) {
    return switch (e) {
        case Const(double v) -> v;
        case Add(Expr l, Expr r) -> eval(l) + eval(r);
        case Mul(Expr l, Expr r) -> eval(l) * eval(r);
        case Div(Expr l, Expr r) -> eval(l) / eval(r);
        case Neg(Expr x) -> -eval(x);
        // Pas de default : exhaustif !
    };
}

9. Pattern Matching

9.1 instanceof (Java 16+)

// Avant
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// Après (Java 16+)
if (obj instanceof String s) {
    System.out.println(s.length());
}

// Avec condition composée
if (obj instanceof String s && s.length() > 5) {
    System.out.println(s.toUpperCase());
}

9.2 Record Patterns (Java 21+)

record Point(int x, int y) {}
record Rectangle(Point topLeft, Point bottomRight) {}

// Déconstruction
void printPoint(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x + ", " + y);
    }
}

// Déconstruction imbriquée
void printArea(Object obj) {
    if (obj instanceof Rectangle(Point(int x1, int y1), Point(int x2, int y2))) {
        int area = (x2 - x1) * (y2 - y1);
        System.out.println("Area: " + area);
    }
}

// Avec var (type inféré)
void printCoords(Object obj) {
    if (obj instanceof Point(var x, var y)) {
        System.out.println(x + ", " + y);
    }
}

10. Virtual Threads (Project Loom)

10.1 Principe

Les Virtual Threads (Java 21+) sont des threads légers gérés par la JVM, pas par l'OS.

// Creer un virtual thread
Thread vThread = Thread.startVirtualThread(() -> {
    System.out.println("Virtual thread: " + Thread.currentThread());
});

// Avec ExecutorService (try-with-resources, Java 19+)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
        int taskId = i;
        futures.add(executor.submit(() -> handleTask(taskId)));
    }
}

// 1 million de threads légers
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 1_000_000)
        .forEach(i -> executor.submit(() -> {
            Thread.sleep(1000);
            return i;
        }));
} // Tout se termine en ~1 seconde

10.2 Virtual Threads vs Platform Threads

// Platform thread : coûteux (~1 Mo de stack)
Thread.ofPlatform().start(() -> { ... });

// Virtual thread : léger (~quelques Ko)
Thread.ofVirtual().start(() -> { ... });

// Différence de performance
long start = System.nanoTime();

// Platform threads : maximum ~4000 avant OOM
try (var executor = Executors.newFixedThreadPool(1000)) {
    // ...
}

// Virtual threads : 1 000 000+ sans problème
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // ...
}

10.3 Bonnes Pratiques

// ✅ Adapter le code existant : remplacer les pools par virtual threads
// ❌ Éviter : synchronized blocks (pinnage)
synchronized (lock) {  // Le thread virtuel pince le thread porteur
    Thread.sleep(100); // Bloque le thread porteur !
}

// ✅ Utiliser ReentrantLock à la place
private final Lock lock = new ReentrantLock();
lock.lock();
try {
    Thread.sleep(100); // Ne bloque pas le thread porteur
} finally {
    lock.unlock();
}

// ✅ Pool de threads pour les tâches CPU-bound
try (var executor = Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors())) {
    // Tâches intensives en calcul
}

// ✅ Virtual threads pour les tâches I/O-bound
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // Tâches réseau, fichier, base de données
}

// ⚠️ ThreadLocal : à éviter (trop de threads)
// ⚠️ ScopedValue (Java 21+) : alternative à privilégier

10.4 Structured Concurrency (Preview Java 21+)

// Exécution structurée : portée définie
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> user = scope.fork(() -> fetchUser());
    Future<Integer> orders = scope.fork(() -> fetchOrders());
    
    scope.join();               // Attend toutes les tâches
    scope.throwIfFailed();      // Lance si une tâche échoue
    
    return new UserData(user.resultNow(), orders.resultNow());
}

11. Résumé

FeatureJavaStatut
Lambdas8Définitif
Streams8Définitif
Optional8Définitif
Modules (JPMS)9Définitif
try-with-resources amélioré9Définitif
Var10Définitif
Text Blocks15Définitif
Records16Définitif
Pattern Matching instanceof16Définitif
Sealed Classes17Définitif
Pattern Matching switch21Définitif
Record Patterns21Définitif
Virtual Threads21Définitif
Structured Concurrency21Preview
Scoped Values21Preview