Skip to content

Java Aide-mémoire

Langage orienté objet de niveau entreprise, écrire une fois, exécuter partout.

01

Premiers pas

Hello World

Chaque programme Java commence par main(). Le nom du fichier doit correspondre au nom de la classe publique (Main.java → Main.class). javac compile en bytecode (.class), java l'exécute sur la JVM. System.out.println imprime sur stdout ; printf supporte les spécificateurs de format (%s, %d, %f, %n pour une nouvelle ligne).

java
// Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
        System.out.printf("Name: %s, Age: %d%n", "Alice", 30);
    }
}

// Compile: javac Main.java  -> Main.class
// Run:     java Main
// Package: java -cp . com.example.Main

// Every Java program needs:
// 1. A class (public class, file name must match)
// 2. A main method: public static void main(String[] args)

Variables & types primitifs

Java a 8 types primitifs (int, long, double, float, boolean, char, byte, short) et des types référence (String, tableaux, objets). Utilisez 'final' pour les constantes. 'var' (Java 10+) infère le type à la compilation — utilisez-le pour les variables locales aux types évidents. Les tirets bas dans les nombres (100_000) améliorent la lisibilité.

java
// Primitive types (8 total)
int age = 30;              // 32-bit integer
long bigNum = 100_000L;   // 64-bit integer
double price = 19.99;     // 64-bit float (default for decimals)
float pi = 3.14f;         // 32-bit float
boolean active = true;    // true/false
char grade = 'A';         // 16-bit Unicode character
byte b = 127;             // 8-bit signed
short s = 32767;          // 16-bit signed

// Reference types
String name = "Alice";    // Object (not primitive)
int[] nums = {1, 2, 3};  // Array object

// Constants
final double PI = 3.14159; // can't be reassigned

// var (Java 10+, local type inference)
var count = 42;    // inferred as int
var list = new ArrayList<String>(); // inferred as ArrayList<String>

Classes wrapper & boxing

Les classes wrapper (Integer, Double, Boolean, etc.) sont des versions objet des primitifs. L'autoboxing/unboxing convertit automatiquement. Integer met en cache les valeurs -128 à 127, donc == fonctionne pour les petits nombres mais échoue pour les grands — utilisez toujours .equals(). Les wrappers sont nécessaires pour les Collections (qui ne peuvent pas contenir de primitifs).

java
// Wrapper classes (Object versions of primitives)
Integer wrapped = Integer.valueOf(42);  // explicit
Integer auto = 42;                       // autoboxing
int unboxed = auto;                      // unboxing

// Useful methods
int max = Integer.MAX_VALUE;     // 2147483647
String bin = Integer.toBinaryString(42);
int parsed = Integer.parseInt("42");
String s = String.valueOf(42);

// Other wrappers: Double, Boolean, Character, Long, Float
Double d = 3.14;
Boolean b = Boolean.TRUE;
Character c = 'A';

// Be careful with == on wrappers
Integer a = 127, b2 = 127;  // a == b2: true (cached)
Integer x = 128, y = 128;   // x == y: false (not cached)
// Always use .equals() for Integer comparison

Packages & imports

Les packages organisent les classes et évitent les conflits de nommage. Convention : nom de domaine inversé (com.example.app). Importez des classes spécifiques ou utilisez des wildcards (*). Les imports statiques ramènent des constantes et méthodes (Math.PI, Math.sqrt). Les noms pleinement qualifiés fonctionnent sans imports mais sont verbeux. Le package java.lang est auto-importé.

java
// Package declaration (must be first line)
package com.example.app;

// Import specific class
import java.util.List;
import java.util.ArrayList;

// Import all classes in a package
import java.util.*;

// Static import (for static members)
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

// Usage
double area = PI * 5 * 5;
double root = sqrt(16);

// Fully qualified name (no import needed)
java.time.LocalDate today = java.time.LocalDate.now();

// Package naming convention: reverse domain
// com.google.gson, org.apache.commons, io.netty.channel

Entrée & sortie

System.out (stdout), System.err (stderr), System.in (stdin). Scanner est le moyen le plus simple de lire l'entrée console — il parse les tokens (nextInt, nextDouble, nextLine). Fermez toujours Scanner pour libérer les ressources. Les arguments en ligne de commande sont dans args[] (args[0] est le premier argument, pas le nom du programme comme en C).

java
import java.util.Scanner;

// Console output
System.out.println("Hello");      // with newline
System.out.print("No newline");   // without newline
System.out.printf("Pi: %.2f%n", 3.14159); // formatted

// Console input with Scanner
Scanner scanner = new Scanner(System.in);
System.out.print("Enter name: ");
String name = scanner.nextLine();

System.out.print("Enter age: ");
int age = scanner.nextInt();

System.out.printf("Hi %s, age %d%n", name, age);
scanner.close(); // always close

// Command-line arguments
// java Main arg1 arg2
// args[0] = "arg1", args[1] = "arg2"
02

Chaînes & formatage

Méthodes de String

Les chaînes sont immuables — les méthodes renvoient de nouvelles chaînes. Utilisez toujours .equals() pour la comparaison de contenu (== compare les références). compareTo() renvoie négatif/zéro/positif pour l'ordre (utile pour le tri). split() renvoie un String[]. Pour des chaînes mutables, utilisez StringBuilder.

java
String s = "Hello, World";

// Length and access
int len = s.length();        // 12
char c = s.charAt(0);        // 'H'

// Comparison
s.equals("Hello, World");    // true (content comparison)
s.equalsIgnoreCase("hello, world"); // true
s.compareTo("Apple");        // positive (s > "Apple")
"abc".compareTo("abd");      // negative

// Search
s.indexOf("World");          // 7 (-1 if not found)
s.lastIndexOf("l");          // 10
s.contains("World");         // true
s.startsWith("Hello");       // true
s.endsWith("World");         // true

// Extract
s.substring(7);              // "World"
s.substring(0, 5);           // "Hello"

// Transform
s.toUpperCase();             // "HELLO, WORLD"
s.toLowerCase();             // "hello, world"
s.replace("o", "0");         // "Hell0, W0rld"
s.trim();                    // remove whitespace
s.split(", ");               // ["Hello", "World"]

StringBuilder & concaténation

La concaténation de chaînes avec + crée un nouveau String à chaque fois (inefficace dans les boucles). StringBuilder est mutable et efficace pour construire des chaînes incrémentalement. StringBuffer est la version thread-safe (rarement nécessaire). String.join() combine avec un délimiteur. Java 11+ ajoute repeat() pour la multiplication de chaînes.

java
// String concatenation (creates new String each time)
String s = "Hello" + ", " + "World";
String formatted = String.format("%s is %d", "Alice", 30);

// StringBuilder (mutable, efficient for many concatenations)
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World");
sb.insert(5, " there");
sb.delete(5, 11);
sb.reverse();
String result = sb.toString();

// StringBuffer (thread-safe, slower than StringBuilder)
StringBuffer sbf = new StringBuffer("thread-safe");

// Join strings
String joined = String.join(", ", "a", "b", "c"); // "a, b, c"

// Repeat (Java 11+)
String repeated = "ab".repeat(3); // "ababab"

Formatage de chaînes

printf/format utilise des spécificateurs de format de style C : %d (int), %f (float), %s (chaîne), %c (char), %b (booléen), %x (hex). Largeur (%5d), alignement à gauche (%-5d), remplissage zéro (%05d), précision (%.2f). %n est la nouvelle ligne de la plateforme. Les blocs de texte (Java 15+) avec des triples guillemets permettent des chaînes multilignes sans échappement.

java
// printf / format specifiers
System.out.printf("Int: %d%n", 42);
System.out.printf("Float: %.2f%n", 3.14159);  // 3.14
System.out.printf("String: %s%n", "hello");
System.out.printf("Char: %c%n", 'A');
System.out.printf("Bool: %b%n", true);
System.out.printf("Hex: %x%n", 255);           // ff
System.out.printf("Octal: %o%n", 8);           // 10

// Width and padding
System.out.printf("[%5d]%n", 42);      // [   42]
System.out.printf("[%-5d]%n", 42);     // [42   ]
System.out.printf("[%05d]%n", 42);     // [00042]
System.out.printf("[%8.2f]%n", 3.14);  // [    3.14]

// String.format returns a String
String s = String.format("Name: %s, Age: %d", "Alice", 30);

// Text blocks (Java 15+)
String json = """
    {
        "name": "Alice",
        "age": 30
    }
    """;

Expressions régulières

Les regex Java utilisent Pattern (compilé) et Matcher (appliqué à l'entrée). Les méthodes de String (matches, split, replaceAll) sont des raccourcis pratiques. Les barres obliques inverses doivent être doublées dans les littéraux de chaîne Java (\\d pour \d). Les groupes sont capturés avec des parenthèses et référencés comme $1, $2 dans les remplacements. Compilez toujours les motifs une fois si utilisés répétitivement.

java
import java.util.regex.*;

// String methods
"hello123".matches("[a-z]+\d+"); // true
"a,b,c".split(",");               // ["a", "b", "c"]
"hello".replaceAll("l", "L");     // "heLLo"

// Pattern and Matcher
Pattern p = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
Matcher m = p.matcher("Date: 2024-01-15");

if (m.find()) {
    System.out.println(m.group()); // "2024-01-15"
}

// Find all matches
while (m.find()) {
    System.out.println(m.group());
}

// Replace with regex
String result = "2024-01-15".replaceAll(
    "(\\d{4})-(\\d{2})-(\\d{2})",
    "$3/$2/$1"); // "15/01/2024"

// Common patterns
String email = "^[\\w.]+@[\\w.]+\\.\\w+$";
String phone = "^\\d{3}-\\d{4}$";

Nombres & Math

La classe Math fournit des fonctions mathématiques statiques. Math.random() renvoie 0.0-1.0. Pour plus de contrôle, utilisez java.util.Random (seedable) ou java.security.SecureRandom (cryptographique). Integer/Double ont des méthodes utilitaires statiques. Attention à la précision des nombres à virgule flottante — utilisez BigDecimal pour les calculs financiers.

java
// Math class
double sqrt = Math.sqrt(16);     // 4.0
double pow = Math.pow(2, 10);    // 1024.0
int abs = Math.abs(-5);          // 5
int max = Math.max(3, 7);        // 7
int min = Math.min(3, 7);        // 3
double rounded = Math.round(3.7); // 4
double ceil = Math.ceil(3.1);    // 4.0
double floor = Math.floor(3.9);  // 3.0
double random = Math.random();   // 0.0 to 1.0

// Constants
double pi = Math.PI;             // 3.14159...
double e = Math.E;               // 2.71828...

// Integer/Long methods
int sum = Integer.sum(3, 4);     // 7
int max2 = Integer.max(3, 7);    // 7

// Rounding modes
double r = Math.round(3.5);      // 4 (round half up)
double r2 = Math.floor(3.5 + 0.5); // alternative

// Random (java.util.Random)
import java.util.Random;
Random rand = new Random();
int n = rand.nextInt(100);  // 0-99
double d = rand.nextDouble(); // 0.0-1.0
boolean b = rand.nextBoolean();
03

Flux de contrôle

If / Else

Le if/else Java fonctionne comme en C/C++. Les conditions doivent être booléennes — pas de truthy/falsy comme en JavaScript (0 et les chaînes non vides ne sont PAS truthy). L'opérateur ternaire (cond ? a : b) est une expression, pas une instruction. Utilisez des accolades même pour les corps d'une ligne (bonne pratique de style de code).

java
int score = 85;

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else if (score >= 70) {
    System.out.println("C");
} else {
    System.out.println("F");
}

// Ternary operator
String grade = score >= 60 ? "Pass" : "Fail";

// Nested if
if (score >= 60) {
    if (score >= 90) {
        System.out.println("Excellent");
    }
}

// Note: conditions must be boolean (no truthy/falsy)
// if (score) { } // Error: int is not boolean

Switch & expressions

Le switch traditionnel a un effet de chute (utilisez break). Les expressions switch de Java 14+ (->) n'ont pas d'effet de chute et peuvent renvoyer des valeurs. Utilisez des virgules pour de multiples labels case (case 1, 2, 3). 'yield' renvoie une valeur d'un bloc complexe. Les expressions switch sont exhaustives — vous avez besoin d'un default ou de tous les cas pour les enums.

java
// Traditional switch (fall-through)
int day = 3;
switch (day) {
    case 1:
        System.out.println("Mon");
        break;
    case 2:
    case 3:
    case 4:
        System.out.println("Midweek");
        break;
    case 6:
    case 7:
        System.out.println("Weekend");
        break;
    default:
        System.out.println("Invalid");
}

// Switch expression (Java 14+, no fall-through)
String type = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Invalid";
};

// Switch with yield (for complex blocks)
int result = switch (day) {
    case 1, 2, 3, 4, 5 -> {
        int hours = 8;
        yield hours * 5;
    }
    case 6, 7 -> 0;
    default -> -1;
};

Boucles

Java a les boucles for, while et do-while. Le for amélioré (for-each) fonctionne avec les tableaux et n'importe quel Iterable. break sort de la boucle ; continue passe à l'itération suivante. Pour les collections, préférez for-each ou les streams aux boucles indexées. do-while s'exécute au moins une fois (rarement utilisé).

java
// For loop
for (int i = 0; i < 5; i++) {
    System.out.println(i);
}

// Enhanced for (for-each)
int[] nums = {1, 2, 3, 4, 5};
for (int n : nums) {
    System.out.println(n);
}

List<String> names = List.of("Alice", "Bob");
for (String name : names) {
    System.out.println(name);
}

// While loop
int count = 0;
while (count < 3) {
    System.out.println(count);
    count++;
}

// Do-while (runs at least once)
int i = 0;
do {
    System.out.println(i);
    i++;
} while (i < 3);

// Break and continue
for (int j = 0; j < 10; j++) {
    if (j == 5) break;      // exit loop
    if (j % 2 == 0) continue; // skip iteration
    System.out.println(j);
}

Break & continue étiquetés

Les labels (outer:) permettent de breaker/continuer les boucles externes depuis des boucles imbriquées. C'est rarement nécessaire — extraire vers une méthode avec return est généralement plus propre. Les labels sont placés avant la boucle, suivis de deux-points. break label sort de la boucle étiquetée ; continue label passe à son itération suivante.

java
// Labels for breaking out of nested loops
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (i == 1 && j == 1) {
            break outer; // exits both loops
        }
        System.out.println(i + "," + j);
    }
}

// Labeled continue
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 1) {
            continue outer; // skip to next i
        }
        System.out.println(i + "," + j);
    }
}

// Alternative: extract to method and use return
void findPair(int[][] matrix, int target) {
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[i].length; j++) {
            if (matrix[i][j] == target) return; // exit method
        }
    }
}

Tableaux

Les tableaux ont une longueur fixe (utilisez ArrayList pour le dynamique). Arrays.sort() trie sur place. Arrays.toString() donne une représentation lisible. Arrays.copyOf() crée une copie avec une nouvelle longueur. Pour les tableaux multidimensionnels, chaque ligne peut avoir une longueur différente (tableaux en dents de scie). Utilisez Arrays pour les méthodes utilitaires sur les tableaux.

java
// Declare and initialize
int[] nums = {1, 2, 3, 4, 5};
int[] empty = new int[5]; // [0, 0, 0, 0, 0]
String[] names = new String[3]; // [null, null, null]

// Access and modify
nums[0] = 10;
int first = nums[0]; // 10
int length = nums.length; // 5

// Multidimensional
int[][] matrix = {{1, 2}, {3, 4}};
int val = matrix[0][1]; // 2

// Arrays utility class
import java.util.Arrays;
int[] sorted = {3, 1, 2};
Arrays.sort(sorted); // [1, 2, 3]
int[] copy = Arrays.copyOf(nums, 3);
String str = Arrays.toString(nums); // "[10, 2, 3, 4, 5]"
boolean eq = Arrays.equals(nums, copy);

// Fill
int[] filled = new int[5];
Arrays.fill(filled, 42); // [42, 42, 42, 42, 42]

// Binary search (sorted array only)
int idx = Arrays.binarySearch(sorted, 2); // index of 2
04

Méthodes & fonctions

Définition de méthode

Les méthodes Java sont toujours à l'intérieur d'une classe. 'static' signifie que la méthode appartient à la classe (appel sans instance). Le type de retour (int, String, void) est déclaré avant le nom. Les paramètres sont typés. Java n'a pas de valeurs de paramètre par défaut — utilisez la surcharge de méthode à la place.

java
public class Calculator {
    // Method with return type
    public static int add(int a, int b) {
        return a + b;
    }

    // Void method (no return)
    public static void printResult(int result) {
        System.out.println("Result: " + result);
    }

    // Method with default (no overloading needed)
    public static String greet(String name, String greeting) {
        return greeting + ", " + name + "!";
    }

    public static void main(String[] args) {
        int sum = add(3, 4);
        printResult(sum);

        String msg = greet("Alice", "Hello");
        System.out.println(msg);
    }
}

Surcharge de méthode

La surcharge de méthode permet plusieurs méthodes avec le même nom mais des listes de paramètres différentes (type, nombre ou ordre). Java résout les surcharges à la compilation selon les types d'arguments. La surcharge est courante pour les constructeurs et méthodes utilitaires. C'est différent de la redéfinition (qui implique l'héritage et le dispatch à l'exécution).

java
public class MathUtils {
    // Overloaded methods (same name, different params)
    public static int add(int a, int b) {
        return a + b;
    }

    public static double add(double a, double b) {
        return a + b;
    }

    public static int add(int a, int b, int c) {
        return a + b + c;
    }

    public static String add(String a, String b) {
        return a + b;
    }
}

// Java picks the most specific match
MathUtils.add(1, 2);        // int version -> 3
MathUtils.add(1.5, 2.5);    // double version -> 4.0
MathUtils.add(1, 2, 3);     // 3-param version -> 6
MathUtils.add("Hello", "!"); // String version -> "Hello!"

Varargs & passage par valeur

Les varargs (Type... name) permettent des arguments variables, reçus comme un tableau. Java est toujours passage par valeur : les primitifs sont copiés, les références d'objet sont copiées (mais pointent vers le même objet). Donc modifier un paramètre à l'intérieur d'une méthode n'affecte pas la variable de l'appelant, mais modifier l'objet pointé oui.

java
// Varargs: variable number of arguments
public static int sum(int... nums) {
    int total = 0;
    for (int n : nums) {
        total += n;
    }
    return total;
}

sum(1, 2, 3);           // 6
sum(1, 2, 3, 4, 5);     // 15
sum();                  // 0 (empty array)
int[] arr = {1, 2, 3};
sum(arr);               // 6 (pass array to varargs)

// Java is ALWAYS pass-by-value
public static void modify(int x) {
    x = 100; // doesn't affect the caller's variable
}

int n = 5;
modify(n);
System.out.println(n); // still 5

// For objects, the reference is passed by value
public static void addItem(List<String> list) {
    list.add("new"); // modifies the same list object
}

Récursion

La récursion est quand une méthode s'appelle elle-même. Ayez toujours un cas de base pour arrêter. Java n'optimise pas la récursion terminale (contrairement à certains langages), donc une récursion profonde peut causer StackOverflowError. Pour la récursion critique en performance ou profonde, convertissez en itération. La mémoïsation (mise en cache des résultats) peut accélérer les solutions récursives comme Fibonacci.

java
// Factorial
public static int factorial(int n) {
    if (n <= 1) return 1;        // base case
    return n * factorial(n - 1); // recursive case
}
// factorial(5) = 5 * 4 * 3 * 2 * 1 = 120

// Fibonacci
public static int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

// Tail recursion (Java doesn't optimize this)
public static int factorialTail(int n, int acc) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);
}
// Call: factorialTail(5, 1)

// Be careful: deep recursion causes StackOverflowError
// For deep recursion, use iteration or a loop instead

Expressions lambda

Les lambdas (Java 8+) sont des fonctions anonymes. Le type est une interface fonctionnelle (une méthode abstraite). Coursantes : Function<T,R> (entrée→sortie), Predicate<T> (test booléen), Consumer<T> (consommer, pas de retour), Supplier<T> (produire, pas d'entrée). Les références de méthode (String::length) sont un raccourci pour les lambdas qui appellent une seule méthode.

java
import java.util.function.*;

// Lambda syntax: (params) -> expression
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> length = s -> s.length();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

// With type annotations
BinaryOperator<Integer> multiply = (Integer a, Integer b) -> a * b;

// Multi-line lambda
Function<String, String> process = s -> {
    String upper = s.toUpperCase();
    return upper.substring(0, 3);
};

// Predicate (boolean test)
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isEmpty = String::isEmpty; // method reference

// Consumer (no return)
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer2 = System.out::println; // method reference

// Supplier (no input, produces value)
Supplier<Double> random = () -> Math.random();

// Usage
int result = square.apply(5); // 25
boolean even = isEven.test(4); // true
printer.accept("Hello"); // prints "Hello"
05

Classes & POO

Classe & constructeur

Les classes sont des modèles pour les objets. Les champs stockent l'état, les méthodes définissent le comportement. Les constructeurs initialisent les nouveaux objets (utilisez 'this' pour distinguer les champs des paramètres). @Override indique qu'une méthode redéfinit une méthode de superclasse (toString vient de Object). Encapsulation : champs privés, getters/setters publics.

java
public class Person {
    // Fields (instance variables)
    private String name;
    private int age;

    // Constructor
    public Person(String name, int age) {
        this.name = name;  // 'this' refers to the current instance
        this.age = age;
    }

    // Methods
    public String getName() { return name; }
    public int getAge() { return age; }

    public void setAge(int age) {
        if (age >= 0) this.age = age;
    }

    public String greet() {
        return "Hi, I'm " + name;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

// Usage
Person p = new Person("Alice", 30);
System.out.println(p.getName());  // "Alice"
System.out.println(p);            // uses toString()

Modificateurs d'accès & static

Modificateurs d'accès : public (partout), private (classe seulement), protected (classe + sous-classes + package), default/package-private (même package). Les membres statiques appartiennent à la classe, pas aux instances — partagés entre tous les objets. Les initialiseurs statiques s'exécutent une fois au chargement de la classe. Utilisez static pour les constantes (static final), méthodes utilitaires et compteurs.

java
public class BankAccount {
    // Access modifiers:
    public String owner;      // accessible everywhere
    private double balance;   // class only
    protected String type;    // class + subclasses + same package
    String id;                // package-private (default)

    // Static field (shared by all instances)
    private static int accountCount = 0;

    // Static constant
    public static final double MIN_BALANCE = 100.0;

    // Static method (call without instance)
    public static int getAccountCount() {
        return accountCount;
    }

    // Static initializer (runs once when class loads)
    static {
        System.out.println("BankAccount class loaded");
    }

    public BankAccount(String owner) {
        this.owner = owner;
        this.balance = MIN_BALANCE;
        accountCount++; // increment shared counter
    }
}

int count = BankAccount.getAccountCount(); // static method call

Héritage & super

Java utilise 'extends' pour l'héritage de classe (héritage simple uniquement). super() appelle le constructeur parent (doit être la première ligne). @Override indique la redéfinition de méthode (polymorphisme à l'exécution). Un Dog IS-A Animal. Utilisez l'héritage pour les relations « est-un » ; utilisez la composition (a-un) pour la réutilisation de code. Java 17+ supporte les classes sealed pour restreindre l'héritage.

java
// Parent class
class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
        System.out.println("Animal constructor");
    }

    public void eat() {
        System.out.println(name + " is eating");
    }
}

// Child class (extends)
class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name); // must be first line — call parent constructor
        this.breed = breed;
    }

    // Override parent method
    @Override
    public void eat() {
        super.eat(); // call parent's eat()
        System.out.println(name + " the " + breed + " eats dog food");
    }

    public void bark() {
        System.out.println("Woof!");
    }
}

Dog dog = new Dog("Rex", "Labrador");
dog.eat();   // calls Dog's eat()
dog.bark();  // Dog-specific method

Classes abstraites & interfaces

Les classes abstraites ne peuvent pas être instanciées et peuvent avoir à la fois des méthodes abstraites (sans corps) et concrètes. Les interfaces définissent des contrats — toutes les méthodes sont public abstract par défaut. Java 8+ permet les méthodes default (avec corps) et statiques dans les interfaces. Une classe étend une classe abstraite mais peut implémenter plusieurs interfaces. Utilisez les classes abstraites pour le code partagé, les interfaces pour les contrats.

java
// Abstract class (can't be instantiated)
abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    // Abstract method (must be implemented by subclasses)
    public abstract double area();

    // Concrete method (inherited)
    public String describe() {
        return color + " " + this.getClass().getSimpleName();
    }
}

// Interface (pure contract, Java 8+ can have default methods)
interface Drawable {
    void draw(); // abstract by default

    // Default method (Java 8+)
    default void drawTwice() {
        draw();
        draw();
    }

    // Static method in interface
    static Drawable empty() {
        return () -> System.out.println("nothing");
    }
}

// A class can extend one class and implement multiple interfaces
class Circle extends Shape implements Drawable {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing " + describe());
    }
}

Polymorphisme & cast

Polymorphisme : une référence parent peut contenir un objet enfant. Les appels de méthode dispatchent vers l'implémentation de l'objet réel (polymorphisme à l'exécution). Utilisez instanceof avant un downcast pour éviter ClassCastException. Java 16+ pattern matching (instanceof Circle c) combine vérification et cast. Redéfinissez equals() et hashCode() ensemble pour un comportement correct dans les collections.

java
// Polymorphism: one interface, many forms
Shape s1 = new Circle("red", 5);
Shape s2 = new Square("blue", 3);

// Calls the overridden method (runtime dispatch)
System.out.println(s1.area()); // Circle's area
System.out.println(s2.area()); // Square's area

// instanceof check
if (s1 instanceof Circle) {
    Circle c = (Circle) s1; // downcast
    System.out.println("Radius: " + c.radius);
}

// Pattern matching (Java 16+)
if (s1 instanceof Circle c) {
    System.out.println("Radius: " + c.radius); // c is already cast
}

// Upcasting (automatic)
Circle circle = new Circle("green", 2);
Shape shape = circle; // upcast (no explicit cast needed)

// Object class methods (all classes inherit from Object)
circle.equals(circle);   // reference equality by default
circle.hashCode();       // hash code
circle.getClass();       // Class<Circle>
circle.toString();       // string representation

Records & Enums

Les records (Java 16+) sont des classes de données immuables — le compilateur génère constructeur, accesseurs, equals, hashCode et toString. Utilisez-les pour les DTO et objets de valeur. Les enums sont des constantes type-safe qui peuvent avoir des champs, méthodes et constructeurs. Les enums implémentent Comparable et ont des méthodes values() et valueOf(). Les deux sont essentiels pour le Java moderne.

java
// Record (Java 16+): concise data class
public record Point(int x, int y) {}

// Equivalent to a class with:
// - final fields x, y
// - constructor
// - getters x(), y()
// - equals, hashCode, toString

Point p = new Point(3, 4);
System.out.println(p.x());      // 3
System.out.println(p.y());      // 4
System.out.println(p);          // Point[x=3, y=4]

// Compact constructor (validation)
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

// Enum (named constants)
public enum Direction {
    UP, DOWN, LEFT, RIGHT;

    public Direction opposite() {
        return switch (this) {
            case UP -> DOWN;
            case DOWN -> UP;
            case LEFT -> RIGHT;
            case RIGHT -> LEFT;
        };
    }
}

Direction d = Direction.UP;
Direction opp = d.opposite(); // DOWN
06

Collections & génériques

List (ArrayList & LinkedList)

ArrayList est soutenu par un tableau (get/set rapides, insert/delete lent au milieu). LinkedList est soutenu par une liste doublement chaînée (insert/delete rapides aux extrémités, accès aléatoire lent). List.of() crée des listes immuables. Utilisez ArrayList dans la plupart des cas ; LinkedList seulement pour les opérations fréquentes en extrémité. Les deux implémentent l'interface List.

java
import java.util.*;

// ArrayList (fast random access, slow insert/delete in middle)
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add(0, "Carol");     // insert at index
list.set(1, "Dave");      // replace at index
String name = list.get(0); // "Carol"
list.remove(0);            // remove by index
list.remove("Dave");       // remove by value
int size = list.size();    // 1
boolean has = list.contains("Bob");

// LinkedList (fast insert/delete at ends)
LinkedList<Integer> linked = new LinkedList<>();
linked.addFirst(1);
linked.addLast(2);
linked.removeFirst();
linked.peek(); // see first element

// Immutable list (Java 9+)
List<String> immutable = List.of("a", "b", "c");
// immutable.add("d"); // UnsupportedOperationException

// Iterate
for (String s : list) {
    System.out.println(s);
}
list.forEach(System.out::println); // method reference

Set (HashSet & TreeSet)

Set stocke des éléments uniques. HashSet est le plus rapide mais non ordonné. TreeSet garde les éléments triés (ordre naturel ou Comparator). LinkedHashSet maintient l'ordre d'insertion. Opérations sur les sets : addAll (union), retainAll (intersection), removeAll (différence). Pour des objets personnalisés dans un HashSet, redéfinissez equals() et hashCode().

java
import java.util.*;

// HashSet (fast, unordered)
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // duplicate ignored
System.out.println(set.size()); // 2
System.out.println(set.contains("apple")); // true
set.remove("banana");

// TreeSet (sorted, slower)
Set<Integer> sorted = new TreeSet<>();
sorted.add(3);
sorted.add(1);
sorted.add(2);
System.out.println(sorted); // [1, 2, 3]

// LinkedHashSet (maintains insertion order)
Set<String> ordered = new LinkedHashSet<>();
ordered.add("c");
ordered.add("a");
ordered.add("b");
System.out.println(ordered); // [c, a, b]

// Set operations
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> b = new HashSet<>(Set.of(2, 3, 4));
a.addAll(b);    // union: [1, 2, 3, 4]
a.retainAll(b); // intersection: [2, 3]
a.removeAll(b); // difference: [1]

// Immutable set
Set<String> immutable = Set.of("x", "y", "z");

Map (HashMap & TreeMap)

Map stocke des paires clé-valeur. HashMap est le plus rapide (non ordonné). TreeMap trie par clés. LinkedHashMap maintient l'ordre d'insertion. getOrDefault évite les vérifications null. compute/merge sont puissants pour mettre à jour les valeurs. Pour des clés personnalisées, redéfinissez equals() et hashCode(). Map.of() crée des maps immuables (Java 9+).

java
import java.util.*;

// HashMap (fast, unordered)
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31); // overwrite

// Access
int age = ages.get("Alice"); // 31
int defaultAge = ages.getOrDefault("Eve", 0); // 0

// Check
boolean has = ages.containsKey("Alice");
boolean hasVal = ages.containsValue(25);

// Remove
ages.remove("Bob");

// Iterate
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

ages.forEach((key, val) -> System.out.println(key + "=" + val));

// Compute (Java 8+)
ages.compute("Alice", (k, v) -> v + 1); // increment
ages.putIfAbsent("Carol", 28);
ages.merge("Alice", 1, Integer::sum); // add 1

// TreeMap (sorted by keys)
Map<String, Integer> sorted = new TreeMap<>();
// LinkedHashMap (maintains insertion order)
Map<String, Integer> ordered = new LinkedHashMap<>();

Queue & Deque

Queue est FIFO (premier entré, premier sorti). Deque est à double extrémité (peut ajouter/supprimer des deux côtés). PriorityQueue ordonne les éléments par ordre naturel ou un Comparator (min-heap par défaut). Pour une pile, utilisez ArrayDeque (push/pop) au lieu de la classe legacy Stack. ArrayDeque est plus rapide que LinkedList pour les opérations queue/deque.

java
import java.util.*;

// Queue (FIFO)
Queue<String> queue = new LinkedList<>();
queue.add("first");   // throws if full (capacity-restricted)
queue.offer("second"); // returns false if full
String head = queue.peek(); // see head (null if empty)
String removed = queue.poll(); // remove and return head

// Deque (double-ended)
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.peekFirst(); // 1
deque.peekLast();  // 2
deque.pollFirst(); // 1
deque.pollLast();  // 2

// PriorityQueue (min-heap by default)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(3);
pq.add(1);
pq.add(2);
System.out.println(pq.poll()); // 1 (smallest first)

// Max-heap (reverse order)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.add(1);
maxHeap.add(3);
System.out.println(maxHeap.poll()); // 3 (largest first)

// Stack (legacy, prefer Deque)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // add to front
stack.push(2);
stack.pop();   // 2 (remove from front)

Génériques

Les génériques permettent des collections et classes type-safe. <T> est un paramètre de type. Les types bornés (<T extends Comparable<T>>) restreignent aux types avec certains comportements. Wildcards : ? (any), ? extends T (covariant, lecture seule), ? super T (contravariant, écriture seule). Les génériques utilisent l'effacement de type — les types sont vérifiés à la compilation, effacés à l'exécution.

java
// Generic class
public class Box<T> {
    private T value;

    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();

Box<Integer> intBox = new Box<>();
intBox.set(42);

// Generic method
public static <T> T firstOf(List<T> list) {
    return list.get(0);
}

String first = firstOf(List.of("a", "b"));

// Bounded type parameter
public static <T extends Comparable<T>> T max(List<T> list) {
    T result = list.get(0);
    for (T item : list) {
        if (item.compareTo(result) > 0) {
            result = item;
        }
    }
    return result;
}

// Wildcards
void process(List<?> list) { }          // any type
void processNums(List<? extends Number> list) { } // Number or subclass
void addNums(List<? super Integer> list) { }      // Integer or superclass

Iterators & Comparable

Iterator permet la suppression sûre pendant l'itération (it.remove()). ListIterator ajoute le parcours bidirectionnel et set/add. Comparable définit l'ordre naturel (compareTo). Comparator définit l'ordre personnalisé (comparing, comparingInt, reversed, thenComparing). Utilisez Comparator.comparing() pour un tri fluide. Collections.sort() utilise l'ordre naturel.

java
import java.util.*;

// Iterator
List<String> list = List.of("a", "b", "c");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
    // it.remove(); // safe removal during iteration
}

// ListIterator (bidirectional)
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
    lit.set(lit.next().toUpperCase()); // replace
}

// Comparable (natural ordering)
public class Person implements Comparable<Person> {
    String name;
    int age;

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

// Comparator (custom ordering)
Comparator<Person> byName = Comparator.comparing(p -> p.name);
Comparator<Person> byAgeDesc = Comparator.comparingInt((Person p) -> p.age).reversed();

List<Person> people = new ArrayList<>();
people.sort(byName);
people.sort(byAgeDesc);
Collections.sort(people); // uses Comparable
07

Streams & fonctionnel

Bases des Streams

Les Streams (Java 8+) fournissent un traitement de données déclaratif. Créez avec .stream() (collections) ou Stream.of(). Les opérations intermédiaires (filter, map, sorted) sont paresseuses — elles ne s'exécutent que lorsqu'une opération terminale (collect, reduce, count, forEach) est appelée. toList() (Java 16+) est une alternative concise à collect(Collectors.toList()).

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

// Filter and collect
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // [2, 4, 6]

// Map (transform)
List<String> doubled = nums.stream()
    .map(n -> "num" + n)
    .collect(Collectors.toList());

// Reduce
int sum = nums.stream().reduce(0, Integer::sum); // 21
int product = nums.stream().reduce(1, (a, b) -> a * b);

// Count
long count = nums.stream().filter(n -> n > 3).count(); // 3

// Find
Optional<Integer> first = nums.stream().filter(n -> n > 3).findFirst();
boolean anyMatch = nums.stream().anyMatch(n -> n > 5);
boolean allMatch = nums.stream().allMatch(n -> n > 0);

// ForEach
nums.stream().forEach(System.out::println);

// toList() shortcut (Java 16+)
List<Integer> result = nums.stream().filter(n -> n > 3).toList();

Opérations de Stream

sorted() ordonne les éléments (naturel ou avec Comparator). distinct() supprime les doublons. limit(n)/skip(n) paginent. flatMap aplatit les streams imbriqués — essentiel pour les transformations un-à-plusieurs. peek() est pour le débogage (effets de bord). groupingBy crée des maps groupant les éléments par un classifieur. Les streams sont paresseux — les opérations s'enchaînent efficacement.

java
List<String> names = List.of("Alice", "Bob", "Charlie", "David");

// Sorted
List<String> sorted = names.stream()
    .sorted()
    .toList();

// Sorted by length
List<String> byLength = names.stream()
    .sorted(Comparator.comparing(String::length))
    .toList();

// Distinct
List<Integer> distinct = List.of(1, 2, 2, 3, 3, 3).stream()
    .distinct()
    .toList(); // [1, 2, 3]

// Limit and Skip
List<Integer> limited = nums.stream()
    .skip(2)   // skip first 2
    .limit(3)  // take next 3
    .toList();

// FlatMap (flatten nested structures)
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .toList(); // [1, 2, 3, 4]

// Peek (debug, side-effect)
nums.stream()
    .peek(n -> System.out.println("before: " + n))
    .filter(n -> n > 2)
    .peek(n -> System.out.println("after: " + n))
    .toList();

// Grouping
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));

Collectors & réduction

Les Collectors fournissent des opérations de réduction riches : joining (concaténer des chaînes), groupingBy (grouper par clé), partitioningBy (séparer par booléen), toMap (créer une map), summarizingInt (stats : count, sum, min, max, average). Les Collectors peuvent être composés (groupingBy avec collector en aval). Ils remplacent les boucles verbeuses par des one-liners déclaratifs.

java
import java.util.stream.*;

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
);

// Join strings
String joined = people.stream()
    .map(Person::getName)
    .collect(Collectors.joining(", ")); // "Alice, Bob, Charlie"

// Group by
Map<Integer, List<Person>> byAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));

// Partition (boolean)
Map<Boolean, List<Person>> partition = people.stream()
    .collect(Collectors.partitioningBy(p -> p.getAge() > 28));

// Count by group
Map<Integer, Long> countByAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));

// Summarizing
IntSummaryStatistics stats = people.stream()
    .collect(Collectors.summarizingInt(Person::getAge));
System.out.println(stats.getAverage()); // 30.0
System.out.println(stats.getMax());     // 35

// To map
Map<String, Integer> nameToAge = people.stream()
    .collect(Collectors.toMap(Person::getName, Person::getAge));

// Reducing
int totalAge = people.stream()
    .collect(Collectors.reducing(0, Person::getAge, Integer::sum));

Optional

Optional<T> est un conteneur qui peut ou non contenir une valeur. Il force la gestion explicite de l'absence — fini les NullPointerException. Utilisez of() pour des valeurs non null, ofNullable() pour du possiblement null. Chaînez avec map/flatMap/filter. N'utilisez jamais get() sans isPresent() — préférez orElse/orElseThrow. Optional est conçu pour les types de retour, pas les champs.

java
import java.util.Optional;

// Creating Optional
Optional<String> present = Optional.of("hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null); // empty if null

// Checking
present.isPresent(); // true
empty.isEmpty();     // true (Java 11+)

// Getting values
String val = present.get(); // throws if empty (avoid!)
String safe = present.orElse("default");
String computed = present.orElseGet(() -> computeDefault());
String orThrow = present.orElseThrow(() -> new RuntimeException("missing"));

// Transform (map/flatMap)
Optional<Integer> length = present.map(String::length); // Optional[5]
Optional<String> upper = present.map(s -> s.toUpperCase());

// Filter
Optional<String> filtered = present.filter(s -> s.length() > 3);

// ifPresent
present.ifPresent(s -> System.out.println(s));
present.ifPresentOrElse(
    s -> System.out.println("Got: " + s),
    () -> System.out.println("Empty")
);

// Chaining (avoid null checks)
String result = getUser(1)
    .map(User::getProfile)
    .map(Profile::getEmail)
    .orElse("no email");

Interfaces fonctionnelles

Les interfaces fonctionnelles ont exactement une méthode abstraite (peuvent avoir plusieurs méthodes default). @FunctionalInterface est optionnel mais documente l'intention. Java en fournit beaucoup dans java.util.function : Function, Predicate, Consumer, Supplier, plus les variantes Bi- et primitives. Utilisez-les au lieu de créer des interfaces personnalisées quand possible. Elles permettent les expressions lambda et références de méthode.

java
import java.util.function.*;

// Built-in functional interfaces
Function<String, Integer> strToInt = Integer::parseInt;
BiFunction<String, String, String> concat = String::concat;

Predicate<String> isEmpty = String::isEmpty;
BiPredicate<String, String> contains = String::contains;

Consumer<String> printer = System.out::println;
BiConsumer<String, Integer> printPair = (s, i) -> System.out.println(s + ":" + i);

Supplier<List<String>> listFactory = ArrayList::new;

// Primitive specializations
IntFunction<String> intToStr = String::valueOf;
ToIntFunction<String> length = String::length;
IntPredicate isPositive = n -> n > 0;
IntConsumer intPrinter = System.out::println;
IntSupplier randomInt = () -> (int)(Math.random() * 100);

// Binary operators
BinaryOperator<Integer> max = Integer::max;
IntBinaryOperator sum = Integer::sum;

// Unary operators
UnaryOperator<String> trim = String::trim;
IntUnaryOperator negate = n -> -n;

// Custom functional interface
@FunctionalInterface
interface StringProcessor {
    String process(String input);

    // Can have default methods
    default StringProcessor andThen(StringProcessor after) {
        return input -> after.process(process(input));
    }
}
08

Exceptions & I/O

Try / Catch / Finally

try/catch/finally gère les exceptions. finally s'exécute toujours (utilisez-le pour le nettoyage). Multi-catch (catch A | B) gère plusieurs exceptions ensemble. Try-with-resources ferme automatiquement tout AutoCloseable (fichiers, connexions, streams) — préféré au nettoyage manuel finally. Les ressources sont fermées dans l'ordre inverse de déclaration.

java
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Math error: " + e.getMessage());
} catch (Exception e) {
    System.out.println("General error: " + e);
} finally {
    // Always runs (even if return/throw in try/catch)
    System.out.println("Cleanup");
}

// Multi-catch (Java 7+)
try {
    riskyOperation();
} catch (IOException | SQLException e) {
    // Handle both exceptions the same way
    log.error(e);
}

// Try-with-resources (auto-close, Java 7+)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"));
     PrintWriter pw = new PrintWriter("output.txt")) {
    String line = br.readLine();
    pw.println(line);
} catch (IOException e) {
    e.printStackTrace();
}
// br and pw are auto-closed (in reverse order)

Checked vs Unchecked

Les exceptions checked (extends Exception) doivent être déclarées avec 'throws' ou capturées — le compilateur l'impose. Utilisez-les pour des conditions récupérables (fichier introuvable, erreur réseau). Les exceptions unchecked (extends RuntimeException) n'ont pas besoin de déclaration — utilisez-les pour les erreurs de programmation (null pointer, argument invalide). Le débat : les exceptions checked forcent la gestion mais peuvent encombrer le code ; beaucoup de frameworks préfèrent unchecked.

java
// Checked exceptions (must be declared or caught)
public void readFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    // IOException is checked — compiler enforces handling
}

// Unchecked exceptions (RuntimeException, no need to declare)
public int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("Divisor cannot be zero");
        // RuntimeException — no 'throws' needed
    }
    return a / b;
}

// Common checked exceptions:
// IOException, SQLException, ClassNotFoundException

// Common unchecked exceptions:
// NullPointerException, IllegalArgumentException,
// IndexOutOfBoundsException, ArithmeticException,
// ClassCastException, IllegalStateException

// Custom checked exception
class DataException extends Exception {
    public DataException(String msg) { super(msg); }
}

// Custom unchecked exception
class ValidationException extends RuntimeException {
    public ValidationException(String msg) { super(msg); }
}

File I/O (NIO.2)

NIO.2 (java.nio.file) est l'API fichier moderne. Files.readString/writeString (Java 11+) sont pratiques pour le texte. Files.lines() renvoie un Stream paresseux — efficace pour les gros fichiers (doit être fermé avec try-with-resources). Path.of() remplace l'ancienne classe File. Files.createDirectories() crée le chemin complet. Gérez toujours IOException.

java
import java.nio.file.*;
import java.io.*;

// Read entire file (small files)
List<String> lines = Files.readAllLines(Path.of("input.txt"));
String content = Files.readString(Path.of("config.json")); // Java 11+
byte[] bytes = Files.readAllBytes(Path.of("image.png"));

// Write file
Files.writeString(Path.of("output.txt"), "Hello, World!");
Files.write(Path.of("data.bin"), bytes);

// Append
Files.writeString(Path.of("log.txt"), "entry\n",
    StandardOpenOption.APPEND, StandardOpenOption.CREATE);

// Stream lines (large files, lazy)
try (Stream<String> lineStream = Files.lines(Path.of("large.txt"))) {
    lineStream.filter(l -> l.contains("ERROR"))
              .forEach(System.out::println);
}

// Copy, move, delete
Files.copy(Path.of("src.txt"), Path.of("dest.txt"));
Files.move(Path.of("old.txt"), Path.of("new.txt"));
Files.delete(Path.of("temp.txt"));

// Check existence
boolean exists = Files.exists(Path.of("file.txt"));

// Create directories
Files.createDirectories(Path.of("a/b/c"));

Reader & Writer (texte)

BufferedReader/Writer sont efficaces pour les I/O texte (le buffering réduit les appels système). PrintWriter offre un formatage de style printf. Scanner parse l'entrée (nextInt, nextDouble, nextLine). InputStreamReader fait le pont entre les flux d'octets et les flux de caractères (spécifiez le charset pour le non-UTF-8). Utilisez toujours try-with-resources pour garantir la fermeture des streams.

java
import java.io.*;
import java.nio.file.*;

// BufferedReader (efficient text reading)
try (BufferedReader br = Files.newBufferedReader(Path.of("input.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// BufferedWriter (efficient text writing)
try (BufferedWriter bw = Files.newBufferedWriter(Path.of("output.txt"))) {
    bw.write("First line");
    bw.newLine();
    bw.write("Second line");
}

// PrintWriter (convenient formatting)
try (PrintWriter pw = new PrintWriter("output.txt")) {
    pw.println("Hello");
    pw.printf("Name: %s, Age: %d%n", "Alice", 30);
}

// Scanner (parsing input)
try (Scanner sc = new Scanner(Path.of("data.txt"))) {
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        // Parse tokens
        Scanner lineSc = new Scanner(line);
        if (lineSc.hasNextInt()) {
            int n = lineSc.nextInt();
        }
    }
}

// InputStreamReader (bytes to chars, e.g., from InputStream)
Reader reader = new InputStreamReader(System.in);

Date & heure (java.time)

java.time (Java 8+) est l'API date/heure moderne, remplaçant l'ancien Date/Calendar. LocalDate (date seulement), LocalTime (heure seulement), LocalDateTime (les deux), ZonedDateTime (avec fuseau horaire). Tous sont immuables et thread-safe. Utilisez Period pour les différences de date, Duration pour les différences de temps. DateTimeFormatter pour parser/formater. Instant pour les timestamps machine (UTC).

java
import java.time.*;
import java.time.format.*;
import java.time.temporal.*;

// Current date/time
LocalDate today = LocalDate.now();      // 2024-01-15
LocalTime now = LocalTime.now();        // 14:30:45.123
LocalDateTime dt = LocalDateTime.now(); // both
ZonedDateTime zdt = ZonedDateTime.now(); // with timezone

// Create specific
LocalDate date = LocalDate.of(2024, 1, 15);
LocalTime time = LocalTime.of(14, 30, 0);
LocalDateTime specific = LocalDateTime.of(2024, 1, 15, 14, 30);

// Parsing and formatting
LocalDate parsed = LocalDate.parse("2024-01-15");
String formatted = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// "15/01/2024"

// Manipulation (immutable, returns new)
LocalDate tomorrow = today.plusDays(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate nextYear = today.plusYears(1);

// Period (date-based)
Period age = Period.between(LocalDate.of(1990, 1, 1), today);
System.out.println(age.getYears()); // 34

// Duration (time-based)
Duration dur = Duration.between(time, LocalTime.now());
System.out.println(dur.toMinutes());

// Instant (machine time, UTC)
Instant instant = Instant.now();
Instant epoch = Instant.ofEpochSecond(0);

Bases de la concurrence

Concurrence Java : Thread (bas niveau), ExecutorService (pools de threads — préféré), CompletableFuture (composition async, comme les Promises). parallelStream() utilise le ForkJoinPool pour le traitement parallèle. Les blocs synchronized protègent l'état partagé. Les variables atomiques (AtomicInteger, etc.) fournissent des opérations thread-safe sans verrou. Pour la concurrence complexe, utilisez les collections java.util.concurrent (ConcurrentHashMap, BlockingQueue).

java
import java.util.concurrent.*;

// Create a thread
Thread thread = new Thread(() -> {
    System.out.println("Running in: " + Thread.currentThread().getName());
});
thread.start();
thread.join(); // wait for completion

// ExecutorService (thread pool)
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(() -> {
    Thread.sleep(1000);
    return 42;
});
Integer result = future.get(); // blocks until done
executor.shutdown();

// CompletableFuture (async, Java 8+)
CompletableFuture<String> cf = CompletableFuture
    .supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")
    .thenApply(String::toUpperCase);
String asyncResult = cf.join(); // "HELLO WORLD"

// Parallel stream
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.parallelStream().mapToInt(Integer::intValue).sum();

// Synchronized
synchronized (this) {
    // only one thread at a time
}

// Atomic variables
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
09

Expressions lambda

Syntaxe de base des lambdas

Les lambdas (Java 8+) sont des implémentations concises d'interfaces fonctionnelles. Syntaxe : (params) -> expression ou (params) -> { instructions; }. Le compilateur infère les types de paramètres depuis le type cible. Les lambdas à un paramètre peuvent omettre les parenthèses ; ceux à zéro paramètre ont besoin de parenthèses vides. Les lambdas permettent la programmation fonctionnelle et sont l'épine dorsale de l'API Streams.

java
// Anonymous class (verbose, pre-Java 8)
Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Old way");
    }
};

// Lambda expression (Java 8+)
Runnable r2 = () -> System.out.println("Lambda");

// With parameters and body
Comparator<Integer> cmp = (a, b) -> {
    int diff = a - b;
    return diff;
};

// Type inference (omit types)
Comparator<Integer> cmp2 = (a, b) -> a - b;

// Single param, no parentheses needed
Consumer<String> printer = s -> System.out.println(s);

// Zero params need empty parens
Runnable noop = () -> {};

Interfaces fonctionnelles

Une interface fonctionnelle a exactement une méthode abstraite (type SAM). L'annotation @FunctionalInterface fait que le compilateur l'impose. Les lambdas ne peuvent cibler que des interfaces fonctionnelles. Les méthodes default et statiques sont autorisées et ne cassent pas la règle de méthode unique. C'est la fondation qui fait fonctionner les lambdas dans le système de types de Java.

java
// Functional interface: exactly one abstract method
@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);
}

// Implement with lambda
MathOperation add = (a, b) -> a + b;
MathOperation mul = (a, b) -> a * b;
MathOperation max = (a, b) -> Math.max(a, b);

int result = add.operate(3, 4);  // 7
int m = mul.operate(3, 4);       // 12

// @FunctionalInterface is optional but recommended
// It prevents accidentally adding a second abstract method
// Default and static methods don't count toward the limit
interface StringProcessor {
    String process(String s);
    default StringProcessor andThen(StringProcessor next) {
        return s -> next.process(this.process(s));
    }
}

Interfaces fonctionnelles intégrées

java.util.function fournit ~40 interfaces fonctionnelles prêtes à l'emploi donc vous écrivez rarement les vôtres. Les quatre principales : Function (transformer), Predicate (tester), Consumer (consommer), Supplier (produire). Les variantes Bi- prennent deux arguments. Les variantes primitives (IntFunction, ToIntFunction, etc.) évitent le surcoût de l'autoboxing. Utilisez-les au lieu de créer des interfaces personnalisées.

java
import java.util.function.*;

// Function<T,R>: input -> output
Function<String, Integer> len = String::length;
int n = len.apply("hello");  // 5

// Predicate<T>: input -> boolean (for filtering)
Predicate<String> isEmpty = String::isEmpty;
boolean e = isEmpty.test("");  // true

// Consumer<T>: input -> void (side effects)
Consumer<String> print = System.out::println;
print.accept("hi");

// Supplier<T>: no input -> output (factories, lazy)
Supplier<Double> random = Math::random;
double r = random.get();

// BiFunction<T,U,R>: two inputs -> output
BiFunction<String, Integer, String> repeat =
    (s, i) -> s.repeat(i);

// Primitives variants avoid boxing
IntFunction<String> f = i -> "n=" + i;
IntPredicate positive = i -> i > 0;
ToIntFunction<String> length = String::length;
IntBinaryOperator sum = (a, b) -> a + b;

Références de méthode

Les références de méthode (::) sont un raccourci pour les lambdas qui appellent une seule méthode. Quatre types : statique (Class::static), instance liée (obj::method), instance non liée (Class::method — le premier paramètre devient récepteur), et constructeur (Class::new). Utilisez-les quand un lambda ne fait que transférer vers une méthode — ils sont plus lisibles. Sinon, restez avec des lambdas explicites.

java
import java.util.*;

List<String> names = List.of("alice", "bob", "charlie");

// Lambda form
names.forEach(s -> System.out.println(s));
// Method reference (shorthand)
names.forEach(System.out::println);

// Four kinds of method references:

// 1. Static method: ClassName::staticMethod
names.stream().map(String::toUpperCase);

// 2. Instance method of particular object: instance::method
var printer = System.out;
names.forEach(printer::println);

// 3. Instance method of arbitrary object: ClassName::instanceMethod
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();

// 4. Constructor: ClassName::new
Supplier<ArrayList<String>> factory = ArrayList::new;
ArrayList<String> list = factory.get();

Capture de variables (effectivement final)

Les lambdas peuvent capturer des variables locales, mais elles doivent être final ou « effectivement final » (jamais réassignées). C'est parce que les lambdas peuvent survivre au cadre de pile. Pour contourner cela, utilisez un tableau à un élément ou un objet AtomicInteger/holder. Les champs d'instance et statiques n'ont pas cette restriction. 'this' à l'intérieur d'un lambda fait référence à l'instance de la classe englobante, pas au lambda lui-même.

java
import java.util.function.*;

int x = 10;
// Capturing a local variable — must be final or effectively final
Supplier<Integer> getter = () -> x * 2;
System.out.println(getter.get());  // 20

// x = 20;  // ERROR: would break the lambda capture
// Local variables captured by lambdas must be final/effectively final

// Workaround: use an array or wrapper (mutable container)
int[] counter = {0};
Runnable inc = () -> counter[0]++;
inc.run();
inc.run();
System.out.println(counter[0]);  // 2

// Instance/static fields CAN be modified (no restriction)
class Holder {
    int value = 0;
    Runnable bump = () -> value++;  // OK, field access
}

// 'this' inside a lambda refers to the enclosing instance
class Outer {
    String name = "Outer";
    Runnable r = () -> System.out.println(this.name);  // "Outer"
}

Références de constructeur

Les références de constructeur (ClassName::new) créent de nouvelles instances de manière concise. Utiles avec Collectors.toCollection() pour choisir le type de résultat, avec la création de tableau (Type[]::new), et dans les motifs de fabrique. Pour les records et objets immuables, les références de constructeur sont la manière idiomatique de construire des copies. Elles se marient naturellement avec les cibles Function/Supplier.

java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

// Supplier constructor reference
Supplier<StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.get();

// Function constructor reference (with one arg)
Function<String, StringBuilder> sbFromString = StringBuilder::new;
StringBuilder named = sbFromString.apply("Hello");

// In streams: collect into a specific collection
List<String> names = List.of("a", "b", "c");
ArrayList<String> copy = names.stream()
    .collect(Collectors.toCollection(ArrayList::new));

// Array constructor reference
IntFunction<String[]> arrayFactory = String[]::new;
String[] arr = arrayFactory.apply(5);  // new String[5]

// Copying via constructor
record Point(int x, int y) {}
Function<Point, Point> copyCtor = Point::new;
Point p = copyCtor.apply(new Point(1, 2));
10

Optional & sécurité null

Créer un Optional

Optional est un conteneur qui peut ou non contenir une valeur. Utilisez empty() pour aucune valeur, of() quand vous êtes certain que la valeur est non null (lève NPE sinon), et ofNullable() quand null est possible. Optional force les appelants à gérer explicitement le cas d'absence. Ne renvoyez jamais null là où Optional est attendu — cela va à l'encontre du but.

java
import java.util.Optional;

// empty() — no value
Optional<String> empty = Optional.empty();

// of() — value must be non-null (throws NPE if null)
Optional<String> present = Optional.of("hello");

// ofNullable() — accepts null safely
Optional<String> maybe = Optional.ofNullable(getName());

// From a stream that may produce 0 or 1 elements
Optional<Integer> first = List.of(1, 2, 3).stream().findFirst();

// Common helper pattern
public Optional<User> findUser(long id) {
    User u = db.lookup(id);
    return Optional.ofNullable(u);
}

String getName() { return Math.random() > 0.5 ? "Alice" : null; }
class User {}
class Db { User lookup(long id) { return null; } }
Db db = new Db();

Consommer des valeurs sûrement

Préférez ifPresent/ifPresentOrElse à isPresent+get. orElse renvoie une constante par défaut ; orElseGet prend un Supplier donc le défaut est calculé paresseusement (important quand le défaut est coûteux). orElseThrow convertit l'absence en exception. Le but est de ne jamais appeler .get() aveuglément — cela réintroduit le risque de NPE que Optional était censé éliminer.

java
import java.util.Optional;

Optional<String> name = Optional.of("Alice");

// isPresent / isEmpty (Java 11+)
if (name.isPresent()) {
    System.out.println(name.get());  // "Alice"
}
// Avoid .get() without checking — throws NoSuchElementException

// ifPresent: run action only if value exists
name.ifPresent(System.out::println);

// ifPresentOrElse (Java 9+)
name.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("No name")
);

// orElse: provide default
String s1 = name.orElse("Anonymous");

// orElseGet: lazy default (computed only if needed)
String s2 = name.orElseGet(() -> expensiveDefault());

// orElseThrow: throw if absent
String s3 = name.orElseThrow(() -> new IllegalStateException("missing"));

String expensiveDefault() { return "computed"; }

Transformer avec map & flatMap

map transforme la valeur contenue (Optional<T> -> Optional<R>). flatMap est utilisé quand la fonction de mapping renvoie elle-même un Optional, évitant les Optionals imbriqués. filter garde la valeur seulement si un prédicat correspond. Chaîner map/filter/flatMap vous permet de construire des pipelines qui court-circuitent au premier vide — beaucoup plus propre que les vérifications null imbriquées.

java
import java.util.Optional;

Optional<String> name = Optional.of("Alice");

// map: transform the value if present
Optional<Integer> length = name.map(String::length);  // Optional[5]
Optional<String> upper = name.map(String::toUpperCase);

// flatMap: when the transform itself returns Optional
// (avoids Optional<Optional<T>>)
public Optional<String> findEmail(long id) {
    return Optional.ofNullable(db.get(id));
}
Optional<String> email = Optional.of(1L)
    .flatMap(this::findEmail);  // Optional<email> not Optional<Optional<email>>

// filter: keep only if predicate matches
Optional<Integer> adultAge = Optional.of(25)
    .filter(a -> a >= 18);  // Optional[25]
Optional<Integer> kid = Optional.of(10)
    .filter(a -> a >= 18);  // Optional.empty

// Chain transformations
String label = Optional.of("alice")
    .map(String::strip)
    .filter(s -> !s.isEmpty())
    .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
    .orElse("unknown");  // "Alice"

class Db { String get(long id) { return "[email protected]"; } }
Db db = new Db();
Optional<String> findEmail(long id) { return Optional.ofNullable(db.get(id)); }

Anti-patterns à éviter

Optional est conçu pour les types de retour, pas les champs ou paramètres. Il n'est pas Serializable et ajoute du surcoût en tant que champ. N'utilisez pas .get() sans vérifier, et n'utilisez pas isPresent()+get() — c'est juste une vérification null verbeuse. Les collections expriment déjà le vide, donc ne les enveloppez pas dans Optional. Utilisez Optional comme un signal de type de retour qu'une valeur peut être absente.

java
import java.util.Optional;

// BAD: using Optional for fields (not serializable, wastes memory)
class Bad {
    private Optional<String> name;  // DON'T
}

// GOOD: use plain field, return Optional from accessor
class Good {
    private String name;
    public Optional<String> getName() { return Optional.ofNullable(name); }
}

// BAD: Optional as method parameter (clutters API)
public void process(Optional<String> input) {}  // DON'T

// GOOD: method overloading or nullable param
public void process(String input) {}
public void process() { process(null); }

// BAD: .get() without check
String x = findName().get();  // throws if empty

// BAD: .isPresent() + .get() — defeats the purpose
Optional<String> opt = findName();
if (opt.isPresent()) {
    use(opt.get());  // just use ifPresent or map instead
}

// BAD: returning Optional from collections
public Optional<Item> find(...) {
    // Collections already express emptiness — return empty List, not Optional<List>
    return Optional.ofNullable(items);
}

Optional avec les streams

Optional.stream() (Java 9+) produit un Stream de 0 ou 1 élément, ce qui vous permet de flatMap les Optionals hors d'un stream élégamment. C'est la manière la plus propre de sauter les valeurs absentes pendant le traitement de stream. Cela évite le motif verbeux filter(isPresent).map(get) et garde le pipeline déclaratif.

java
import java.util.*;
import java.util.stream.*;

// stream() on Optional: 0 or 1 element stream
Optional<String> opt = Optional.of("hi");
opt.stream().forEach(System.out::println);

// Useful: flatMap Optional out of a stream
class User {
    String email;  // may be null
    User(String e) { email = e; }
    Optional<String> getEmail() { return Optional.ofNullable(email); }
}

List<User> users = List.of(
    new User("[email protected]"),
    new User(null),
    new User("[email protected]")
);

// Extract emails, skipping nulls — clean with Optional::stream
List<String> emails = users.stream()
    .flatMap(u -> u.getEmail().stream())
    .toList();  // [[email protected], [email protected]]

// Without Optional::stream you'd need filter+map
List<String> emails2 = users.stream()
    .map(User::getEmail)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .toList();
11

Approfondissement de l'API Streams

Créer des streams

Les streams peuvent être créés à partir de collections, tableaux ou fabriques statiques. iterate() et generate() produisent des streams infinis — suivez toujours avec limit(). iterate avec un prédicat (Java 9+) est plus sûr qu'un iterate nu. IntStream/LongStream/DoubleStream évitent le boxing pour le travail numérique. Les streams sont à usage unique : une fois qu'une opération terminale s'exécute, le stream est consommé.

java
import java.util.*;
import java.util.stream.*;

// From collections
Stream<String> s1 = List.of("a", "b").stream();
Stream<String> s2 = Set.of("x").stream();

// From arrays
int[] nums = {1, 2, 3};
IntStream s3 = Arrays.stream(nums);
Stream<String> s4 = Arrays.stream(new String[]{"a", "b"});

// Static factory methods
Stream<Integer> s5 = Stream.of(1, 2, 3);
Stream<Integer> s6 = Stream.empty();
Stream<Integer> s7 = Stream.iterate(1, n -> n * 2);  // infinite
Stream<Integer> s8 = Stream.iterate(1, n -> n < 100, n -> n + 1);  // bounded (Java 9+)
Stream<Double> s9 = Stream.generate(Math::random);  // infinite
Stream<String> s10 = Stream.ofNullable(null);  // 0 or 1 element (Java 9+)

// From functions (infinite, must limit)
List<Integer> powers = Stream.iterate(1, n -> n * 2)
    .limit(10)
    .toList();

// Numeric ranges
IntStream range = IntStream.range(0, 5);       // 0,1,2,3,4
IntStream closed = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

Opérations intermédiaires

Les opérations intermédiaires sont paresseuses — elles ne s'exécutent pas tant qu'une opération terminale n'est pas invoquée. filter garde les éléments, map transforme 1:1, flatMap transforme 1:plusieurs. distinct/sorted/limit/skip sont stateful. takeWhile/dropWhile (Java 9+) s'arrêtent au premier élément non correspondant (contrairement à filter, qui scanne tout). Utilisez peek pour le débogage, pas pour les effets de bord en production.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5);

// filter: keep matching
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0).toList();

// map: transform
List<String> labels = nums.stream()
    .map(n -> "n=" + n).toList();

// flatMap: one-to-many
List<Integer> expanded = List.of(List.of(1, 2), List.of(3))
    .stream().flatMap(List::stream).toList();  // [1,2,3]

// distinct: remove duplicates
List<Integer> uniq = nums.stream().distinct().toList();

// sorted
List<Integer> asc = nums.stream().sorted().toList();
List<Integer> desc = nums.stream().sorted(Comparator.reverseOrder()).toList();

// peek: inspect (mainly for debugging)
nums.stream().peek(n -> System.out.println("seen " + n)).count();

// limit / skip
List<Integer> first3 = nums.stream().limit(3).toList();
List<Integer> after2 = nums.stream().skip(2).toList();

// takeWhile / dropWhile (Java 9+)
List<Integer> lt5 = nums.stream().takeWhile(n -> n < 5).toList();

Collectors : grouping & partitioning

Collectors.groupingBy est le SQL GROUP BY des streams Java. La fonction classifieur définit la clé ; un collector en aval optionnel traite chaque groupe (counting, summing, mapping, etc.). partitioningBy est un cas spécial avec un prédicat booléen (exactement deux buckets). Passez un supplier TreeMap pour des clés triées. Ils se composent puissamment — vous pouvez construire des groupements multi-niveaux.

java
import java.util.*;
import java.util.stream.*;

record Person(String name, String city, int age) {}

List<Person> people = List.of(
    new Person("Alice", "NYC", 30),
    new Person("Bob", "LA", 25),
    new Person("Carol", "NYC", 35),
    new Person("Dave", "LA", 40)
);

// groupingBy: Map<key, List<item>>
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::city));
// {NYC=[Alice,Carol], LA=[Bob,Dave]}

// groupingBy with downstream collector
Map<String, Long> countByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city, Collectors.counting()));

Map<String, Integer> sumAgeByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city,
        Collectors.summingInt(Person::age)));

Map<String, List<String>> namesByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city,
        Collectors.mapping(Person::name, Collectors.toList())));

// partitioningBy: Map<Boolean, List> (2 buckets)
Map<Boolean, List<Person>> byAge = people.stream()
    .collect(Collectors.partitioningBy(p -> p.age() >= 30));

// groupingBy with TreeMap for sorted keys
Map<String, List<Person>> sorted = people.stream()
    .collect(Collectors.groupingBy(Person::city, TreeMap::new, Collectors.toList()));

Réduction & statistiques

reduce combine tous les éléments en une seule valeur — fournissez une identité pour la sécurité avec les streams vides. summaryStatistics donne count/sum/min/max/avg en un seul passage. Collectors.joining est pratique pour construire des chaînes délimitées. teeing (Java 12+) exécute deux collectors en parallèle et fusionne leurs résultats — utile quand vous avez besoin de deux agrégats (comme min et max) en un seul passage.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(1, 2, 3, 4, 5);

// reduce: combine all into one
int sum = nums.stream().reduce(0, Integer::sum);          // 15
Optional<Integer> product = nums.stream().reduce((a, b) -> a * b);  // Optional[120]

// Built-in summary collectors
IntSummaryStatistics stats = nums.stream()
    .mapToInt(Integer::intValue)
    .summaryStatistics();
// stats.getCount(), getSum(), getMin(), getMax(), getAverage()

// Common terminal collectors
long count = nums.stream().collect(Collectors.counting());
double avg = nums.stream().collect(Collectors.averagingInt(Integer::intValue));
int max = nums.stream().collect(Collectors.maxBy(Comparator.naturalOrder())).orElse(0);

// join strings
String joined = List.of("a", "b", "c").stream()
    .collect(Collectors.joining(", ", "[", "]"));  // "[a, b, c]"

// teeing (Java 12+): two collectors, merge results
record Range(int min, int max) {}
Range range = nums.stream().collect(Collectors.teeing(
    Collectors.minBy(Comparator.naturalOrder()),
    Collectors.maxBy(Comparator.naturalOrder()),
    (mn, mx) -> new Range(mn.orElse(0), mx.orElse(0))
));

Streams numériques

IntStream/LongStream/DoubleStream sont des spécialisations primitives qui évitent le surcoût de l'autoboxing — utilisez-les pour le travail numérique. mapToInt/mapToLong/mapToDouble convertissent les streams d'objets en streams primitifs ; boxed() fait le chemin inverse. Les streams primitifs ont des opérations terminales spécialisées (sum, average, max) qui renvoient OptionalInt/Double pour gérer les streams vides. Génial pour les pipelines numériques sensibles à la performance.

java
import java.util.stream.*;
import java.util.*;

// IntStream / LongStream / DoubleStream avoid boxing
IntStream range = IntStream.rangeClosed(1, 100);
int sum = range.sum();                       // 5050
double avg = IntStream.of(1, 2, 3).average().orElse(0);

// mapToInt / mapToLong / mapToDouble from object stream
int totalAge = people().stream().mapToInt(Person::age).sum();

// boxed: convert primitive stream back to object stream
List<Integer> list = IntStream.range(0, 5).boxed().toList();

// mapToObj: primitive -> objects
List<String> labels = IntStream.range(1, 4)
    .mapToObj(i -> "item-" + i).toList();

// asLongStream / asDoubleStream
LongStream longs = IntStream.range(0, 5).asLongStream();

// Common numeric operations
int max = IntStream.of(3, 1, 4, 1, 5).max().orElse(Integer.MIN_VALUE);
boolean anyEven = IntStream.of(1, 3, 5).anyMatch(n -> n % 2 == 0);

// Iterate to build numeric sequences
List<Integer> fib = Stream.iterate(new int[]{0, 1}, a -> new int[]{a[1], a[0] + a[1]})
    .limit(10).mapToInt(a -> a[0]).boxed().toList();

List<Person> people() { return List.of(new Person("a", "c", 30)); }
record Person(String name, String city, int age) {}

Streams parallèles

parallelStream répartit le travail sur le ForkJoinPool commun (dimensionné selon les cœurs CPU). Utilisez-le seulement pour de gros volumes de données avec des opérations CPU-intensives, sans état et indépendantes de l'ordre — pour les petites données le surcoût dépasse le bénéfice. Évitez l'état mutable partagé (cause des courses). Les I/O dans les streams parallèles bloquent le pool partagé — utilisez un ForkJoinPool personnalisé pour le travail bloquant. Mesurez avant de supposer que le parallèle est plus rapide.

java
import java.util.*;
import java.util.stream.*;

// parallelStream: uses common ForkJoinPool
long sum = List.of(1, 2, 3, 4, 5).parallelStream()
    .mapToInt(Integer::intValue).sum();

// Convert sequential to parallel
long count = IntStream.range(0, 1_000_000).parallel()
    .filter(n -> n % 2 == 0).count();

// Order may differ — use forEachOrdered if order matters
List.of(1, 2, 3, 4).parallelStream()
    .forEachOrdered(System.out::println);

// Collecting preserves encounter order (but work is parallel)
List<Integer> doubled = IntStream.range(0, 1000).parallel()
    .map(n -> n * 2).boxed().toList();

// Custom thread pool (avoid blocking the common pool)
import java.util.concurrent.ForkJoinPool;
ForkJoinPool pool = new ForkJoinPool(8);
int result = pool.submit(() ->
    IntStream.range(0, 1000).parallel().sum()
).get();

// WHEN to use parallel: large dataset, CPU-heavy per element,
// order-independent, stateless operations
// WHEN NOT: small data, I/O-bound, shared mutable state, ordered ops
12

Approfondissement des génériques

Classes & méthodes génériques

Les génériques permettent du code réutilisable type-safe. Les classes déclarent des paramètres de type (<T>) ; les méthodes aussi (<T> avant le type de retour). L'opérateur diamant <> infère le type à la construction. Les génériques sont vérifiés à la compilation — ils rendent les collections et API plus sûrs en capturant les erreurs de type tôt au lieu de à l'exécution via ClassCastException.

java
// Generic class
public class Box<T> {
    private T value;
    public void set(T v) { value = v; }
    public T get() { return value; }
}

Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get();  // no cast needed

// Multiple type parameters
public class Pair<K, V> {
    private final K key;
    private final V value;
    public Pair(K k, V v) { key = k; value = v; }
    public K key() { return key; }
    public V value() { return value; }
}

Pair<String, Integer> p = new Pair<>("age", 30);

// Generic method (independent of class type params)
public static <T> T first(List<T> list) {
    return list.get(0);
}

// Generic method with multiple type params
public static <K, V> Map<K, V> zip(List<K> keys, List<V> values) {
    Map<K, V> m = new HashMap<>();
    for (int i = 0; i < keys.size(); i++) m.put(keys.get(i), values.get(i));
    return m;
}

Paramètres de type bornés

Les paramètres de type bornés (<T extends Bound>) restreignent les types utilisables et vous permettent d'appeler les méthodes de la borne. <T extends Number> signifie que T doit être un Number ou un sous-type. Les bornes multiples utilisent & — au plus une classe (doit être première), le reste des interfaces. Les bornes sont essentielles pour écrire des algorithmes nécessitant des capacités spécifiques (comparabilité, opérations numériques).

java
// Upper bound: T must be a subtype of Number
public static <T extends Number> double sum(List<T> nums) {
    double total = 0;
    for (Number n : nums) total += n.doubleValue();
    return total;
}

sum(List.of(1, 2, 3));        // Integer is a Number
sum(List.of(1.0, 2.5));       // Double is a Number
// sum(List.of("a"));          // compile error

// Multiple bounds: T must extend all (first is class, rest interfaces)
interface Comparable<T> { int compareTo(T o); }
interface Serializable {}

public static <T extends Number & Comparable<T> & Serializable>
    T max(List<T> list) {
    T best = list.get(0);
    for (T t : list) if (t.compareTo(best) > 0) best = t;
    return best;
}

// Bound lets you call methods of the bound
public static <T extends CharSequence> int totalLength(List<T> items) {
    int len = 0;
    for (CharSequence c : items) len += c.length();  // can call .length()
    return len;
}

Wildcards : ?, extends, super

Les wildcards rendent les types génériques flexibles. ? extends T (covariant) vous permet de lire T mais pas écrire — utilisez pour les producteurs. ? super T (contravariant) vous permet d'écrire T mais seulement lire Object — utilisez pour les consommateurs. La règle PECS (Producer Extends, Consumer Super) guide lequel utiliser. copy(dest, src) est l'exemple classique : dest est un consommateur (super), src est un producteur (extends).

java
import java.util.*;

// ? (unbounded wildcard) — any type
void printAll(List<?> list) {
    for (Object o : list) System.out.println(o);
}

// ? extends T (upper-bounded / covariant) — producer (PECS: Producer Extends)
double sum(List<? extends Number> nums) {
    double total = 0;
    for (Number n : nums) total += n.doubleValue();
    return total;
}
sum(List.of(1, 2, 3));     // List<Integer> OK
sum(List.of(1.0, 2.0));    // List<Double> OK
// nums.add(5);  // ERROR: can't add (don't know exact type)

// ? super T (lower-bounded / contravariant) — consumer (PECS: Consumer Super)
void addNumbers(List<? super Integer> list) {
    list.add(1); list.add(2); list.add(3);  // OK to add Integer
}
addNumbers(new ArrayList<Number>());  // OK
addNumbers(new ArrayList<Object>());  // OK
// Number n = list.get(0);  // only safe to read as Object

// PECS rule: Producer Extends, Consumer Super
// If you read from a collection, use ? extends T
// If you write to a collection, use ? super T
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) dest.add(t);
}

Effacement de type

Les génériques Java utilisent l'effacement de type — les types génériques n'existent qu'à la compilation ; à l'exécution, List<String> et List<Integer> sont tous les deux juste List. Cela permet la compatibilité ascendante mais a des limites : vous ne pouvez pas faire new T(), créer des tableaux génériques, utiliser instanceof avec des génériques, ou avoir des méthodes surchargées avec des signatures effacées identiques. La pollution du tas se produit quand des casts non vérifiés mettent de mauvais types dans les génériques, reportant les erreurs à l'exécution.

java
import java.util.*;

// At runtime, generic types are erased to their bounds (or Object)
// List<String>, List<Integer>, List<?> all become List at runtime

List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// Runtime: both are just ArrayList

// You CANNOT do these due to erasure:
// new T()              — can't instantiate type param
// new T[]              — can't create generic array
// instanceof List<String>  — only instanceof List (raw)
// class MyException<T> extends Exception  — can't extend Throwable generically
// static T field       — no static generic fields

// Erasure means overloads clash:
// void process(List<String> list) {}
// void process(List<Integer> list) {}  // ERROR: same erasure

// Checking types at runtime requires Class<T>
public static <T> List<T> filter(List<?> items, Class<T> type) {
    List<T> result = new ArrayList<>();
    for (Object o : items) {
        if (type.isInstance(o)) result.add(type.cast(o));
    }
    return result;
}

// Heap pollution: when unchecked warnings lead to runtime ClassCastException
List<String> polluted = (List<String>)(List) List.of(1, 2);  // unchecked
// String s = polluted.get(0);  // ClassCastException at runtime

Méthodes génériques & inférence

L'inférence de type laisse le compilateur déterminer les arguments de type depuis le contexte (arguments et type cible), donc vous les écrivez rarement explicitement. L'opérateur diamant <> est l'inférence pour les constructeurs. Le typage par cible utilise le type attendu de la variable. Utilisez un témoin de type explicite (Class.<T>method()) seulement quand l'inférence ne peut pas résoudre l'ambiguïté. L'inférence fait que le code générique se lit aussi proprement que le code non générique.

java
import java.util.*;

// Type inference: compiler figures out T from arguments
public static <T> T pick(T a, T b) { return Math.random() > 0.5 ? a : b; }
String s = pick("hello", "world");      // T inferred as String
Number n = pick(1, 2.0);                // T inferred as Number (common supertype)

// Target typing: inference uses the expected type
List<String> list = Collections.emptyList();  // T inferred from target

// Inference with method chains
List<Integer> nums = List.of(1, 2, 3);
String joined = nums.stream()
    .map(Object::toString)              // Stream<String>
    .collect(Collectors.joining(","));  // inferred

// Explicit type witness (rarely needed)
Collections.<String>emptyList();

// Generic constructor
class Holder<T> {
    private T value;
    <U extends T> Holder(U init) { value = init; }  // constructor type param
    T get() { return value; }
}
Holder<Number> h = new Holder<>(42);  // U=Integer, T=Number

import java.util.stream.Collectors;
class Math { static double random() { return 0.5; } }

Interfaces génériques & motifs

Les interfaces génériques (comme Repository<T,ID>) définissent des contrats réutilisables. Le motif de borne auto-référentielle (class X implements Comparable<X>) garantit que compareTo n'accepte que le même type. Le motif type token (utiliser Class<T> comme clé) contourne l'effacement pour fournir la sécurité de type à l'exécution dans les conteneurs hétérogènes. Ces motifs sont l'épine dorsale de frameworks comme Spring Data.

java
// Generic interface
interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    void save(T entity);
}

// Implement with concrete types
class UserRepository implements Repository<User, Long> {
    public Optional<User> findById(Long id) { /* ... */ return Optional.empty(); }
    public List<User> findAll() { return List.of(); }
    public void save(User entity) {}
}

// Generic interface with self-referencing bound (Comparable pattern)
interface Comparable<T> {
    int compareTo(T other);
}
class Temperature implements Comparable<Temperature> {
    private final double celsius;
    Temperature(double c) { celsius = c; }
    public int compareTo(Temperature other) {
        return Double.compare(celsius, other.celsius);
    }
}

// Generic builder pattern
class Builder<T> {
    private T value;
    public Builder<T> set(T v) { value = v; return this; }
    public T build() { return value; }
}

// Type token pattern for runtime type safety
class TypeSafeMap {
    private final Map<Class<?>, Object> map = new HashMap<>();
    public <T> void put(Class<T> type, T value) { map.put(type, value); }
    public <T> T get(Class<T> type) { return type.cast(map.get(type)); }
}

record User(String name) {}
import java.util.Optional;
13

Annotations

Annotations intégrées

Annotations intégrées de Java : @Override (détecte les coquilles dans la redéfinition — utilisez-le toujours), @Deprecated (signale que l'API ne devrait pas être utilisée, avec métadonnées since/forRemoval), @SuppressWarnings (supprime des avertissements spécifiques — utilisez avec parcimonie), @FunctionalInterface (impose la règle SAM). Ce sont les annotations quotidiennes qui améliorent la sécurité à la compilation et la documentation.

java
import java.util.*;

// @Override: declares intent to override (compiler checks)
class Animal {
    public void sound() { System.out.println("..."); }
}
class Dog extends Animal {
    @Override
    public void sound() { System.out.println("Woof"); }
}

// @Deprecated: marks API as outdated
class OldApi {
    @Deprecated(since = "1.5", forRemoval = true)
    public void legacy() {}
}

// @SuppressWarnings: silence compiler warnings
@SuppressWarnings("unchecked")
List<String> list = (List<String>) new ArrayList();

// @FunctionalInterface: enforces single abstract method
@FunctionalInterface
interface Op { int apply(int a, int b); }

// Common warning keys: unchecked, deprecation, rawtypes, null

// Java 17+ sealed-related
@Deprecated
class ToRemove {}

Annotations personnalisées

Les annotations personnalisées sont déclarées avec @interface. Les membres ressemblent à des méthodes mais sont des attributs d'annotation — ils peuvent avoir des valeurs par défaut. Utilisez @Target pour restreindre où elle s'applique (TYPE, METHOD, FIELD, etc.) et @Retention pour contrôler la disponibilité. Les annotations marqueur (sans membres) étiquettent simplement des éléments. Les annotations elles-mêmes ne portent aucun comportement — les processeurs (réflexion, outils d'annotation) les lisent et agissent.

java
import java.lang.annotation.*;

// Define an annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
    String value() default "";
    long timeout() default 0L;
}

// Use it
class MyTests {
    @Test
    public void quickCheck() {}

    @Test(timeout = 5000)
    public void slowCheck() {}

    @Test("custom-name")
    public void named() {}
}

// Annotation with default values
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Entity {
    String table() default "";
    String[] columns() default {};
}

@Entity(table = "users", columns = {"id", "name"})
class User {}

// Marker annotation (no members)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {}

Retention & Target

@Retention contrôle combien de temps une annotation survit : SOURCE (compilation seulement, comme @Override), CLASS (dans le bytecode mais pas visible à l'exécution — la valeur par défaut), RUNTIME (accessible via réflexion). @Target restreint où une annotation peut apparaître. Java 8+ a ajouté TYPE_USE et TYPE_PARAMETER, vous permettant d'annoter les génériques et casts (List<@NonNull String>). Choisissez RUNTIME seulement si vous avez besoin d'accès par réflexion.

java
import java.lang.annotation.*;

// RetentionPolicy.SOURCE: discarded by compiler (e.g., @Override)
@Retention(RetentionPolicy.SOURCE)
@interface CompileOnly {}

// RetentionPolicy.CLASS: kept in .class but not loaded (default)
@Retention(RetentionPolicy.CLASS)
@interface BytecodeOnly {}

// RetentionPolicy.RUNTIME: available via reflection at runtime
@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeVisible {}

// ElementType targets
@Target(ElementType.TYPE)         // classes, interfaces, enums
@interface ForType {}

@Target(ElementType.METHOD)
@interface ForMethod {}

@Target(ElementType.FIELD)
@interface ForField {}

@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@interface ForTypeUse {}

// Java 8+ type-use annotations (annotate any type occurrence)
@RuntimeVisible String[] names;  // example usage
List<@RuntimeVisible String> typed;

Lire les annotations via la réflexion

Les annotations avec rétention RUNTIME peuvent être lues via réflexion : isAnnotationPresent() vérifie l'existence, getAnnotation() la récupère. C'est ainsi que les frameworks (Spring, JUnit, JAX-RS) câblent le comportement de manière déclarative — vous étiquettez des méthodes/classes, le framework scanne et dispatche. Le traitement des annotations à la compilation (annotation processors) est une alternative pour la génération de code sans le coût de la réflexion à l'exécution.

java
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Route {
    String path();
    String method() default "GET";
}

class Api {
    @Route(path = "/users", method = "GET")
    public void listUsers() {}

    @Route(path = "/users", method = "POST")
    public void createUser() {}
}

// Scan methods for @Route at runtime
for (Method m : Api.class.getDeclaredMethods()) {
    if (m.isAnnotationPresent(Route.class)) {
        Route r = m.getAnnotation(Route.class);
        System.out.println(r.method() + " " + r.path() + " -> " + m.getName());
    }
}
// Output:
// GET /users -> listUsers
// POST /users -> createUser

// Reading annotations on a class
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Table { String name(); }

@Table(name = "orders")
class Order {}
Table t = Order.class.getAnnotation(Table.class);
System.out.println(t.name());  // "orders"

Repeatable & méta-annotations

@Repeatable (Java 8+) vous permet d'appliquer la même annotation plusieurs fois en définissant une annotation conteneur. @Inherited fait que les annotations se propagent aux sous-classes (seulement pour les annotations de niveau classe). @Documented inclut l'annotation dans le Javadoc. @Target avec ANNOTATION_TYPE crée des méta-annotations (annotations qui annotent d'autres annotations) — c'est ainsi que Spring construit des stéréotypes d'annotation composables comme @RestController = @Controller + @ResponseBody.

java
import java.lang.annotation.*;

// Repeatable: allow same annotation multiple times
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule {
    String cron();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules {
    Schedule[] value();  // container annotation
}

// Make Schedule repeatable
@Repeatable(Schedules.class)
@interface Schedule2 {
    String cron();
}

// Now you can repeat it (Java 8+)
class Job {
    @Schedule2(cron = "0 0 * * *")
    @Schedule2(cron = "0 30 * * *")
    public void run() {}
}

// Meta-annotations: annotations on annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)  // can only annotate other annotations
@interface TestCategory {}

@TestCategory
@Retention(RetentionPolicy.RUNTIME)
@interface UnitTest {}

// Inherited: subclass inherits the annotation
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Persistent {}
@Persistent class Base {}
class Child extends Base {}  // Child also has @Persistent

// Documented: appears in Javadoc
@Documented
@interface PublicApi {}
14

Réflexion

Objet Class & obtenir les Classes

Chaque type chargé a un objet Class unique — le point d'entrée de la réflexion. Obtenez-le via le littéral de classe (Type.class), instance.getClass(), ou Class.forName() (chargement dynamique, lève ClassNotFoundException). L'objet Class expose le nom, les modificateurs, la superclasse, les interfaces et les vérifications de type (isInterface, isArray, isEnum, isRecord). isAssignableFrom vérifie les relations polymorphiques.

java
import java.lang.reflect.*;

// Three ways to get a Class object
Class<String> c1 = String.class;                  // class literal
Class<?> c2 = "hello".getClass();                 // from instance
Class<?> c3 = Class.forName("java.lang.String");  // by name (throws checked)

// Basic introspection
Class<?> c = String.class;
System.out.println(c.getName());          // "java.lang.String"
System.out.println(c.getSimpleName());    // "String"
System.out.println(c.getPackage());       // "package java.lang"
System.out.println(c.getSuperclass());    // "class java.lang.Object"
System.out.println(Modifier.toString(c.getModifiers()));  // "public final"

// Check type relationships
System.out.println(c.isInterface());      // false
System.out.println(c.isArray());          // false
System.out.println(c.isEnum());           // false
System.out.println(c.isRecord());         // false (Java 16+)
System.out.println(CharSequence.class.isAssignableFrom(c));  // true

// Primitive class objects
Class<?> intClass = int.class;
Class<?> intArrayClass = int[].class;
System.out.println(intClass.isPrimitive());  // true

Inspecter les champs, méthodes, constructeurs

getDeclaredFields/Methods/Constructors renvoient TOUS les membres (y compris privés) déclarés dans cette classe uniquement. getFields/getMethods renvoient seulement les membres publics mais incluent les hérités. Pour trouver un membre spécifique, utilisez getDeclaredField(name) ou getDeclaredMethod(name, paramTypes...) — les types de paramètres sont nécessaires pour lever l'ambiguïté des surcharges. La réflexion contourne le contrôle d'accès sauf si vous appelez setAccessible(true).

java
import java.lang.reflect.*;
import java.util.*;

class Sample {
    public String name;
    private int count;
    public Sample() {}
    public Sample(String n) { name = n; }
    private void secret() {}
    public int compute(int x) { return x * 2; }
}

Class<?> c = Sample.class;

// Fields: getDeclaredFields includes private; getFields only public
for (Field f : c.getDeclaredFields()) {
    System.out.println(f.getName() + " : " + f.getType().getSimpleName()
        + " (" + Modifier.toString(f.getModifiers()) + ")");
}

// Methods
for (Method m : c.getDeclaredMethods()) {
    System.out.println(m.getName()
        + " params=" + Arrays.toString(m.getParameterTypes())
        + " returns=" + m.getReturnType().getSimpleName());
}

// Constructors
for (Constructor<?> ctor : c.getConstructors()) {
    System.out.println("ctor params=" + Arrays.toString(ctor.getParameterTypes()));
}

// Lookup specific member
Field nameField = c.getDeclaredField("name");
Method compute = c.getDeclaredMethod("compute", int.class);
Constructor<?> ctor = c.getDeclaredConstructor(String.class);

Invoquer des méthodes & créer des instances

Method.invoke(obj, args...) appelle une méthode par réflexion — renvoie toujours Object, donc castez le résultat. setAccessible(true) contourne les vérifications d'accès Java (les membres privés deviennent accessibles ; peut nécessiter --add-opens sur les modules). Constructor.newInstance() crée des objets — l'équivalent réflexif de new. Array.newInstance crée des tableaux d'un type de composant connu à l'exécution. La réflexion est plus lente que les appels directs et contourne la sécurité à la compilation.

java
import java.lang.reflect.*;

class Greeter {
    public String greet(String name) { return "Hello, " + name; }
    private String secret() { return "hidden"; }
}

Object obj = new Greeter();
Class<?> c = obj.getClass();

// Invoke public method
Method greet = c.getMethod("greet", String.class);
String result = (String) greet.invoke(obj, "Alice");  // "Hello, Alice"

// Invoke private method
Method sec = c.getDeclaredMethod("secret");
sec.setAccessible(true);  // bypass access check
String s = (String) sec.invoke(obj);  // "hidden"

// Create instances via constructor
Constructor<?> noArg = c.getConstructor();
Object o1 = noArg.newInstance();

// With args
class Person {
    String name;
    public Person(String n) { name = n; }
    public String toString() { return "Person(" + name + ")"; }
}
Constructor<?> ctor = Person.class.getConstructor(String.class);
Object p = ctor.newInstance("Bob");
System.out.println(p);  // "Person(Bob)"

// Array creation via reflection
Object strArray = Array.newInstance(String.class, 5);
Array.set(strArray, 0, "first");
String v = (String) Array.get(strArray, 0);

Lire & modifier les champs

Field.get(instance) lit la valeur d'un champ ; Field.set(instance, value) l'écrit. Pour les primitifs, utilisez les accesseurs spécifiques au type (getInt/setInt) pour éviter le boxing. Les champs statiques prennent null comme argument d'instance. setAccessible(true) est requis pour les champs privés. L'accès aux champs par réflexion est comment les bibliothèques de sérialisation (Jackson, Gson) et les frameworks ORM lisent/écrivent l'état des objets de manière générique.

java
import java.lang.reflect.*;

class Config {
    public String env = "dev";
    private int retries = 3;
    public static String VERSION = "1.0";
}

Config cfg = new Config();
Class<?> c = cfg.getClass();

// Read public field
Field env = c.getField("env");
String e = (String) env.get(cfg);  // "dev"

// Read private field
Field retries = c.getDeclaredField("retries");
retries.setAccessible(true);
int r = retries.getInt(cfg);  // 3
// For objects: Object val = field.get(instance);

// Modify fields
env.set(cfg, "prod");
retries.setInt(cfg, 5);
System.out.println(cfg.env);     // "prod"

// Static fields: pass null as the instance
Field version = c.getField("VERSION");
String v = (String) version.get(null);  // "1.0"
version.set(null, "2.0");

// Type-specific getters/setters avoid boxing
// getInt/setInt, getLong/setLong, getBoolean/setBoolean, etc.
// For reference types, use get()/set()

Proxies dynamiques & cas d'usage

java.lang.reflect.Proxy crée des proxies dynamiques qui implémentent des interfaces à l'exécution — un InvocationHandler intercepte chaque appel. C'est ainsi que fonctionnent Spring AOP, le lazy loading d'Hibernate et les mocks de Mockito. La réflexion alimente la plupart des frameworks Java (DI, ORM, sérialisation, tests) mais a des coûts : plus lent que les appels directs, sécurité de type plus faible, et restrictions du système de modules. Utilisez-la quand vous avez besoin de flexibilité à l'exécution, pas pour le code ordinaire.

java
import java.lang.reflect.*;
import java.util.*;

// JDK dynamic proxy: implements interfaces at runtime
interface UserService {
    String getUser(long id);
    void deleteUser(long id);
}

// InvocationHandler intercepts every method call
class LoggingHandler implements InvocationHandler {
    private final Object target;
    LoggingHandler(Object t) { target = t; }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Calling " + method.getName() + "(" + Arrays.toString(args) + ")");
        long start = System.nanoTime();
        Object result = method.invoke(target, args);
        System.out.println("  -> " + result + " (" + (System.nanoTime() - start) + "ns)");
        return result;
    }
}

UserService real = id -> "user-" + id;
UserService proxied = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class<?>[]{UserService.class},
    new LoggingHandler(real)
);
proxied.getUser(42);  // logs the call and result

// Common reflection use cases:
// - Frameworks: Spring DI, JPA entities, Jackson serialization
// - AOP/proxies: transactions, logging, security
// - Annotation processing at runtime
// - Test frameworks: JUnit discovers @Test methods
// - Code generation tools

// Caution: reflection is slower, bypasses compile-time checks,
// and can break under modules (--add-opens). Prefer alternatives when possible.
15

JDBC & accès aux bases de données

Connexion & DriverManager

DriverManager.getConnection() ouvre une connexion de base de données — enveloppez-la toujours dans try-with-resources pour éviter les fuites. Depuis JDBC 4, les drivers s'auto-enregistrent via ServiceLoader, donc Class.forName() est rarement nécessaire. Le format d'URL varie selon le fournisseur. Utilisez Properties pour les options de connexion (SSL, timeouts). En production, préférez un pool de connexions (HikariCP) aux appels directs à DriverManager.

java
import java.sql.*;

// Basic connection (try-with-resources auto-closes)
String url = "jdbc:postgresql://localhost:5432/mydb";
try (Connection conn = DriverManager.getConnection(url, "user", "pass")) {
    System.out.println("Connected: " + conn.getSchema());
    // ... use connection
}

// Modern: no need for Class.forName() with JDBC 4+ (auto-discovery)
// Legacy: Class.forName("org.postgresql.Driver");

// Common URL patterns:
// jdbc:postgresql://host:5432/db
// jdbc:mysql://host:3306/db
// jdbc:oracle:thin:@host:1521:db
// jdbc:sqlite:/path/to/db.sqlite
// jdbc:h2:mem:test  (in-memory H2)

// Connection properties
import java.util.Properties;
Properties props = new Properties();
props.setProperty("user", "user");
props.setProperty("password", "pass");
props.setProperty("ssl", "true");
try (Connection c = DriverManager.getConnection(url, props)) {
    // ...
}

Statement vs PreparedStatement

Utilisez toujours PreparedStatement plutôt que Statement pour toute requête avec paramètres — il prévient l'injection SQL en séparant la structure SQL des données. Les paramètres sont définis par index basé sur 1 avec des setters spécifiques au type. Les PreparedStatements peuvent être réutilisés (re-paramétrer et exécuter à nouveau) et supportent le batching (addBatch/executeBatch) pour les opérations en masse. Statement convient seulement pour le SQL statique et de confiance comme le DDL.

java
import java.sql.*;

// Statement: plain SQL (vulnerable to injection — avoid for user input)
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
     Statement st = c.createStatement()) {

    st.execute("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))");
    st.execute("INSERT INTO users VALUES (1, 'Alice')");

    // NEVER do this with user input — SQL injection!
    // st.execute("SELECT * FROM users WHERE name = '" + userInput + "'");
}

// PreparedStatement: parameterized, safe from injection, can be reused
String sql = "INSERT INTO users (id, name) VALUES (?, ?)";
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
     PreparedStatement ps = c.prepareStatement(sql)) {

    // Set parameters by index (1-based!)
    ps.setInt(1, 1);
    ps.setString(2, "Alice");
    ps.executeUpdate();

    // Reuse with different params
    ps.setInt(1, 2);
    ps.setString(2, "Bob");
    ps.executeUpdate();

    // Batch inserts
    for (int i = 3; i <= 100; i++) {
        ps.setInt(1, i);
        ps.setString(2, "user" + i);
        ps.addBatch();
    }
    ps.executeBatch();
}

ResultSet & requêtage

ResultSet est un curseur sur les lignes de requête — appelez next() pour avancer (renvoie false à la fin). Lisez les colonnes par nom (lisible) ou par index basé sur 1. wasNull() distingue le SQL NULL d'un défaut primitif (par exemple, getInt renvoie 0 pour NULL). Les ResultSets par défaut sont forward-only ; TYPE_SCROLL_INSENSITIVE + CONCUR_UPDATABLE permet l'accès aléatoire et les mises à jour sur place, bien que rarement utilisé dans les applications modernes.

java
import java.sql.*;
import java.util.*;

record User(int id, String name, String email) {}

// Execute query and map rows
String sql = "SELECT id, name, email FROM users WHERE active = ?";
List<User> users = new ArrayList<>();
try (Connection c = getConnection();
     PreparedStatement ps = c.prepareStatement(sql)) {
    ps.setBoolean(1, true);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {  // advance to next row
            users.add(new User(
                rs.getInt("id"),       // by column name
                rs.getString("name"),
                rs.getString("email")  // by column name (preferred)
            ));
        }
    }
}

// Column access by index (1-based) or name (more readable)
// rs.getInt(1), rs.getString(2), rs.getBoolean("active")

// Handle NULLs
String nick = rs.getString("nickname");
if (rs.wasNull()) nick = "anonymous";  // distinguish NULL from real null

// Scrollable/updatable ResultSet (needs specific flags)
Statement st = c.createStatement(
    ResultSet.TYPE_SCROLL_INSENSITIVE,
    ResultSet.CONCUR_UPDATABLE
);
ResultSet rs = st.executeQuery("SELECT * FROM users");
rs.absolute(5);  // jump to row 5
rs.updateString("name", "newname");
rs.updateRow();  // persist change

Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}

Transactions & batch

JDBC auto-commit chaque instruction par défaut — set autoCommit(false) pour regrouper des instructions dans une transaction. Committez en cas de succès, rollback en cas d'échec. Les savepoints permettent un rollback partiel dans une transaction. Les niveaux d'isolation contrôlent la visibilité des changements concurrents (READ_COMMITTED est le défaut courant ; SERIALIZABLE est le plus sûr mais le plus lent). Restaurez toujours autoCommit ou fermez la connexion pour éviter de fuiter l'état de transaction.

java
import java.sql.*;

// Transactions: disabled by default (auto-commit = true)
try (Connection c = getConnection()) {
    c.setAutoCommit(false);  // start transaction
    try (PreparedStatement ps = c.prepareStatement(
            "UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
        ps.setInt(1, 100); ps.setInt(2, 1); ps.executeUpdate();  // debit
        ps.setInt(1, -100); ps.setInt(2, 2); ps.executeUpdate();  // credit
    }
    c.commit();  // commit both

    // If any step fails, rollback
} catch (SQLException e) {
    // connection auto-closed; rollback happens implicitly on close if not committed
}

// Explicit rollback pattern
try (Connection c = getConnection()) {
    c.setAutoCommit(false);
    try {
        // ... multiple statements
        c.commit();
    } catch (SQLException e) {
        c.rollback();  // undo all changes in this transaction
        throw e;
    }
}

// Savepoints: partial rollback
Statement st = c.createStatement();
st.execute("INSERT INTO log VALUES (1)");
Savepoint sp = c.setSavepoint("before-risky");
st.execute("INSERT INTO log VALUES (2)");
c.rollback(sp);  // undo only after savepoint
c.commit();

// Transaction isolation levels
c.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// Levels: NONE, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE

Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}

Pooling de connexions (HikariCP)

Les pools de connexions (HikariCP est le standard de facto) gardent les connexions chaudes et les réutilisent, évitant le coût de 10-100ms d'ouverture d'une nouvelle connexion TCP+auth par requête. Configurez la taille max du pool (limitée par la capacité DB), les timeouts et la durée de vie. Empruntez avec getConnection(), rendez en fermant (elle retourne au pool, pas fermée). Fermez toujours le DataSource à l'arrêt. Dans Spring Boot, HikariCP est auto-configuré.

java
import com.zaxxer.hikari.*;
import java.sql.*;
import java.util.*;

// HikariCP: high-performance JDBC connection pool
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30_000);   // ms to wait for a connection
config.setIdleTimeout(600_000);        // ms before idle connections close
config.setMaxLifetime(1_800_000);      // ms max connection lifetime
config.setPoolName("app-pool");

HikariDataSource ds = new HikariDataSource(config);

// Borrow a connection, use it, return it (auto via try-with-resources)
try (Connection c = ds.getConnection();
     PreparedStatement ps = c.prepareStatement("SELECT * FROM users")) {
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) System.out.println(rs.getString("name"));
    }
}  // connection returns to pool here

// Shutdown pool when app stops
ds.close();

// Why pool? Opening a TCP+auth connection is expensive (~10-100ms).
// A pool keeps connections warm and reuses them across requests,
// dramatically reducing latency under load.

// Alternative pools: Apache DBCP, c3p0, Tomcat JDBC, Agroal
16

Approfondissement I/O & NIO

InputStream & OutputStream (octets)

InputStream/OutputStream gèrent les octets bruts. Enveloppez toujours dans des variantes Buffered* — les I/O non bufferisés font un appel système par octet, ce qui est catastrophique pour la performance. read() renvoie -1 à la fin du flux. transferTo() (Java 9+) fait une copie en masse efficace. readAllBytes() est pratique mais charge tout le flux en mémoire — seulement pour les petits fichiers. Fermez toujours les streams (try-with-resources).

java
import java.io.*;

// Read bytes from a file
try (InputStream in = new FileInputStream("input.bin")) {
    int b;
    while ((b = in.read()) != -1) {  // -1 = end of stream
        // process byte
    }
}

// Buffered for performance (8KB default buffer)
try (InputStream in = new BufferedInputStream(new FileInputStream("big.bin"))) {
    byte[] buffer = new byte[8192];
    int read;
    while ((read = in.read(buffer)) != -1) {
        // process buffer[0..read]
    }
}

// readAllBytes (small files only — loads everything into memory)
byte[] all = new FileInputStream("small.bin").readAllBytes();

// Write bytes
try (OutputStream out = new BufferedOutputStream(new FileOutputStream("out.bin"))) {
    out.write(65);  // single byte
    out.write(new byte[]{66, 67, 68});
    out.flush();  // force buffered data to disk
}

// Copy streams (Java 9+)
try (InputStream in = new FileInputStream("src.bin");
     OutputStream out = new FileOutputStream("dst.bin")) {
    in.transferTo(out);  // efficient bulk copy
}

// Standard streams
System.in.read();   // stdin (InputStream)
System.out.write(65);  // stdout (PrintStream)

Canaux & tampons (NIO)

Les canaux NIO + ByteBuffers sont l'alternative haute performance aux streams. Les tampons ont position/limit/capacity ; flip() passe du mode écriture au mode lecture, clear() réinitialise pour l'écriture, compact() préserve les données non lues. Les tampons directs (allocateDirect) vivent en dehors du tas JVM et évitent une étape de copie pour les gros I/O. transferTo entre canaux peut utiliser le zero-copy sur les OS supportés. Utilisez NIO quand la performance de streaming compte.

java
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.*;

// Channel: high-performance, block-oriented I/O
try (FileChannel ch = FileChannel.open(Path.of("data.bin"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {

    // ByteBuffer: fixed-capacity block of bytes
    ByteBuffer buf = ByteBuffer.allocate(1024);
    int read = ch.read(buf);  // fill buffer from channel
    buf.flip();  // switch from write mode to read mode

    while (buf.hasRemaining()) {
        byte b = buf.get();
    }
    buf.clear();  // reset for next read (or compact() to preserve unread)
}

// Direct buffer: outside JVM heap, faster for large I/O (no copy)
ByteBuffer direct = ByteBuffer.allocateDirect(64 * 1024);

// Scatter/gather: read into multiple buffers / write from multiple
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ch.read(new ByteBuffer[]{header, body});  // scatter

// Transfer between channels (zero-copy on some OSes)
try (FileChannel src = FileChannel.open(Path.of("a.bin"));
     FileChannel dst = FileChannel.open(Path.of("b.bin"), StandardOpenOption.WRITE)) {
    src.transferTo(0, src.size(), dst);
}

// ByteOrder
buf.order(ByteOrder.LITTLE_ENDIAN);
int value = buf.getInt();

Opérations sur les chemins (NIO.2)

Path (NIO.2) remplace l'ancienne classe File. Les opérations Path (getFileName, getParent, resolve, normalize, relativize) sont des mathématiques de chaînes pures — elles ne touchent pas au disque. Les méthodes Files.* interagissent avec le système de fichiers : taille, timestamps, permissions, symlinks. normalize() nettoie les segments . et ... resolveSibling est pratique pour renommer (même répertoire, nom différent). Préférez Path à File dans le code moderne.

java
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;

// Creating Path objects
Path p1 = Path.of("a/b/c.txt");          // relative
Path p2 = Path.of("/usr/local/bin");     // absolute
Path p3 = Paths.get("config.json");      // older API, same thing

// Path manipulation (doesn't touch filesystem)
System.out.println(p1.getFileName());    // "c.txt"
System.out.println(p1.getParent());      // "a/b"
System.out.println(p1.getRoot());        // "" (relative)
System.out.println(p2.getRoot());        // "/"
System.out.println(p1.toAbsolutePath()); // "/cwd/a/b/c.txt"
System.out.println(p1.normalize());      // removes . and ..
System.out.println(p1.resolve("d.txt")); // "a/b/c.txt/d.txt"
System.out.println(p1.resolveSibling("x.txt")); // "a/b/x.txt"
System.out.println(p1.relativize(Path.of("a/b")));  // "../.."

// File metadata
Path file = Path.of("notes.txt");
System.out.println(Files.size(file));            // bytes
System.out.println(Files.getLastModifiedTime(file));
System.out.println(Files.isReadable(file));
System.out.println(Files.isWritable(file));

// PosixFilePermissions (Linux/macOS)
String perms = "rwxr-xr--";
Set<PosixFilePermission> set = PosixFilePermissions.fromString(perms);
Files.setPosixFilePermissions(file, set);

// Symbolic links
Path link = Path.of("link.txt");
Files.createSymbolicLink(link, Path.of("target.txt"));
Path target = Files.readSymbolicLink(link);

Parcours de répertoires & arbres de fichiers

Files.list() liste un niveau ; Files.walk() parcourt récursivement l'arbre (renvoie un Stream, doit être fermé). Files.find() filtre par chemin et attributs pendant le parcours. Pour un contrôle total, walkFileTree avec un FileVisitor vous permet de sauter des sous-arbres, gérer les erreurs et agir avant/après la visite des répertoires. Tous renvoient des Streams qui détiennent des handles de fichiers — utilisez toujours try-with-resources. Utilisez une profondeur max pour borner les parcours coûteux.

java
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.*;

// List directory entries (one level)
try (Stream<Path> entries = Files.list(Path.of("."))) {
    entries.filter(Files::isRegularFile)
           .map(Path::getFileName)
           .forEach(System.out::println);
}

// Walk file tree recursively (depth-first)
try (Stream<Path> walk = Files.walk(Path.of("/project"))) {
    walk.filter(Files::isRegularFile)
        .filter(p -> p.toString().endsWith(".java"))
        .forEach(System.out::println);
}

// Walk with depth limit
try (Stream<Path> walk = Files.walk(Path.of("/project"), 3)) {
    walk.forEach(System.out::println);
}

// Find with BiPredicate (path + attributes)
try (Stream<Path> found = Files.find(Path.of("/logs"), 10,
        (path, attrs) -> attrs.isRegularFile()
            && attrs.size() > 1_000_000
            && path.toString().endsWith(".log"))) {
    found.forEach(p -> System.out.println("Large log: " + p));
}

// FileVisitor for full control (pre/post visit, skip subtrees)
Files.walkFileTree(Path.of("/project"), new SimpleFileVisitor<>() {
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("Visiting " + file);
        return FileVisitResult.CONTINUE;
    }
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        if (dir.getFileName().toString().equals("target")) {
            return FileVisitResult.SKIP_SUBTREE;  // skip target dirs
        }
        return FileVisitResult.CONTINUE;
    }
});

import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.BasicFileAttributes;

WatchService & événements de fichiers

WatchService reçoit les événements du système de fichiers (création, modification, suppression) pour les répertoires enregistrés. take() bloque jusqu'à ce qu'un événement arrive ; pollEvents() les draine. Surveillez le répertoire parent et filtrez par context() pour suivre un fichier spécifique. Les événements peuvent être coalescés ou perdus (OVERFLOW). La surveillance récursive nécessite l'enregistrement de chaque sous-répertoire. WatchService est natif à l'OS (inotify sur Linux, FSEvents sur macOS) mais son API est de bas niveau — envisagez des bibliothèques pour les besoins complexes.

java
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.IOException;

// Watch a directory for changes
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
    Path dir = Path.of("/var/log");
    dir.register(watcher,
        ENTRY_CREATE,
        ENTRY_DELETE,
        ENTRY_MODIFY);

    System.out.println("Watching " + dir + "...");
    while (true) {
        WatchKey key = watcher.take();  // blocks until event
        for (WatchEvent<?> event : key.pollEvents()) {
            Path changed = dir.resolve((Path) event.context());
            System.out.println(event.kind() + " -> " + changed);

            if (event.kind() == OVERFLOW) continue;  // events lost

            // React to change
            if (event.kind() == ENTRY_CREATE) {
                System.out.println("New file: " + changed);
            }
        }
        if (!key.reset()) break;  // key no longer valid (dir deleted)
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

// Note: WatchService watches directories, not individual files.
// To watch a file, watch its parent dir and filter by context().
// Events may be coalesced (rapid modifications may yield one event).
// For recursive watching, register each subdirectory manually
// or use a library like Apache Commons IO's FileAlterationMonitor.
17

Records & pattern matching

Bases des records

Les records (Java 16+) sont des porteurs de données immuables transparents. L'en-tête `record Name(Type1 f1, Type2 f2)` génère un constructeur, des accesseurs (f1(), f2() — pas getF1()), equals, hashCode et toString. Ils sont idéaux pour les DTO, objets de valeur et résultats de fonction. Le constructeur compact (juste `{ ... }`) valide ou normalise sans réassigner les champs. Les records peuvent implémenter des interfaces mais ne peuvent pas étendre de classes.

java
// Record: concise immutable data carrier (Java 16+)
public record Point(int x, int y) {}

// Equivalent to writing:
// - final class with private final int x, y
// - constructor Point(int x, int y)
// - accessors x(), y()
// - equals, hashCode, toString (all auto-generated)

Point p = new Point(3, 4);
System.out.println(p.x());          // 3 (accessor, not getX())
System.out.println(p.y());          // 4
System.out.println(p);              // "Point[x=3, y=4]"
System.out.println(p.equals(new Point(3, 4)));  // true

// Immutable: no setters
// p.setX(5);  // no such method

// Custom record with validation
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age: " + value);
        }
    }
}

// Record with multiple components
public record Employee(String name, int id, double salary, String dept) {}

// Records implement interfaces
public interface Named { String name(); }
public record Customer(String name, long id) implements Named {}

Constructeurs compacts & méthodes personnalisées

Le constructeur compact (`public Name { ... }`) s'exécute avant l'assignation des champs — assignez au paramètre pour normaliser, et le compilateur l'assigne au champ. Les records peuvent avoir des méthodes supplémentaires et des fabriques statiques mais pas de champs d'instance au-delà de l'en-tête. Les constructeurs non canoniques doivent déléguer au canonique via this(...). Utilisez des fabriques statiques pour une construction plus claire (Point.origin()) et pour mettre en cache les instances courantes.

java
// Compact constructor: validation without reassigning
public record Email(String address) {
    public Email {
        if (!address.contains("@")) {
            throw new IllegalArgumentException("Bad email: " + address);
        }
        address = address.toLowerCase().strip();  // normalize
    }
}

// Add custom methods (but not mutable fields)
public record Money(double amount, String currency) {
    public Money plus(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(amount + other.amount, currency);
    }
    public Money times(int factor) {
        return new Money(amount * factor, currency);
    }
    public static Money usd(double amt) { return new Money(amt, "USD"); }
}

// Canonical + non-canonical constructors
public record Range(int start, int end) {
    // Compact canonical (validation)
    public Range {
        if (start > end) throw new IllegalArgumentException();
    }
    // Non-canonical convenience constructor
    public Range(int end) {
        this(0, end);
    }
}

// Static factories are common on records
public record Point(int x, int y) {
    public static Point origin() { return new Point(0, 0); }
    public static Point of(int x, int y) { return new Point(x, y); }
}

Classes sealed

Les classes/interfaces sealed (Java 17+) restreignent les types qui peuvent les étendre via la clause `permits`. Chaque sous-type autorisé doit être final, sealed ou non-sealed. Combinés aux records, ils forment des types de données algébriques (héritage fermé + données immuables). Le bénéfice clé : le compilateur connaît tous les sous-types, donc les expressions switch peuvent être exhaustives sans branche default — si vous ajoutez un nouveau sous-type, le compilateur signale chaque switch à mettre à jour.

java
// Sealed class: restricts which classes can extend it (Java 17+)
public sealed interface Shape
    permits Circle, Square, Triangle {}

public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}

// Every permitted subtype must be final, sealed, or non-sealed
public non-sealed class WeirdShape implements Shape {}  // open again

// Why sealed? Enables exhaustive pattern matching
public double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Square sq -> sq.side() * sq.side();
        case Triangle t -> 0.5 * t.base() * t.height();
        // No default needed — compiler knows all subtypes
    };
}

// Sealed class hierarchy with records = algebraic data types
// Combines: closed inheritance (sealed) + immutable data (record)

// Sealed classes also work with classes (not just interfaces)
public sealed abstract class Result permits Success, Failure {}
public final class Success<T>(T value) extends Result {}
public final class Failure(String error) extends Result {}

Pattern matching pour instanceof

Le pattern matching pour instanceof (Java 16+) déclare une variable qui est liée seulement quand le test réussit, éliminant le cast explicite. La portée de la variable découle de la vérité du motif — utilisable dans les continuations && et après les retours anticipés. Java 21 a ajouté le pattern matching dans switch (case Type var when guard), permettant le dispatch basé sur le type avec des gardes. Cela rend le code de vérification de type bien plus concis et moins sujet aux erreurs.

java
// Old way: cast after instanceof
Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;  // redundant cast
    System.out.println(s.length());
}

// Java 16+: pattern variable
if (obj instanceof String s) {
    System.out.println(s.length());  // s is in scope, no cast
}

// The variable is scoped by the pattern's truth
if (obj instanceof String s && s.length() > 3) {
    System.out.println("Long string: " + s);
}
// s is NOT in scope here if the && short-circuited

// Negation scoping
if (!(obj instanceof String s)) {
    // s NOT in scope here
    return;
}
// s IS in scope here (the instanceof must have been true)

// Combining with other patterns
Object data = 42;
String desc = switch (data) {
    case Integer i when i > 0 -> "positive int: " + i;
    case Integer i -> "non-positive int: " + i;
    case String s -> "string: " + s;
    case null -> "null";
    default -> "other";
};

Switch pattern matching (Java 21)

Le switch pattern matching (Java 21, final) vous permet de switcher sur des types, déstructurer des records et ajouter des gardes (when). Combiné aux types sealed, le compilateur vérifie l'exhaustivité — pas de default nécessaire si tous les sous-types sont couverts. Les motifs de record (case Point(int x, int y)) déstructurent en une étape. Le case null est explicite (pas de NPE). Cela rapproche Java du pattern matching de ML/Scala pour modéliser des domaines de manière déclarative.

java
// Java 21: switch pattern matching with type patterns and guards
sealed interface Event permits Login, Logout, Message, Error {}
record Login(String user) implements Event {}
record Logout(String user) implements Event {}
record Message(String from, String text) implements Event {}
record Error(int code, String detail) implements Event {}

String describe(Event e) {
    return switch (e) {
        case Login l -> l.user() + " logged in";
        case Logout l -> l.user() + " logged out";
        case Message m when m.from().equals("system") -> "[system] " + m.text();
        case Message m -> m.from() + ": " + m.text();
        case Error(int code, String detail) when code >= 500 -> "SERVER ERROR " + code;
        case Error(int code, String detail) -> "error " + code + ": " + detail;
        case null -> "no event";  // explicit null handling
    };
}

// Record patterns: destructure records in one go
record Point(int x, int y) {}
String classify(Object o) {
    return switch (o) {
        case Point(int x, int y) when x == y -> "diagonal";
        case Point(int x, int y) -> "point at (" + x + "," + y + ")";
        default -> "not a point";
    };
}

// Nested patterns
record Box(Point p) {}
String describe2(Object o) {
    return switch (o) {
        case Box(Point(int x, int y)) -> "box at " + x + "," + y;
        default -> "unknown";
    };
}
18

Modules (JPMS)

Bases de module-info.java

module-info.java déclare un module (Java 9+ JPMS). requires ajoute une dépendance ; exports rend les packages accessibles ; opens permet l'accès par réflexion (nécessaire pour les frameworks de sérialisation/DI) ; uses/provides câblent le ServiceLoader. Sans ce fichier, le code vit sur le classpath comme un « module sans nom » avec un comportement legacy. Les modules donnent une forte encapsulation (seuls les packages exportés sont publics) et une configuration fiable (dépendances explicites).

java
// File: src/com.example.app/module-info.java
module com.example.app {
    requires java.sql;           // depends on java.sql module
    requires transitive com.example.lib;  // re-export (consumers see it too)
    requires static java.annotation;  // compile-time only (optional at runtime)

    exports com.example.app.api;       // public API visible to all
    exports com.example.app.internal to com.example.test;  // qualified export

    opens com.example.app.model to com.fasterxml.jackson.databind;  // reflection only
    opens com.example.app.dynamic;  // open to all for reflection

    uses com.example.app.spi.Plugin;  // service consumer
    provides com.example.app.spi.Plugin with com.example.app.plugins.DefaultPlugin;  // service provider
}

// Key directives:
// requires: depends on another module
// exports: makes packages public to other modules
// opens: allows deep reflection (for frameworks like Jackson, Hibernate)
// uses/provides: service loader integration

// A module without module-info.java is an "unnamed module" (classpath behavior)

requires, exports, opens

requires déclare une dépendance ; requires transitive la propage (utilisez quand votre API publique expose des types de ce module). exports rend les packages publics ; exports to restreint aux modules nommés (exports qualifiés). opens accorde l'accès par réflexion (réflexion profonde) — essentiel pour les frameworks qui font setAccessible(true). La distinction entre exports (API publique) et opens (réflexion) est clé : la forte encapsulation est le défaut, et vous optez par package.

java
// Module A: com.example.library
module com.example.library {
    // Public API anyone can use
    exports com.example.library.api;

    // Internal package: only visible to specific modules
    exports com.example.library.internal to com.example.app;

    // Allow reflection for frameworks (Jackson, JPA)
    opens com.example.library.model;

    // Only specific module can reflect
    opens com.example.library.config to com.example.app;

    // Dependencies
    requires java.logging;
    requires transitive java.sql;  // consumers of A also get java.sql
}

// Module B: com.example.app (consumer)
module com.example.app {
    requires com.example.library;  // use A's exported packages
    // Note: transitive means java.sql is also available here

    requires com.fasterxml.jackson.databind;
}

// 'requires transitive X' means: any module requiring this one
// also reads X. Use when your exported API exposes X's types.

// 'opens' vs 'exports':
// exports: compile-time + runtime access to public members
// opens: runtime reflective access to ALL members (including private)

// Reflective access without 'opens' fails with InaccessibleObjectException
// in Java 16+ (strong encapsulation enforced by default).

ServiceLoader & services

ServiceLoader implémente le motif SPI : une interface dans un module, des implémentations découvertes à l'exécution. Le module API exporte l'interface ; les modules fournisseurs déclarent `provides X with Y` ; les modules consommateurs déclarent `uses X`. ServiceLoader.load(X.class) trouve tous les fournisseurs sur le chemin de modules. Cela découple l'interface de l'implémentation — les drivers JDBC, les backends de logging (SLF4J) et les fournisseurs de Charset fonctionnent tous ainsi. Pas de dépendance de compilation sur l'implémentation.

java
// SPI (Service Provider Interface) pattern with modules

// 1. Define the service interface in an API module
module com.example.spi {
    exports com.example.spi;
}
package com.example.spi;
public interface Plugin {
    String name();
    void run();
}

// 2. Implement in a provider module
module com.example.plugin.impl {
    requires com.example.spi;
    provides com.example.spi.Plugin with com.example.plugin.impl.DefaultPlugin;
}
package com.example.plugin.impl;
import com.example.spi.Plugin;
public class DefaultPlugin implements Plugin {
    public String name() { return "default"; }
    public void run() { System.out.println("running"); }
}

// 3. Consume in an app module
module com.example.app {
    requires com.example.spi;
    uses com.example.spi.Plugin;  // declares intent to load services
}

// Loading services at runtime
import java.util.ServiceLoader;
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin p : loader) {
    System.out.println("Found: " + p.name());
    p.run();
}

// ServiceLoader is how JDBC drivers, SLF4J backends, and many
// plugin systems are discovered without compile-time dependencies.

Chemin de modules vs classpath

Le chemin de modules (--module-path) contient des JARs modulaires avec forte encapsulation ; le classpath (-cp) contient des JARs legacy comme modules sans nom sans encapsulation. Les JARs modulaires fonctionnent sur les deux. Les modules automatiques sont des JARs sans module-info placés sur le chemin de modules — leur nom vient du nom de fichier ou de l'attribut de manifest Automatic-Module-Name. --add-opens est une échappatoire pour la réflexion legacy qui casse sous la forte encapsulation.

java
// Compile a module
//   javac -d out/com.example.app \
//         --module-source-path src \
//         --module com.example.app

// Run a modular app
//   java --module-path out --module com.example.app/com.example.app.Main

// Package as a modular JAR (includes module-info.class)
//   jar --create --file app.jar --main-class com.example.app.Main -C out/com.example.app .

// Module path vs classpath:
// --module-path: modules with module-info, strong encapsulation enforced
// --class-path (or -cp): legacy "unnamed module", everything public, no encapsulation

// Mixing: modular JARs can be used on the classpath too (automatic module)
//   java -cp lib/app.jar:lib/dep.jar com.example.app.Main
// An automatic module: a JAR without module-info on the module path.
// Its name is derived from the JAR filename (Automatic-Module-Name in MANIFEST.MF
// gives an explicit name).

// Inspect a module JAR
//   jar --describe-module --file app.jar

// List observable modules
//   java --list-modules
//   java --describe-module java.sql

// Add opens at runtime for legacy reflection (escape hatch)
//   java --add-opens com.example.app/com.example.app.internal=ALL-UNNAMED

jdeps & jlink (runtimes personnalisés)

jdeps analyse le bytecode pour lister les dépendances de modules — utile pour migrer vers les modules et trouver les dépendances inutilisées. jlink crée un JRE personnalisé contenant seulement les modules dont votre application a besoin, produisant un runtime autonome, plus petit, à démarrage plus rapide. C'est idéal pour les images Docker et les installateurs : livrez l'application plus un JRE de 30-50MB au lieu de nécessiter une installation JDK de 300MB. Ensemble, jdeps + jlink permettent des déploiements Java maigres et autonomes.

java
# jdeps: analyze dependencies (find unused, list required modules)

# List dependencies of a JAR
jdeps --module-path lib app.jar

# Generate module-info.java for an existing JAR
jdeps --generate-module-info ./out app.jar

# Show which JDK modules a JAR uses
jdeps --print-module-deps --ignore-missing-deps app.jar
# Output: java.base,java.logging,java.sql

# jlink: create a custom stripped-down JRE containing only needed modules
jlink \
  --module-path "$JAVA_HOME/jmods:./out" \
  --add-modules com.example.app \
  --output ./custom-jre \
  --strip-debug \
  --compress=zip-6 \
  --no-header-files \
  --no-man-pages \
  --launcher app=com.example.app/com.example.app.Main

# The custom JRE is self-contained:
#   ./custom-jre/bin/app   # launches the app
#   ./custom-jre/bin/java  # the stripped JVM

# Benefits of jlink:
# - Smaller distribution (only needed modules)
# - Faster startup (less to load)
# - No need to install Java on target machine
# - Can cross-target (different OS/arch) with matching jmods

# Common workflow:
# 1. jdeps to find required modules
# 2. jlink to build a custom runtime
# 3. Package app + custom JRE together (Docker image, installer)
19

Approfondissement de la concurrence

Verrous : ReentrantLock & ReadWriteLock

ReentrantLock offre plus de contrôle que synchronized : tryLock (non bloquant/chronométré), équité, interruptibilité et inspection de l'état du verrou. Déverrouillez toujours dans finally. ReadWriteLock permet de nombreux lecteurs concurrents mais des écrivains exclusifs — génial pour les caches à forte lecture. StampedLock (Java 8+) ajoute des lectures optimistes pour un encore meilleur débit de lecture. Préférez synchronized pour les cas simples ; utilisez Lock quand vous avez besoin de ses fonctionnalités avancées.

java
import java.util.concurrent.locks.*;

// ReentrantLock: more flexible than synchronized
class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // MUST be in finally
        }
    }

    public int get() {
        lock.lock();
        try { return count; }
        finally { lock.unlock(); }
    }
}

// tryLock with timeout (avoids deadlock-induced hangs)
if (lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
    try { /* work */ } finally { lock.unlock(); }
}

// Fair lock (FIFO ordering, slower)
ReentrantLock fair = new ReentrantLock(true);

// ReadWriteLock: many readers OR one writer
class Cache {
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private final Lock read = rw.readLock();
    private final Lock write = rw.writeLock();
    private java.util.Map<String, String> data = new java.util.HashMap<>();

    public String get(String key) {
        read.lock();
        try { return data.get(key); }
        finally { read.unlock(); }
    }
    public void put(String key, String val) {
        write.lock();
        try { data.put(key, val); }
        finally { write.unlock(); }
    }
}

Collections concurrentes

ConcurrentHashMap est la map thread-safe de travail — utilisez compute/merge pour les mises à jour atomiques au lieu de check-then-act. CopyOnWriteArrayList est meilleur pour les listes à forte lecture, écriture rare (écouteurs d'événements) — les écritures copient le tableau. BlockingQueue est l'épine dorsale des pipelines producteur-consommateur (put bloque quand plein, take bloque quand vide). ConcurrentLinkedQueue est non bloquante et non bornée. Ils remplacent les wrappers synchronized (Collections.synchronizedX) qui sont à grain grossier et plus lents.

java
import java.util.concurrent.*;
import java.util.*;

// ConcurrentHashMap: thread-safe HashMap (no null keys/values)
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.putIfAbsent("a", 2);  // only if absent
map.compute("a", (k, v) -> v == null ? 1 : v + 1);  // atomic update
map.merge("a", 1, Integer::sum);  // add 1 atomically
Integer val = map.getOrDefault("a", 0);

// CopyOnWriteArrayList: snapshot semantics, fast reads, slow writes
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("x");  // copies entire array
for (String l : listeners) { /* safe iteration, no ConcurrentModificationException */ }

// BlockingQueue: producer-consumer pattern
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// Producer
queue.put("task");        // blocks if full
boolean added = queue.offer("task", 1, TimeUnit.SECONDS);  // timed
// Consumer
String task = queue.take();  // blocks if empty
String polled = queue.poll(1, TimeUnit.SECONDS);

// ConcurrentLinkedQueue: non-blocking, unbounded
Queue<String> q = new ConcurrentLinkedQueue<>();
q.offer("a");

// SkipListMap / SkipListSet: concurrent sorted collections
ConcurrentNavigableMap<Integer, String> sorted = new ConcurrentSkipListMap<>();

import java.util.concurrent.TimeUnit;

CountDownLatch & CyclicBarrier

CountDownLatch est une porte à usage unique — N threads comptent à rebours, d'autres attendent ; non réinitialisable. CyclicBarrier est réutilisable — les threads s'attendent à un point de rendez-vous, avec une action optionnelle quand tous arrivent. Phaser est le plus flexible : participants variables, multiples phases, structure en arbre. Utilisez latch pour la coordination de démarrage, barrier pour les algorithmes parallèles multi-phases, phaser pour les comptes de participants dynamiques.

java
import java.util.concurrent.*;
import java.util.*;

// CountDownLatch: one-shot gate, N threads must arrive before proceeding
CountDownLatch ready = new CountDownLatch(3);
List<String> results = Collections.synchronizedList(new ArrayList<>());

for (int i = 0; i < 3; i++) {
    final int id = i;
    new Thread(() -> {
        try { Thread.sleep(id * 100); } catch (InterruptedException e) {}
        results.add("worker-" + id);
        ready.countDown();  // signal done
    }).start();
}
ready.await();  // main thread blocks until count reaches 0
System.out.println("All done: " + results);

// CyclicBarrier: reusable barrier, threads wait for each other
CyclicBarrier barrier = new CyclicBarrier(3, () ->
    System.out.println("--- phase complete ---"));

Runnable worker = () -> {
    try {
        System.out.println(Thread.currentThread().getName() + " phase 1");
        barrier.await();  // wait for all 3
        System.out.println(Thread.currentThread().getName() + " phase 2");
        barrier.await();  // reusable for next phase
    } catch (Exception e) {}
};
for (int i = 0; i < 3; i++) new Thread(worker).start();

// Phaser: more flexible (variable parties, multiple phases)
Phaser phaser = new Phaser(3);
phaser.register();  // dynamically add a party
phaser.arriveAndAwaitAdvance();

Semaphore & Exchanger

Semaphore contrôle l'accès à N permis — acquire bloque jusqu'à ce qu'un soit disponible, release le rend. Utilisez pour la limitation de débit, les pools de connexions ou tout scénario de ressource bornée. tryAcquire offre des variantes non bloquantes et chronométrées. Exchanger laisse deux threads échanger des valeurs à un point de rendez-vous — niche mais élégant pour les designs de pipeline où deux threads échangent des tampons. Les deux sont dans java.util.concurrent et sont de plus bas niveau que les verrous pour certains motifs de coordination.

java
import java.util.concurrent.*;

// Semaphore: limit concurrent access to N permits
Semaphore pool = new Semaphore(5);  // 5 concurrent allowed

pool.acquire();  // blocks until a permit is available
try {
    // critical section (at most 5 threads here at once)
    System.out.println("Working, permits left: " + pool.availablePermits());
} finally {
    pool.release();  // return the permit
}

// Try-acquire (non-blocking)
if (pool.tryAcquire()) {
    try { /* work */ } finally { pool.release(); }
} else {
    System.out.println("Too busy, try later");
}

// Timed acquire
if (pool.tryAcquire(1, TimeUnit.SECONDS)) {
    try { /* work */ } finally { pool.release(); }
}

// Use case: rate limiting, connection pools, parking lots

// Exchanger: two threads swap values
Exchanger<String> ex = new Exchanger<>();
new Thread(() -> {
    try {
        String got = ex.exchange("from-A");  // gives "from-A", receives "from-B"
        System.out.println("A got: " + got);
    } catch (InterruptedException e) {}
}).start();
String got = ex.exchange("from-B");  // gives "from-B", receives "from-A"
System.out.println("B got: " + got);

import java.util.concurrent.TimeUnit;

CompletableFuture avancé

CompletableFuture est la Promise de Java — composez du travail async avec thenApply (map), thenCompose (flatMap), thenCombine (zip deux). allOf attend tous, anyOf pour le premier. exceptionally récupère des erreurs ; handle couvre les deux. orTimeout (Java 9+) annule si ça prend trop de temps. Passez toujours un exécuteur explicite — le commonPool par défaut peut affamer sous un travail bloquant. C'est la fondation du code async de style réactif en Java.

java
import java.util.concurrent.*;
import java.util.*;

ExecutorService pool = Executors.newFixedThreadPool(4);

// Async composition (like JS Promises)
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "hello", pool)            // async supplier
    .thenApply(String::toUpperCase)              // transform
    .thenCompose(s -> CompletableFuture.supplyAsync(  // flatMap
        () -> s + " world"))
    .thenApply(s -> s + "!");

System.out.println(future.join());  // "HELLO world!"

// Combine two independent futures
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> sum = a.thenCombine(b, Integer::sum);
System.out.println(sum.join());  // 30

// Run multiple and wait for all (or any)
List<CompletableFuture<Integer>> futures = List.of(
    CompletableFuture.supplyAsync(() -> 1, pool),
    CompletableFuture.supplyAsync(() -> 2, pool),
    CompletableFuture.supplyAsync(() -> 3, pool)
);
CompletableFuture<Void> all = CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0]));
all.join();
List<Integer> results = futures.stream().map(CompletableFuture::join).toList();

// Error handling
CompletableFuture<String> safe = CompletableFuture
    .supplyAsync(() -> { throw new RuntimeException("boom"); })
    .exceptionally(ex -> "fallback: " + ex.getMessage());
System.out.println(safe.join());  // "fallback: boom"

// handle: both success and failure
CompletableFuture<String> handled = CompletableFuture
    .supplyAsync(() -> "ok")
    .handle((val, ex) -> ex == null ? val : "error");

// Timeout (Java 9+)
CompletableFuture<String> timed = CompletableFuture
    .supplyAsync(() -> { Thread.sleep(5000); return "slow"; })
    .orTimeout(1, TimeUnit.SECONDS)
    .exceptionally(ex -> "timed out");

Threads virtuels (Java 21)

Les threads virtuels (Java 21) sont des threads légers ordonnancés par la JVM sur un petit pool de threads porteurs (de plateforme). Quand un thread virtuel bloque sur des I/O, il est suspendu et le porteur en exécute un autre — donc vous pouvez avoir des millions d'opérations bloquantes concurrentes. Cela vous permet d'écrire du code bloquant simple au lieu de chaînes réactives/async complexes. Utilisez-les pour les charges de travail liées aux I/O (gestionnaires HTTP, appels DB) ; pour le travail CPU-bound, les threads de plateforme ou parallelStream restent appropriés.

java
import java.util.concurrent.*;
import java.util.*;

// Virtual threads: lightweight, cheap, millions possible
// Java 21 LTS feature — "Project Loom"

// Start a virtual thread
Thread vt = Thread.ofVirtual().start(() -> {
    System.out.println("Running on: " + Thread);
});

// Builder pattern
Thread vt2 = Thread.ofVirtual().name("worker-1").start(() -> {
    // blocking I/O here is fine — virtual thread yields, not the OS thread
});

// Per-thread factory
ThreadFactory factory = Thread.ofVirtual().factory();

// ExecutorService for virtual threads (Java 21+)
try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
    // Submit a million tasks — each gets its own virtual thread
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 1_000_000; i++) {
        final int id = i;
        futures.add(es.submit(() -> {
            Thread.sleep(100);  // blocking call is cheap on virtual threads
            return "done-" + id;
        }));
    }
    // virtual threads yield when blocking, so 1M concurrent is feasible
}

// Why virtual threads?
// - Platform threads (OS threads) are heavy (~1MB stack, kernel scheduling)
// - Virtual threads are user-mode, ~KB, scheduled by JVM on a small carrier pool
// - Blocking I/O on a virtual thread doesn't block a platform thread
// - Lets you write straightforward blocking code at scale (no reactive complexity)

// Best practice: use virtual threads for I/O-bound work,
// NOT for CPU-bound work (use platform threads / parallelStream).
20

Approfondissement du framework Collections

Comparators & tri

Comparator.comparing(keyExtractor) construit des comparateurs à partir d'une fonction clé — bien plus propre que d'écrire de la logique compare brute. thenComparing chaîne des clés de tri secondaires. nullsFirst/nullsLast gèrent les nulls sûrement. Utilisez comparingInt/comparingLong/comparingDouble pour éviter l'autoboxing. List.sort() trie sur place (listes mutables seulement) ; Stream.sorted() renvoie un nouveau stream trié. Les comparateurs alimentent le tri, l'ordre TreeSet/TreeMap et les opérations de stream.

java
import java.util.*;
import java.util.stream.*;

record Person(String name, int age) {}

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Carol", 30),
    new Person("Dave", 25)
);

// Comparator.comparing (key extractor)
people.stream().sorted(Comparator.comparing(Person::name)).toList();
people.stream().sorted(Comparator.comparingInt(Person::age)).toList();

// Reversed
people.stream().sorted(Comparator.comparing(Person::age).reversed()).toList();

// Chained (thenComparing): sort by age, then by name
List<Person> sorted = people.stream().sorted(
    Comparator.comparingInt(Person::age)
              .thenComparing(Person::name)
).toList();
// [Bob(25), Dave(25), Alice(30), Carol(30)]

// Nulls handling
Comparator<String> cmp = Comparator.nullsFirst(Comparator.naturalOrder());
List.of("b", null, "a").stream().sorted(cmp).toList();  // [null, a, b]

// Custom comparator
Comparator<Person> byNameLen = (a, b) -> a.name().length() - b.name().length();

// Mutable list sort
List<String> names = new ArrayList<>(List.of("charlie", "alice", "bob"));
names.sort(Comparator.naturalOrder());
// names = [alice, bob, charlie]

// Comparing with primitive specializations avoids boxing
Comparator<Person> byAge = Comparator.comparingInt(Person::age);

Collections non modifiables & immuables

List.of/Set.of/Map.of (Java 9+) créent des collections véritablement immuables — pas de nulls, pas de mutations. Collections.unmodifiableX crée une vue en lecture seule qui reflète encore les changements de la collection sous-jacente. List.copyOf (Java 10+) fait une copie immuable indépendante. Arrays.asList est une vue à taille fixe d'un tableau (set fonctionne, add/remove non). Choisissez selon le besoin : fabriques immuables pour les constantes, vues non modifiables pour exposer les internes sûrement, copyOf pour les copies défensives.

java
import java.util.*;

// Java 9+ immutable factories (List.of, Set.of, Map.of)
List<String> immutable = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Map<String, Integer> bigMap = Map.ofEntries(
    Map.entry("x", 1), Map.entry("y", 2), Map.entry("z", 3)
);
// immutable.add("d");  // throws UnsupportedOperationException
// immutable.set(0, "z");  // throws
// nulls not allowed in these immutable collections

// Unmodifiable view (wraps an existing collection)
List<String> mutable = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(mutable);
// view.add("c");  // throws
mutable.add("c");  // but changes to backing list ARE visible in view
System.out.println(view);  // [a, b, c]

// CopyOf (Java 10+): creates immutable copy
List<String> copy = List.copyOf(mutable);  // independent immutable copy

// Arrays.asList: fixed-size view of an array
String[] arr = {"a", "b"};
List<String> fixed = Arrays.asList(arr);
fixed.set(0, "x");  // OK (writes through to array)
// fixed.add("c");  // throws (size fixed)

// To make a truly mutable copy:
List<String> mut = new ArrayList<>(Arrays.asList(arr));

Implémentations Queue & Deque

ArrayDeque est l'implémentation préférée de pile et file — plus rapide que le Stack legacy (qui est synchronisé) et LinkedList. PriorityQueue ordonne les éléments par un Comparator (min-heap par défaut) — utilisez pour l'ordonnancement, problèmes top-K. Deque supporte les deux extrémités ; utilisez addFirst/removeFirst pour la sémantique de pile, addLast/removeFirst pour la sémantique de file. Pour les files concurrentes, utilisez les implémentations java.util.concurrent (LinkedBlockingQueue, etc.).

java
import java.util.*;

// Queue: FIFO (offer/poll/peek)
Queue<String> queue = new LinkedList<>();
queue.offer("a"); queue.offer("b");
System.out.println(queue.peek());  // "a" (head)
System.out.println(queue.poll());  // "a" (remove head)

// Deque: double-ended (add/remove at both ends)
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("a"); deque.addLast("b");
System.out.println(deque.peekFirst());  // "a"
System.out.println(deque.peekLast());   // "b"
deque.removeFirst(); deque.removeLast();

// ArrayDeque as a stack (push/pop/peek)
Deque<String> stack = new ArrayDeque<>();
stack.push("first");  // addFirst
stack.push("second");
System.out.println(stack.pop());  // "second" (LIFO)
System.out.println(stack.peek()); // "first"

// PriorityQueue: orders by Comparator (not insertion order)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5); pq.offer(1); pq.offer(3);
System.out.println(pq.poll());  // 1 (smallest first)
System.out.println(pq.poll());  // 3

// With custom comparator
PriorityQueue<String> byLen = new PriorityQueue<>(Comparator.comparingInt(String::length));
byLen.offer("aaa"); byLen.offer("a"); byLen.offer("aa");
System.out.println(byLen.poll());  // "a"

// BlockingQueue implementations (concurrency): see concurrency-deep section
// ArrayDeque is faster than Stack/LinkedList for stack/queue use

Map merge, compute, getOrDefault

Ces méthodes Map rendent les motifs courants atomiques et concis. merge est l'idiome de comptage de mots — combine la valeur existante et nouvelle, supprimant l'entrée si la fonction renvoie null. computeIfAbsent est le motif de cache paresseux (mémoïsation). getOrDefault évite les vérifications null. replaceAll transforme toutes les valeurs. C'est bien plus propre que la danse check-then-act get/put, et ce sont les blocs de construction pour les mises à jour atomiques de ConcurrentHashMap dans le code concurrent.

java
import java.util.*;

Map<String, Integer> counts = new HashMap<>();

// getOrDefault: safe read with default
int n = counts.getOrDefault("missing", 0);  // 0, no null

// putIfAbsent: only set if not present
counts.putIfAbsent("a", 1);  // sets to 1
counts.putIfAbsent("a", 2);  // no change (already present)

// compute: recompute value for a key
counts.compute("a", (k, v) -> v == null ? 1 : v + 1);  // increment

// computeIfAbsent: lazy initialization (cache pattern)
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("key", k -> new ArrayList<>()).add("item");
// creates list if absent, then adds — atomic, no race in single thread

// computeIfPresent: update only if present
counts.computeIfPresent("a", (k, v) -> v > 0 ? v - 1 : null);  // decrement, remove at 0

// merge: combine existing and new value (great for counting)
counts.merge("word", 1, Integer::sum);  // word count pattern
counts.merge("word", 1, Integer::sum);  // now 2
counts.merge("word", 1, Integer::sum);  // now 3

// merge with removal: function returns null -> entry removed
counts.merge("word", 1, (old, v) -> old > 1 ? old - 1 : null);

// replaceAll: transform all values
Map<String, Integer> doubled = new HashMap<>(counts);
doubled.replaceAll((k, v) -> v * 2);

// Word frequency counter (idiomatic)
String text = "the cat the dog the bird";
Map<String, Integer> freq = new HashMap<>();
for (String w : text.split(" ")) {
    freq.merge(w, 1, Integer::sum);
}

Méthodes utilitaires de Collections

Collections.* fournit les utilitaires classiques : sort, binarySearch (nécessite une entrée triée), shuffle, reverse, frequency, min/max. Les fabriques singleton/empty renvoient des collections immuables à un élément ou vides — préférez emptyList() à renvoyer null. nCopies est économe en mémoire (un élément partagé). Les wrappers synchronisés existent pour le code legacy mais préférez les collections java.util.concurrent. Les wrappers checked capturent les violations de type générique à l'exécution, utiles pour l'interop avec les types bruts.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));

// Sorting
Collections.sort(nums);                    // in-place, natural order
Collections.sort(nums, Comparator.reverseOrder());
nums.sort(Comparator.naturalOrder());      // List.sort (preferred)

// Searching (list must be sorted first)
int idx = Collections.binarySearch(nums, 4);  // index, or negative if absent

// Shuffling & reversing
Collections.shuffle(nums);
Collections.reverse(nums);

// Frequency & disjoint
int freq = Collections.frequency(nums, 1);  // count occurrences
boolean dis = Collections.disjoint(List.of(1, 2), List.of(3, 4));  // true

// Min/max
int min = Collections.min(nums);
int max = Collections.max(nums, Comparator.reverseOrder());

// Singleton collections (immutable, single element)
Set<String> one = Collections.singleton("only");
List<Integer> oneList = Collections.singletonList(42);
Map<String, Integer> oneMap = Collections.singletonMap("k", 1);

// Empty collections (prefer over returning null)
List<Object> empty = Collections.emptyList();
Set<Object> emptySet = Collections.emptySet();

// nCopies (immutable list of n copies)
List<String> padding = Collections.nCopies(5, "x");  // [x,x,x,x,x]

// Synchronized wrappers (legacy — prefer concurrent collections)
List<String> sync = Collections.synchronizedList(new ArrayList<>());

// Checked wrappers (catch heap pollution at runtime)
List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
// checked.add(123);  // throws ClassCastException at the add site
21

Tests (JUnit 5 & Mockito)

Bases de JUnit 5

Annotations JUnit 5 (Jupiter) : @Test marque un test ; @BeforeEach/@AfterEach s'exécutent autour de chaque test ; @BeforeAll/@AfterAll s'exécutent une fois pour la classe (doivent être statiques). @DisplayName personnalise les noms de test. @Disabled saute les tests. assertThrows vérifie les exceptions. Les tests devraient être indépendants — utilisez @BeforeEach pour réinitialiser l'état, pas des champs statiques. JUnit 5 vit dans org.junit.jupiter.api (différent du org.junit de JUnit 4).

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    private Calculator calc;

    @BeforeAll
    static void setUpOnce() {
        // runs once before all tests (must be static)
        System.out.println("Starting CalculatorTest");
    }

    @BeforeEach
    void setUp() {
        // runs before each test
        calc = new Calculator();
    }

    @Test
    @DisplayName("2 + 2 should equal 4")
    void addsTwoNumbers() {
        assertEquals(4, calc.add(2, 2));
    }

    @Test
    void dividesByZeroThrows() {
        ArithmeticException ex = assertThrows(
            ArithmeticException.class,
            () -> calc.divide(1, 0)
        );
        assertEquals("Division by zero", ex.getMessage());
    }

    @AfterEach
    void tearDown() {
        // runs after each test (cleanup)
        calc = null;
    }

    @AfterAll
    static void tearDownOnce() {
        // runs once after all tests
    }

    @Disabled("until bug #42 is fixed")
    @Test
    void skippedTest() {}
}

class Calculator {
    int add(int a, int b) { return a + b; }
    int divide(int a, int b) {
        if (b == 0) throw new ArithmeticException("Division by zero");
        return a / b;
    }
}

Assertions

Assertions JUnit 5 : assertEquals/assertNotEquals, assertTrue/False, assertNull/NotNull, assertSame (identité). assertAll groupe les vérifications pour que toutes s'exécutent même si certaines échouent. assertTimeout fait échouer les tests lents. Les messages peuvent être des chaînes ou des Suppliers (paresseux — construits seulement en cas d'échec, évitant la concaténation de chaînes quand les tests passent). assertIterableEquals compare des collections ordonnées. Celles-ci viennent de org.junit.jupiter.api.Assertions.

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class AssertionsTest {

    @Test
    void equality() {
        assertEquals(4, 2 + 2);
        assertEquals(4, 2 + 2, "math is broken");
        assertNotEquals(5, 2 + 2);
    }

    @Test
    void booleans() {
        assertTrue(5 > 3);
        assertFalse(5 < 3, "5 should not be less than 3");
    }

    @Test
    void nullness() {
        assertNull(null);
        assertNotNull(new Object());
    }

    @Test
    void sameInstance() {
        String a = "x";
        assertSame(a, a);          // ==
        assertNotSame(a, new String("x"));
    }

    @Test
    void collections() {
        assertIterableEquals(List.of(1, 2, 3), List.of(1, 2, 3));
        assertLinesMatch(List.of("a.*", "b"), List.of("abc", "b"));
    }

    @Test
    void groupedAssertions() {
        // All run even if one fails — reports all failures
        assertAll("person",
            () -> assertEquals("Alice", "Alice"),
            () -> assertEquals(30, 30),
            () -> assertNotNull("x")
        );
    }

    @Test
    void timeout() {
        // Fails if it takes longer than 100ms
        assertTimeout(Duration.ofMillis(100), () -> {
            Thread.sleep(10);
        });
        // assertTimeoutPreemptively: stops the task early (in another thread)
    }

    @Test
    void customMessage() {
        int result = 5;
        assertEquals(4, result, () -> "expected 4 but got " + result);
        // Supplier<String> — message built lazily only on failure
    }

    import java.time.Duration;
    import java.util.List;
}

Tests paramétrés

Les tests paramétrés exécutent la même logique de test avec de multiples entrées. @ValueSource fournit un tableau à un argument. @CsvSource mappe des lignes CSV à de multiples paramètres. @MethodSource (le plus flexible) utilise un Stream<Arguments> statique. @EnumSource itère les valeurs d'enum. @NullAndEmptySource ajoute des cas null/vide. Cela élimine les méthodes de test copier-coller et rend les tests data-driven propres. Les noms d'affichage incluent automatiquement les paramètres pour un diagnostic facile.

java
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class ParameterizedTests {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    void positiveNumbersArePositive(int n) {
        assertTrue(n > 0);
    }

    @ParameterizedTest
    @ValueSource(strings = {"", "  ", "\t"})
    void blankStrings(String s) {
        assertTrue(s.isBlank());
    }

    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = {"  ", "\t"})
    void blankOrNull(String s) {
        assertTrue(s == null || s.isBlank());
    }

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "10, -5, 5"
    })
    void addition(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/testdata.csv", numLinesToSkip = 1)
    void fromCsv(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    @ParameterizedTest
    @MethodSource("additionProvider")
    void fromMethod(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }
    static java.util.stream.Stream<Arguments> additionProvider() {
        return java.util.stream.Stream.of(
            Arguments.of(1, 1, 2),
            Arguments.of(2, 3, 5)
        );
    }

    @ParameterizedTest
    @EnumSource(TimeUnit.class)
    void allEnums(TimeUnit unit) {
        assertNotNull(unit);
    }
}

Lifecycle, Nested & Conditional

@Nested crée des classes de test internes qui partagent le lifecycle — génial pour les structures BDD de style « quand X alors Y » où le setup externe s'applique aux tests internes. Les annotations conditionnelles (@EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable) sautent les tests selon l'environnement. @Tag groupe les tests pour une exécution sélective (par exemple, rapide vs lent, unitaire vs intégration). Les classes imbriquées ne peuvent pas avoir @BeforeAll (elles sont non statiques). Ces fonctionnalités rendent l'organisation des tests expressive.

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.condition.*;

class LifecycleTest {

    @Test
    void topLevel() {}

    @Nested
    @DisplayName("when stack is empty")
    class WhenEmpty {
        @BeforeEach
        void setUp() { /* stack is empty here */ }

        @Test
        void throwsOnPop() {
            assertThrows(Exception.class, () -> {});
        }

        @Nested
        @DisplayName("after pushing one element")
        class AfterPush {
            @BeforeEach
            void push() { /* push one */ }

            @Test
            void popReturnsElement() {
                // ...
            }
        }
    }

    // Conditional execution
    @Test
    @EnabledOnOs(OS.LINUX)
    void onlyOnLinux() {}

    @Test
    @EnabledIfSystemProperty(named = "env", matches = "ci")
    void onlyInCi() {}

    @Test
    @EnabledIfEnvironmentVariable(named = "DATABASE", matches = "postgres")
    void onlyWithPostgres() {}

    @Test
    @DisabledIf("customCondition")
    void conditional() {}
    static boolean customCondition() { return java.time.LocalTime.now().getHour() < 9; }

    // Tagging for selective runs
    @Test
    @Tag("slow")
    void slowIntegrationTest() {}

    @Test
    @Tag("fast")
    void fastUnitTest() {}

    // Run only fast: mvn test -Dgroups=fast
    import org.junit.jupiter.api.condition.OS;
    import java.util.concurrent.TimeUnit;

Mocking avec Mockito

Mockito crée des doubles de test pour les dépendances. @Mock crée un mock ; @InjectMocks construit un objet réel avec des mocks injectés. when(...).thenReturn(...) stubbe les valeurs de retour ; verify(...) vérifie les interactions. Les matchers d'arguments (any(), eq(), argThat()) correspondent flexiblement aux appels. Les spies enveloppent des objets réels (mocking partiel). Le motif Arrange-Act-Assert garde les tests lisibles. Le mocking isole l'unité testée de ses dépendances (base de données, réseau, temps).

java
import org.junit.jupiter.api.*;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Mock
    UserRepository repo;  // mock dependency

    @InjectMocks
    UserService service;  // real service with mocks injected

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void returnsUserWhenFound() {
        // Arrange: stub the mock
        User alice = new User(1, "Alice");
        when(repo.findById(1)).thenReturn(alice);

        // Act
        String name = service.getUserName(1);

        // Assert
        assertEquals("Alice", name);
        verify(repo).findById(1);       // called once
        verify(repo, never()).findById(2);
    }

    @Test
    void throwsWhenNotFound() {
        when(repo.findById(99)).thenReturn(null);
        // or: when(repo.findById(99)).thenThrow(new RuntimeException());

        assertThrows(RuntimeException.class, () -> service.getUserName(99));
    }

    @Test
    void argumentMatchers() {
        when(repo.findById(anyInt())).thenReturn(new User(0, "default"));
        // matchers: eq(), any(), anyInt(), contains(), argThat()

        service.getUserName(42);
        verify(repo).findById(intThat(n -> n > 0));
    }

    @Test
    void verifyInteractionDetails() {
        service.getUserName(1);
        verify(repo, times(1)).findById(1);
        verify(repo, atLeastOnce()).findById(anyInt());
        verifyNoMoreInteractions(repo);
    }

    @Test
    void spy_partialMock() {
        List<String> spy = spy(new ArrayList<>());
        spy.add("real");
        when(spy.size()).thenReturn(100);  // stub one method
        assertEquals(100, spy.size());     // stubbed
        assertEquals(1, spy.size());       // wait, this would be 100 too
    }
}

interface UserRepository { User findById(int id); }
record User(int id, String name) {}
class UserService {
    UserRepository repo;
    UserService(UserRepository r) { repo = r; }
    String getUserName(int id) {
        User u = repo.findById(id);
        if (u == null) throw new RuntimeException("not found");
        return u.name();
    }
}
22

Tests JUnit

Test de base

JUnit 5 utilise @Test de org.junit.jupiter.api. assertEquals vérifie que expected equals actual. Autres assertions : assertTrue, assertThrows, assertAll.

java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalcTest {
    @Test
    void testAdd() {
        assertEquals(5, calc.add(2, 3));
    }
}

Tests paramétrés

Les tests paramétrés exécutent le même test avec différentes entrées. @ValueSource fournit des arguments uniques. @CsvSource fournit de multiples arguments. Réduit la duplication des tests.

java
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4})
void testPositive(int n) { assertTrue(n > 0); }

@ParameterizedTest
@CsvSource({"1,2,3", "4,5,9"})
void testAdd(int a, int b, int expected) {
    assertEquals(expected, calc.add(a, b));
}

Méthodes de lifecycle

@BeforeAll/@AfterAll s'exécutent une fois par classe (doivent être statiques). @BeforeEach/@AfterEach s'exécutent autour de chaque test. Utilisez-les pour les connexions de base de données et le setup des mocks.

java
class DbTest {
    @BeforeAll static void setupAll() { /* once before all */ }
    @AfterAll static void tearDownAll() { /* once after all */ }
    @BeforeEach void setup() { /* before each test */ }
    @AfterEach void tearDown() { /* after each test */ }
}

Assertions

assertAll exécute toutes les assertions même si certaines échouent. assertThrows vérifie que le code lève une exception spécifique. Utilisez assertTimeout pour les tests bornés dans le temps.

java
@Test
void testAll() {
    assertAll("person",
        () -> assertEquals("Alice", p.getName()),
        () -> assertEquals(30, p.getAge())
    );
}
@Test
void testException() {
    assertThrows(IllegalArgumentException.class, () -> service.process(-1));
}

Mockito

@Mock crée des objets mock, @InjectMocks les injecte. when().thenReturn() stubbe les appels. verify() vérifie qu'une méthode a été appelée. Mockito est le framework de mocking standard.

java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock UserRepository repo;
    @InjectMocks UserService service;
    @Test
    void testFind() {
        when(repo.findById(1)).thenReturn(Optional.of(new User("Alice")));
        assertEquals("Alice", service.findUser(1).getName());
        verify(repo).findById(1);
    }
}
23

Maven/Gradle

POM Maven

Maven utilise pom.xml. groupId/artifactId/version identifient le projet. Les dépendances ont un scope (compile, test, provided). Maven impose une structure de répertoires standard.

java
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>myapp</artifactId>
  <version>1.0.0</version>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.10.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Commandes Maven

Lifecycle Maven : clean, compile, test, package, install, deploy. Chaque phase exécute les phases précédentes. Utilisez -DskipTests pour sauter les tests.

java
mvn clean          # Remove target/
mvn compile        # Compile sources
mvn test           # Run tests
mvn package        # Build JAR
mvn install        # Install to local repo
mvn dependency:tree  # Show dependency tree

Build Gradle

Gradle utilise build.gradle (Groovy) ou build.gradle.kts (Kotlin). implementation pour les dépendances de compilation, testImplementation pour les tests. Gradle est plus rapide que Maven.

java
plugins { id 'java'; id 'application' }
repositories { mavenCentral() }
dependencies {
    implementation 'com.google.guava:guava:32.1.3-jre'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
application { mainClass = 'com.example.App' }

Commandes Gradle

Tâches Gradle : build, test, run, clean. Le wrapper (gradlew) garantit des versions cohérentes. Utilisez --parallel pour les builds de modules parallèles.

java
gradle clean       # Clean build
gradle build       # Build + test
gradle test        # Run tests
gradle run         # Run application
gradle bootRun     # Run Spring Boot
gradle dependencies  # Show dependency tree

Projet multi-module

Les projets multi-module divisent les grandes applications. settings.gradle liste les modules. project(:core) crée des dépendances inter-modules. Chaque module a son propre build.gradle.

java
// settings.gradle
include 'core', 'web', 'api'
// build.gradle (root)
subprojects {
    apply plugin: 'java'
    repositories { mavenCentral() }
}
// In web/build.gradle
dependencies { implementation project(':core') }
24

Bases de Spring

Application Spring Boot

@SpringBootApplication active l'auto-configuration, le scan de composants et la configuration. SpringApplication.run démarre le serveur embarqué. Élimine la configuration XML.

java
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

Contrôleur REST

@RestController combine @Controller et @ResponseBody. @GetMapping, @PostMapping sont des raccourcis. @PathVariable extrait les paramètres d'URL, @RequestBody lie le JSON.

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    @PostMapping
    public User create(@RequestBody User user) {
        return userService.save(user);
    }
}

Injection de dépendances

@Autowired injecte les dépendances. L'injection par constructeur est recommandée (testable, immuable). @Service, @Repository, @Component sont des stéréotypes pour l'injection.

java
@Service
public class UserService {
    private final UserRepository repo;
    @Autowired  // Constructor injection (recommended)
    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}

Configuration

@Configuration marque les classes de config. @Bean déclare des beans gérés par Spring. @Primary rend un bean préféré. Utilisez pour les classes tierces.

java
@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    @Bean @Primary
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

Propriétés d'application

application.properties configure Spring Boot. Les profils activent la configuration spécifique à l'environnement. Activez avec spring.profiles.active=dev. Utilisez @Value ou @ConfigurationProperties.

java
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost/mydb
spring.jpa.hibernate.ddl-auto=update
# Profile-specific
# application-dev.properties
server.port=9090
25

Approfondissement JDBC

Connexion & Statement

DriverManager.getConnection établit une connexion. Statement exécute du SQL statique. ResultSet itère les résultats. Fermez toujours les ressources ou utilisez try-with-resources.

java
Connection conn = DriverManager.getConnection(url, user, pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
    System.out.println(rs.getString("name"));
}
rs.close(); stmt.close(); conn.close();

PreparedStatement

PreparedStatement prévient l'injection SQL en paramétrant les requêtes. Définissez les valeurs par index (basé sur 1). try-with-resources ferme automatiquement. Améliore la performance via la pré-compilation.

java
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, "Alice");
    ps.setString(2, "[email protected]");
    ps.executeUpdate();
}

Gestion des transactions

setAutoCommit(false) démarre une transaction. commit persiste, rollback annule. Si une instruction échoue, rollback pour maintenir l'intégrité. @Transactional gère cela dans Spring.

java
conn.setAutoCommit(false);
try {
    stmt.executeUpdate("UPDATE accounts SET bal = bal - 100 WHERE id = 1");
    stmt.executeUpdate("UPDATE accounts SET bal = bal + 100 WHERE id = 2");
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
}

Pooling de connexions

Le pooling de connexions réutilise les connexions. HikariCP est le pool le plus rapide. maximumPoolSize limite les connexions concurrentes. Fermez toujours (retour au pool). Spring Boot configure HikariCP automatiquement.

java
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(10);
HikariDataSource ds = new HikariDataSource(config);
Connection conn = ds.getConnection();

Métadonnées ResultSet

ResultSetMetaData décrit la structure des résultats : noms de colonnes, types, propriétés. Utile pour l'accès générique aux données. Les indices de colonne sont basés sur 1.

java
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
for (int i = 1; i <= cols; i++)
    System.out.println(meta.getColumnName(i) + ": " + meta.getColumnTypeName(i));
26

Utilitaires de concurrence

ExecutorService

ExecutorService gère les pools de threads. submit renvoie un Future pour des résultats async. get bloque jusqu'à la fin (avec timeout). Fermez toujours l'exécuteur.

java
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(() -> {
    Thread.sleep(1000);
    return "Result";
});
String result = future.get(5, TimeUnit.SECONDS);
executor.shutdown();

CompletableFuture

CompletableFuture permet la programmation async fonctionnelle. supplyAsync s'exécute dans le ForkJoinPool. thenApply transforme, thenAccept consomme, exceptionally gère les erreurs.

java
CompletableFuture.supplyAsync(() -> fetchData())
    .thenApply(data -> process(data))
    .thenAccept(result -> System.out.println(result))
    .exceptionally(ex -> { ex.printStackTrace(); return null; });

Collections concurrentes

ConcurrentHashMap est thread-safe sans verrouillage complet. CopyOnWriteArrayList copie à l'écriture (lecture-intensive). BlockingQueue supporte les motifs producteur-consommateur.

java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("b", k -> k.length());
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);

CountDownLatch & CyclicBarrier

CountDownLatch attend N threads (usage unique). CyclicBarrier attend N threads puis se réinitialise (réutilisable). Utilisez latch pour le démarrage, barrier pour le calcul par phases.

java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++)
    new Thread(() -> { work(); latch.countDown(); }).start();
latch.await();  // Wait for all

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All ready"));

Variables atomiques

Les variables atomiques fournissent des opérations thread-safe sans verrou. compareAndSet permet le verrouillage optimiste. LongAdder est plus rapide qu'AtomicLong pour les compteurs à forte contention.

java
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
LongAdder adder = new LongAdder();
adder.increment();
27

Internes JVM

Zones mémoire

Mémoire JVM : Heap (objets, géré par GC), Stack (appels de méthode, par thread), Metaspace (métadonnées de classe). Young Gen utilise le GC par copie, Old Gen utilise mark-sweep-compact.

java
// Heap: objects and arrays (shared)
// - Young Gen: Eden, S0, S1
// - Old Gen: long-lived objects
// Stack: method frames (per thread)
// Metaspace: class metadata (Java 8+)
// JVM flags: -Xms512m -Xmx2g

Chargement de classes

Le chargement de classes est paresseux. Bootstrap charge le Java core, Extension charge les extensions, Application charge le classpath. Les initialiseurs statiques s'exécutent une fois. Les classloaders personnalisés permettent le hot-reload.

java
// Bootstrap -> Extension -> Application classloaders
class MyClass {
    static { System.out.println("Static init"); }
}
// Class.forName("MyClass") triggers loading
// -verbose:class shows class loading

Bytecode

Java compile en bytecode (basé sur une pile). javap -c désassemble les fichiers de classe. Chaque instruction pousse/dépile la pile d'opérandes. Les agents Java peuvent modifier le bytecode au chargement.

java
// javap -c MyClass.class
// Method int add(int, int):
//   iload_1      // Load local var 1
//   iload_2      // Load local var 2
//   iadd         // Add
//   ireturn      // Return int

Compilation JIT

JIT compile le bytecode fréquemment exécuté en code natif. La compilation à niveaux équilibre le démarrage et la performance de pointe. Les méthodes chaudes sont inlinées et optimisées.

java
// JIT compiles hot methods to native code
// -XX:+PrintCompilation  // Show JIT activity
// -XX:CompileThreshold=10000  // Method call count
// Tiered: Interpreter -> C1 -> C2

Dump de threads

Les dumps de threads montrent tous les états de threads et les traces de pile. Essentiel pour déboguer les deadlocks et les blocages. jstack est l'outil en ligne de commande. Cherchez les threads BLOCKED et WAITING.

java
// Get thread dump
jstack <pid>
// Or: kill -3 <pid>
// Deadlock detection
jstack -l <pid> | grep -A 20 "Found deadlock"
28

Garbage Collection

Algorithmes GC

Serial GC pour les petites applications. Parallel GC maximise le débit. G1 GC équilibre débit et latence (par défaut). ZGC fournit des pauses sub-milliseconde pour les grands tas.

java
# Serial GC (single-threaded)
-XX:+UseSerialGC
# Parallel GC (throughput)
-XX:+UseParallelGC
# G1 GC (balanced, default in Java 9+)
-XX:+UseG1GC
# ZGC (low-latency)
-XX:+UseZGC

Réglage G1 GC

G1 divise le tas en régions. MaxGCPauseMillis fixe un objectif de pause souple. G1 priorise les régions avec le plus de déchets. Utilisez GCViewer ou GCEasy pour analyser les logs.

java
# Set heap
-Xms4g -Xmx4g
# Max GC pause target
-XX:MaxGCPauseMillis=200
# Region size (1-32MB)
-XX:G1HeapRegionSize=16m
# Enable GC logging
-Xlog:gc*:file=gc.log:time,uptime

Fuites mémoire

Les fuites mémoire sont causées par la rétention non intentionnelle d'objets. Les collections statiques, les ressources non fermées et les enregistrements de listeners sont courants. jmap montre les comptes d'objets. Analysez hprof avec MAT.

java
// Common leak: static collections
static Map<String, Object> cache = new HashMap<>();
// Objects never removed -> leak
// Detect with:
jmap -histo <pid> | head -20
// Heap dump:
jmap -dump:format=b,file=heap.hprof <pid>

Références faibles

WeakReference permet le GC quand aucune référence forte n'existe. SoftReference survit jusqu'à la pression mémoire. Les clés WeakHashMap sont faibles. Utilisez pour les caches qui ne devraient pas empêcher le GC.

java
WeakReference<Object> weakRef = new WeakReference<>(new Object());
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024]);
WeakHashMap<Object, String> map = new WeakHashMap<>();
map.put(key, "value");  // Entry removed when key is GC'd

Finalisation

finalize() est déprécié (imprévisible, lent). L'API Cleaner (Java 9+) fournit un meilleur nettoyage. try-with-resources est préféré pour un nettoyage déterministe.

java
// Cleaner API (Java 9+)
class Resource implements AutoCloseable {
    private final Cleaner.Cleanable cleanable;
    Resource() {
        cleanable = Cleaner.create().register(this, () -> cleanup());
    }
    public void close() { cleanable.clean(); }
}
29

Collectors de Stream

Grouping By

groupingBy partitionne les éléments par un classifieur. Le deuxième argument est un collector en aval pour l'agrégation. counting, averaging, summing sont des collectors en aval courants.

java
Map<String, List<Person>> byCity =
    people.stream().collect(Collectors.groupingBy(Person::getCity));
Map<String, Long> countByCity =
    people.stream().collect(Collectors.groupingBy(
        Person::getCity, Collectors.counting()));

Partitioning

partitioningBy sépare en deux groupes (true/false). Plus efficace que groupingBy pour les clés booléennes. Le résultat a toujours les deux clés. Les collectors en aval agrègent chaque partition.

java
Map<Boolean, List<Person>> partition =
    people.stream().collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
// {false=[minors], true=[adults]}
Map<Boolean, Long> count =
    people.stream().collect(Collectors.partitioningBy(
        p -> p.getAge() >= 18, Collectors.counting()));

Joining

joining concatène des chaînes avec délimiteur, préfixe et suffixe optionnels. Les éléments doivent être des chaînes ; utilisez map d'abord. Utilise StringBuilder en interne.

java
String names = people.stream()
    .map(Person::getName)
    .collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
String csv = people.stream()
    .map(p -> p.getName() + "=" + p.getAge())
    .collect(Collectors.joining("\n", "[", "]"));

Reducing

reducing effectue une opération de fold. La version à trois arguments prend identité, mapper, reducer. La version à deux arguments renvoie Optional. Utilisez quand les collectors standard sont insuffisants.

java
int totalAge = people.stream()
    .collect(Collectors.reducing(0, Person::getAge, Integer::sum));
Optional<Person> oldest = people.stream()
    .collect(Collectors.reducing((p1, p2) ->
        p1.getAge() > p2.getAge() ? p1 : p2));

Collector personnalisé

Collector.of crée des collectors personnalisés : supplier, accumulator, combiner, finisher. Le combiner fusionne les résultats partiels pour les streams parallèles. Utile pour des formats de sortie spécialisés.

java
Collector<Person, ?, String> toJson = Collector.of(
    StringBuilder::new,
    (sb, p) -> sb.append(`{"name":"${p.getName()}"}`),
    StringBuilder::append,
    StringBuilder::toString
);
30

Pièges courants

Cache d'Integer

Java met en cache les valeurs Integer de -128 à 127. == compare les références, pas les valeurs. Pour Integer hors de la plage de cache, == renvoie false. Utilisez toujours .equals() pour Integer.

java
Integer a = 127; Integer b = 127;
System.out.println(a == b);  // true (cached)
Integer c = 128; Integer d = 128;
System.out.println(c == d);  // false (not cached)
System.out.println(c.equals(d));  // true

Immuabilité des chaînes

Les chaînes sont immuables : des méthodes comme concat renvoient de nouvelles chaînes. Oublier d'assigner le résultat est un bug courant. Utilisez StringBuilder pour la concaténation répétée.

java
String s = "Hello";
s.concat(" World");  // Returns new string, s unchanged
System.out.println(s);  // "Hello"
// Use StringBuilder for mutation
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");  // Mutates in place

Equals & HashCode

Des objets égaux doivent avoir des codes de hachage égaux. HashMap et HashSet utilisent hashCode pour le bucketing et equals pour la comparaison. Si vous redéfinissez equals, vous DEVEZ redéfinir hashCode.

java
class Person {
    String name;
    public boolean equals(Object o) {
        if (!(o instanceof Person)) return false;
        return name.equals(((Person)o).name);
    }
    public int hashCode() { return name.hashCode(); }
}

Checked vs Unchecked

Les exceptions checked doivent être déclarées ou capturées (IOException, SQLException). Les unchecked (RuntimeException) ne nécessitent pas de gestion. Évitez de capturer un Exception large.

java
// Checked: must catch or declare
try { Thread.sleep(1000); }
catch (InterruptedException e) { /* handle */ }
// Unchecked: RuntimeException
throw new IllegalArgumentException("bad input");

Fuites de ressources

Les ressources doivent être fermées. try-with-resources (Java 7+) ferme automatiquement les AutoCloseable. Sans cela, les exceptions causent des fuites. Ne laissez jamais une ressource non fermée.

java
// BAD: resource leak
FileInputStream fis = new FileInputStream("file.txt");
// If exception here, fis never closed
// GOOD: try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // Use resource
}  // Auto-closed even on exception
31

Patrons de conception

Singleton

Le double-checked locking avec volatile assure une initialisation paresseuse thread-safe. volatile empêche le réordonnancement d'instructions. L'enum singleton est plus simple : public enum Singleton { INSTANCE; }.

java
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();
            }
        }
        return instance;
    }
}

Builder

Le patron Builder gère les objets avec de nombreux paramètres optionnels. Évite les constructeurs télescopiques. Le Builder interne collecte les paramètres de manière fluide. build() crée l'objet immuable.

java
public class Pizza {
    private final String size;
    private final boolean cheese;
    private Pizza(Builder b) { size = b.size; cheese = b.cheese; }
    public static class Builder {
        private String size; private boolean cheese;
        public Builder size(String s) { size = s; return this; }
        public Builder cheese(boolean c) { cheese = c; return this; }
        public Pizza build() { return new Pizza(this); }
    }
}

Strategy

Le patron Strategy encapsule des algorithmes interchangeables. Le contexte délègue à l'interface strategy. Évite les grandes chaînes if-else. Suit le principe ouvert-fermé.

java
interface PaymentStrategy { void pay(double amount); }
class CreditCard implements PaymentStrategy {
    public void pay(double amount) { System.out.println("Card: " + amount); }
}
class Cart { private PaymentStrategy strategy; void checkout() { strategy.pay(100); } }

Observer

Le patron Observer définit une dépendance un-à-plusieurs. Quand le sujet change, tous les observateurs sont notifiés. Utilisé dans MVC, les systèmes d'événements et la programmation réactive.

java
interface Observer { void update(String event); }
class Subject {
    private List<Observer> observers = new ArrayList<>();
    void subscribe(Observer o) { observers.add(o); }
    void notify(String event) { observers.forEach(o -> o.update(event)); }
}

Factory Method

Le Factory Method définit une interface pour créer des objets mais laisse les sous-classes décider quelle classe instancier. Découple le code client des classes concrètes.

java
abstract class Document { abstract void open(); }
class PDF extends Document { void open() { /* ... */ } }
abstract class DocFactory { abstract Document create(); }
class PDFFactory extends DocFactory { Document create() { return new PDF(); } }

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.