Module 1 / 8 · 90 min
Valeurs, promotions, texte et contrôle de flux
Séparer types de compilation, valeurs et chemins exécutés.
Comprendre le mécanisme
Java détermine les types à la compilation. var infère un type local ; il ne rend pas la variable dynamiquement typée. Les opérations sur byte et short peuvent promouvoir les opérandes en int. Un cast explicite peut perdre de l’information. Pour des objets, == compare généralement les références ; equals porte le contrat d’égalité de la classe. String est immuable alors que StringBuilder accumule des modifications.
Raisonner et vérifier
Tracez séparément la sélection d’une branche et la valeur calculée. Un switch expression doit fournir un résultat sur chaque chemin possible ; yield fournit la valeur d’un bloc. Un wrapper null peut provoquer une NullPointerException à l’unboxing. Les tests doivent inclure bornes numériques, chaîne vide, null et chemin par défaut, pas seulement un exemple nominal.
Deux familles de valeurs
Les primitifs représentent directement des valeurs comme un entier ou un booléen ; les références désignent des objets ou null. Un Integer et un int ne se comportent donc pas identiquement face à l’absence. Avant une opération, repérez la famille de chaque opérande et les conversions possibles. Cette lecture évite d’attribuer à une valeur nullable les garanties d’un primitif simplement parce que leurs noms se ressemblent.
Erreur fréquente à comprendre Tout int est une référence nullable.
Un primitif ne peut pas porter null.
Inférer sans rendre dynamique
var laisse le compilateur déterminer le type d’une variable locale à partir de son initialisation. Une fois établi, ce type ne change pas à chaque affectation. Si le résultat initial est un entier, lui affecter ensuite une chaîne ne devient pas valide grâce au mot-clé. Utilisez l’inférence lorsque le type demeure lisible et vérifiez séparément le type de l’expression initiale, notamment après une promotion numérique.
Erreur fréquente à comprendre var rend toute variable dynamiquement retypable.
Le type inféré reste un type statique.
Affecter avant de consulter
Une variable locale doit être définitivement assignée sur le chemin où elle est lue. Les valeurs initiales prévues pour certains champs ne doivent pas être généralisées aux locales. Tracez les branches : si une condition laisse la variable sans affectation, la lecture n’a pas de contrat sûr. Ajouter un zéro arbitraire peut faire compiler le code mais masquer l’absence d’une décision métier nécessaire.
Erreur fréquente à comprendre Toute locale reçoit automatiquement zéro.
Les règles d’initialisation locale imposent une affectation.
Le déballage exige une valeur présente
Une opération sur un wrapper peut demander son unboxing vers un primitif. Si la référence vaut null, cette conversion peut lancer une NullPointerException avant le calcul attendu. Définissez si l’absence est une erreur ou un état normal, puis placez une garde adaptée. Un test avec une référence non nulle ne couvre pas ce chemin, même si toutes les valeurs numériques nominales ont été essayées.
Erreur fréquente à comprendre Déballer null produit toujours zéro.
La conversion nécessite une valeur emballée non nulle.
Promotion et division entière
Les opérations sur des types numériques étroits peuvent promouvoir les opérandes, par exemple byte plus byte vers int. Avec deux int, une division entière ne conserve pas la fraction : cinq divisé par deux produit deux. var infère le type de ce résultat, sans changer l’opération réalisée. Pour une division décimale, choisissez explicitement une représentation compatible avec la précision attendue et vérifiez les règles d’arrondi utiles.
Erreur fréquente à comprendre 5 divisé par 2 en int produit 2,5.
Le résultat entier ne conserve pas la fraction.
Conversions et perte de précision
Un cast peut réduire une plage ou une précision ; il ne prouve pas que la valeur initiale était admissible. Avant de convertir une donnée d’API, vérifiez son format et les bornes du type cible. Pour un identifiant qui doit garder tous ses chiffres sans calcul, une représentation textuelle peut être plus appropriée qu’un nombre approximatif. La conversion doit être une étape du contrat, pas une réparation aveugle d’entrée invalide.
Erreur fréquente à comprendre Tout cast numérique préserve exactement toute valeur.
Les plages et précisions diffèrent.
Identité, contenu et texte mutable
Pour des références, == répond à une question d’identité, tandis que equals applique le contrat de la classe. Deux chaînes de même contenu ne doivent pas être comparées par une hypothèse générale sur leur partage en mémoire. String est immuable ; StringBuilder offre des opérations de modification de son contenu. Distinguez donc égalité de texte, identité d’objet et mutabilité avant de déduire ce qu’une affectation ou concaténation préserve.
Erreur fréquente à comprendre Deux String de même contenu sont toujours la même référence.
L’identité d’objet et la valeur ne sont pas équivalentes.
Suivre tous les chemins du switch
Un switch doit respecter le type du sélecteur et les règles des cas de la version ciblée. Une expression switch doit fournir une valeur selon son contrat ; un bloc de branche peut utiliser yield pour la produire. Listez les états attendus et la politique pour une entrée inconnue ou null. Le nom d’un état reçu du réseau n’est pas encore une valeur validée de votre modèle.
Erreur fréquente à comprendre Tout switch accepte toute valeur sans règles de compatibilité.
Le langage vérifie le contrat du sélecteur et des branches.
Le type ne définit pas le domaine
Un int représente une plage de valeurs machine, pas une quantité positive ou un âge autorisé. Après reconnaissance du type et conversion contrôlée, appliquez les bornes et relations du domaine. Un zéro peut être une valeur métier distincte d’une absence. Les tests doivent donc séparer ces catégories, puis vérifier que chaque refus intervient avant une mutation ou un appel externe.
Erreur fréquente à comprendre Un entier représente nécessairement un âge autorisé.
La plage métier doit être validée.
Respecter l’ordre et les effets
Une expression peut consulter et modifier plusieurs états. La réordonner pour la rendre plus compacte peut changer les valeurs observées, même si les opérateurs semblent arithmétiques. Décomposez les étapes lorsque des effets se combinent et notez l’état après chaque affectation. Cette méthode sert aussi à analyser un QCM : distinguez l’ordre d’évaluation, le type obtenu et la valeur calculée au lieu de deviner à partir de l’affichage.
Erreur fréquente à comprendre Toute expression Java est librement réordonnable sans conséquence.
L’ordre d’évaluation et les effets doivent être respectés.
Atelier guidé : lire, expliquer et vérifier le code
class Main {
public static void main(String[] args) {
var result = 5 / 2;
System.out.print(result);
}
}
Le compilateur voit deux littéraux entiers dans la division. L’opération produit donc un entier dont la partie fractionnaire est écartée selon les règles de division entière. La variable déclarée avec var reçoit ce type inféré et le programme affiche 2. L’inférence ne transforme pas rétrospectivement la division en calcul flottant.
Modifiez un opérande pour obtenir une division avec un type décimal, puis comparez le type et l’affichage. Reprenez ensuite l’addition de deux byte et vérifiez que le type de l’expression est int. Enfin, essayez une réaffectation textuelle à result dans un fichier de laboratoire : le refus de compilation démontre que var n’est pas un mécanisme de typage dynamique.
Exemple commenté
byte a = 10;
byte b = 20;
int total = a + b;
var label = "total=" + total;
System.out.println(label);
L’addition produit un int, puis la concaténation construit une chaîne. L’inférence de label n’autorise pas une réaffectation ultérieure à un nombre.
Exercice de mise en pratique
Écrivez un switch expression qui transforme trois états de réservation en libellés et traite explicitement une entrée inconnue.
Voir une proposition de correction
Utilisez un enum et un switch exhaustif. Comparez avec une entrée texte validée avant conversion. Ajoutez des tests pour chaque branche et pour null selon le contrat choisi.
- Repérer promotion et unboxing.
- Distinguer identité et égalité.
- Analyser tous les chemins du switch.
Contrôle de connaissances
Quel est le type de a + b lorsque a et b sont byte ?
Réponse expliquée : int — La promotion numérique produit int.
Cours associé : Valeurs, promotions, texte et contrôle de flux ↗
Documentation de référence ↗ https://docs.oracle.com/javase/specs/jls/se21/html/jls-5.html
Module 2 / 8 · 90 min
Objets, héritage, records et interfaces
Expliquer surcharge, redéfinition et immutabilité des objets.
Comprendre le mécanisme
La surcharge est résolue à partir des signatures et types connus à la compilation. Une méthode d’instance redéfinie est choisie selon l’objet réel à l’exécution. Les champs et méthodes static ne suivent pas ce même dispatch polymorphe. Un record fournit des composants, accesseurs et méthodes de valeur ; il n’effectue pas une copie profonde des objets reçus. Une hiérarchie sealed restreint ses sous-types directs autorisés.
Raisonner et vérifier
Protégez un objet de valeur contre les collections mutables reçues au constructeur. Les interfaces peuvent porter des méthodes default, mais un conflit doit être résolu explicitement. Un enum exprime un ensemble de cas ; une annotation comme Override fait vérifier une intention par le compilateur. La finalité d’un champ empêche sa réaffectation, sans bloquer les mutations internes de l’objet référencé.
Surcharge et redéfinition répondent à deux questions
Une surcharge propose plusieurs signatures ; la sélection dépend notamment des types connus à la compilation. Une redéfinition fournit une autre implémentation dans une hiérarchie compatible. Commencez par identifier quelle méthode est sélectionnable, puis examinez le receveur réel pour le dispatch d’instance. Deux méthodes partageant un nom ne suffisent pas à conclure qu’un appel sera décidé dynamiquement de la même manière.
Erreur fréquente à comprendre Toute méthode de même nom est une redéfinition.
Les signatures et la hiérarchie déterminent le mécanisme.
Le receveur réel choisit l’implémentation d’instance
Une variable de type parent peut référencer un objet enfant. Lorsqu’une méthode d’instance est correctement redéfinie, l’appel utilise l’implémentation correspondant à l’objet réel. Cette propriété permet une substitution utile pour des stratégies ou transports. Ne la généralisez pas sans distinction à tous les membres et mécanismes du langage : la sélection d’une surcharge et l’accès à certains membres suivent d’autres règles.
Erreur fréquente à comprendre Le type statique choisit toujours seul toute implémentation dynamique.
Le dispatch d’instance utilise le receveur réel.
Records et copie défensive
Un record fournit un modèle de composants et des comportements associés à sa valeur. Une référence de composant finale ne fige pas automatiquement les éléments d’une collection mutable. Si une liste reçue doit rester stable, une copie défensive du conteneur peut empêcher une modification extérieure de sa structure. Examinez encore les objets de cette liste : une copie superficielle ne crée pas leur immutabilité profonde.
Erreur fréquente à comprendre Un record rend tout graphe référencé profondément immuable.
Les composants peuvent référencer des objets mutables.
Borner les extensions avec sealed
Une hiérarchie sealed rend explicites des extensions permises selon les règles du langage. Ce choix facilite un raisonnement sur les variantes connues et certaines analyses exhaustives. Il ne remplace pas les invariants des constructeurs ni une politique d’accès métier. Lorsqu’un nouveau cas est introduit, recherchez les consommateurs qui supposaient la liste précédente et vérifiez les branches de traitement et les règles de compatibilité.
Erreur fréquente à comprendre sealed autorise n’importe quelle sous-classe sans contrainte.
Les extensions admises sont contrôlées.
Contrat d’interface et capacités limitées
Une interface décrit les opérations qu’un client peut demander à une implémentation. Cela aide à remplacer un service par un double de test sans coupler le consommateur à toutes les possibilités du service réel. L’interface ne donne pas un droit utilisateur sur les données. Dans une revue, identifiez les méthodes nécessaires puis vérifiez séparément les permissions appliquées à l’intérieur du parcours métier.
Erreur fréquente à comprendre Une interface accorde automatiquement une permission métier.
Le contrat de code ne décide pas des droits d’un acteur.
Final stabilise une extension, pas toute la sécurité
Une méthode final ne peut pas être redéfinie selon les règles du langage. Cela peut stabiliser une séquence choisie par la conception, mais ne prouve pas que le corps vérifie correctement les droits ou gère la concurrence. Écrivez les garanties attendues de cette séquence et testez-les. Le mot-clé ne doit pas devenir une justification qui remplace l’observation du comportement.
Erreur fréquente à comprendre final donne un pouvoir d’administration à la méthode.
Le mot-clé concerne le langage, pas l’accès métier.
Établir les invariants dès la construction
Le constructeur doit refuser ou représenter explicitement les états incomplets. Si un objet exige une quantité positive et une devise, une construction sans ces données oblige les méthodes à deviner l’intention. Pensez aussi aux références reçues : une collection mutable conservée directement peut être modifiée plus tard par son propriétaire initial. Une copie ou une abstraction de valeur doit suivre le besoin réel de stabilité.
Erreur fréquente à comprendre Un nom de classe suffit à garantir tout état métier.
Le code doit vérifier les conditions du domaine.
Encapsulation et frontière de code
La visibilité limite les accès permis entre éléments de code. private ne chiffre pas le contenu d’un champ et ne remplace pas une autorisation sur un compte métier. Un accesseur public peut aussi exposer indirectement un objet mutable qui contourne une intention d’encapsulation. Examinez ce qui est retourné et qui peut ensuite le modifier, pas seulement le mot-clé placé sur le champ.
Erreur fréquente à comprendre private chiffre automatiquement les champs.
Le contrôle de visibilité n’est pas un chiffrement.
Deux références peuvent désigner le même état
Affecter une référence d’objet ne duplique pas récursivement l’objet et ses dépendances. Une mutation à travers une variable peut donc être visible depuis l’autre. Pour une donnée dont le destinataire doit disposer indépendamment, définissez une stratégie de copie ou d’immuabilité. Testez un objet imbriqué : une copie du conteneur peut laisser ses éléments partagés et conserver exactement le couplage que vous vouliez éviter.
Erreur fréquente à comprendre Affecter un objet duplique récursivement toutes ses dépendances.
Deux variables peuvent référencer le même objet.
Égalité cohérente et collections de hachage
equals et hashCode doivent respecter un contrat cohérent pour les usages de hachage : deux objets égaux doivent produire le même hashCode. Un champ mutable participant à ces calculs peut rendre une clé difficile à retrouver après insertion dans une Map. Identifiez les composants stables de l’identité avant de choisir les champs. Le fait qu’une classe compile n’établit pas que sa notion de valeur convient au domaine.
Erreur fréquente à comprendre Deux objets égaux doivent pouvoir avoir des hashCode différents sans contrat.
Les collections de hachage exigent cette cohérence.
Atelier guidé : lire, expliquer et vérifier le code
class Parent { String label() { return "parent"; } }
class Child extends Parent { @Override String label() { return "child"; } }
class Main {
public static void main(String[] args) {
Parent value = new Child();
System.out.print(value.label());
}
}
La variable value annonce le type Parent, mais l’objet construit est un Child. label est une méthode d’instance redéfinie, et le dispatch utilise donc l’implémentation enfant. Le texte affiché est child. Il faut distinguer cette décision de la sélection d’une surcharge, qui ne serait pas justifiée par le seul type réel du receveur.
Ajoutez un second objet Parent pour comparer les deux appels dans le même programme. Puis remplacez le résultat textuel par un collaborateur qui enregistre les appels afin d’en faire un test de substitution. Si une implémentation enfant exige des préconditions supplémentaires qui cassent le consommateur, le polymorphisme syntaxique ne suffira pas à rendre le contrat métier correct.
Spring : proxy transactionnel et auto-appel
@Service
public class CheckoutService {
public void checkout() { this.persist(); }
@Transactional
public void persist() { /* écritures SQL */ }
}
Dans le mode proxy standard, l’interception transactionnelle s’applique aux appels qui traversent le proxy. Lorsque checkout appelle directement persist sur le même objet, cet auto-appel ne passe pas par cette frontière. L’annotation de persist ne crée donc pas une transaction pour ce chemin dans l’hypothèse où aucune transaction n’est déjà ouverte.
Placez l’annotation sur la frontière publique appelée via le proxy si elle correspond à l’unité métier, ou extrayez le travail transactionnel dans un autre bean injecté et appelé normalement. Ne déduisez pas une transaction du seul emplacement visuel de l’annotation.
Un test d’intégration doit invoquer le bean géré par Spring depuis l’extérieur, observer le comportement transactionnel et vérifier un rollback attendu. Instancier la classe avec new contourne aussi les mécanismes du conteneur. Les modes de weaving ou une transaction préexistante changeraient le contexte ; ils ne font pas partie du cas posé et doivent être annoncés lorsqu’ils sont utilisés.
Points de vigilance : Un appel interne au même objet ne traverse pas le proxy transactionnel. L’auto-appel contourne l’interception. Annoter la frontière appelée via le proxy ou extraire un autre bean est une solution. L’annotation ignorée ne crée pas de transactions par instruction ; l’autocommit relève de la connexion. final ne corrige pas le contournement du proxy et peut empêcher certaines interceptions.
Documentation de référence ↗
Exemple commenté
record Batch(java.util.List<String> ids) {
Batch { ids = java.util.List.copyOf(ids); }
}
var input = new java.util.ArrayList<String>();
input.add("R1");
var batch = new Batch(input);
input.clear();
System.out.println(batch.ids().size());
La copie non modifiable protège ici la liste contre un clear sur la collection initiale ; le résultat reste 1. Des éléments eux-mêmes mutables nécessiteraient un raisonnement supplémentaire.
Exercice de mise en pratique
Créez une interface Reservation et deux implémentations ; appelez la même méthode via une référence d’interface.
Voir une proposition de correction
Définissez le contrat, redéfinissez une méthode d’instance et vérifiez le dispatch. Ajoutez un record qui copie défensivement ses collections. Testez la mutation du conteneur d’origine.
- Distinguer surcharge et redéfinition.
- Inspecter les références mutables des records.
- Utiliser Override pour vérifier l’intention.
Contrôle de connaissances
Un record contenant une List mutable est-il profondément immuable par défaut ?
Réponse expliquée : Non, la référence finale ne fige pas les éléments — Le constructeur doit protéger les composants mutables.
Cours associé : Objets, héritage, records et interfaces ↗
Documentation de référence ↗ https://docs.oracle.com/en/java/javase/21/language/records.html
Module 3 / 8 · 90 min
Tableaux, génériques et collections
Choisir le conteneur et comprendre les bornes de types.
Comprendre le mécanisme
List conserve une séquence, Set impose un contrat d’unicité et Map associe des clés à des valeurs. Un tableau connaît son type d’élément à l’exécution ; sa covariance peut conduire à ArrayStoreException. Les génériques sont invariants : une List<Integer> n’est pas une List<Number>. Une borne extends convient à la lecture d’un type producteur ; une borne super sert souvent à recevoir des éléments d’un type consommé.
Raisonner et vérifier
Les collections de fabrique comme List.of ne permettent pas les modifications structurelles et refusent null. Elles ne rendent pas leurs éléments profondément immuables. Un Comparator doit respecter un ordre cohérent ; préférez les comparateurs dédiés aux soustractions pouvant déborder. Java 21 fournit des interfaces séquencées pour raisonner sur un ordre de rencontre explicite.
Exprimer les relations entre types
Un paramètre générique relie les types d’entrée, de sortie ou d’éléments d’un contrat. Il permet au compilateur de détecter certaines incompatibilités sans attendre le parcours réel. Cette précision ne valide pas toute donnée métier : une liste de réservations peut encore contenir des objets qui n’appartiennent pas à l’acteur courant. Gardez séparées la structure de la collection et la règle de périmètre appliquée à ses éléments.
Erreur fréquente à comprendre Les génériques valident toute donnée métier à l’exécution.
Le typage ne prouve pas les invariants du domaine.
Pourquoi les listes génériques sont invariantes
Si une List<Integer> devenait librement une List<Number> mutable, un consommateur pourrait y ajouter un autre type de Number incompatible avec la liste initiale. L’invariance empêche ce raccourci. Pour partager une vue compatible avec un usage particulier, exprimez une borne adaptée plutôt qu’un cast qui retire la vérification. Commencez par demander si le consommateur doit lire, ajouter ou faire les deux.
Erreur fréquente à comprendre Toute liste d’entiers peut être librement traitée comme liste mutable de Number.
L’invariance évite des insertions incompatibles.
Lire une source avec extends
Une borne ? extends Number permet de lire les éléments comme des Number, mais le type précis peut être une catégorie plus étroite. Vous ne pouvez donc pas ajouter librement tout Number en supposant que la liste réelle l’accepte. Le contrat vous donne une capacité de lecture limitée par cette borne. Ce raisonnement porte sur les possibilités du type, et non sur ce que contient par hasard un exemple vide.
Erreur fréquente à comprendre Une collection ? extends Number accepte librement tout Number ajouté.
Le type réel d’élément peut être plus restreint.
Écrire vers un consommateur avec super
Une borne ? super Integer décrit un conteneur capable de recevoir des valeurs Integer selon son contrat. Elle ne promet pas que chaque lecture retourne un Integer : le conteneur réel peut être plus général et déjà contenir d’autres valeurs. Cette asymétrie explique pourquoi lecture et écriture demandent des bornes différentes. Évitez de mémoriser une formule sans relier la borne à l’opération réellement autorisée.
Erreur fréquente à comprendre ? super Integer garantit que toute lecture est un Integer.
Une superclasse peut contenir d’autres valeurs.
Cardinalité et présence sont des contrats distincts
Le type d’élément ne garantit pas qu’une liste contient au moins une valeur. Avant get sur un indice supposé, traitez le cas vide ou annoncez explicitement un contrat de non-vacuité établi par le code. Pour une recherche, une absence peut être un résultat normal. Les tests doivent couvrir liste vide, singleton et plusieurs éléments afin de vérifier les choix de résultat et les bornes d’accès.
Erreur fréquente à comprendre Le paramètre générique implique au moins un élément.
La cardinalité est une contrainte distincte.
Unicité d’un Set et définition de l’égalité
Un Set vise une collection d’éléments distincts selon son contrat. La définition de l’égalité et, selon l’implémentation, du hachage ou de l’ordre, participe au résultat. Un comparateur doit rester cohérent : soustraire deux grands int peut déborder et casser l’ordre attendu. Utilisez les primitives de comparaison adaptées et testez bornes et égalités, plutôt que vérifier seulement quelques petits nombres positifs.
Erreur fréquente à comprendre Tout Set conserve nécessairement tous les doublons.
La structure vise des éléments distincts selon ses règles.
Choisir la Map selon ses garanties
Les implémentations de Map n’offrent pas toutes le même ordre d’itération ni les mêmes capacités concurrentes. Définissez si l’insertion, le tri des clés ou une absence d’ordre stable fait partie du contrat public. Un affichage observé sur quelques éléments ne suffit pas à transformer ce comportement en promesse générale. Si une sérialisation doit être reproductible, choisissez et testez explicitement la règle d’ordre utilisée.
Erreur fréquente à comprendre Toute Map garantit l’ordre d’insertion.
Les implémentations ont des garanties différentes.
Structure non modifiable et éléments mutables
Une liste créée avec List.of ne permet pas des ajouts de structure. Cela ne rend pas automatiquement chaque objet référencé immuable. Un StringBuilder conservé dans cette liste peut encore changer et produire un affichage différent. Pour protéger une valeur composée, examinez séparément le conteneur, ses éléments et les références extérieures qui les désignent. La propriété recherchée doit couvrir le niveau où l’état peut effectivement évoluer.
Erreur fréquente à comprendre Non modifiable signifie tous les éléments profondément gelés.
La structure et les objets référencés sont distincts.
Le coût des types bruts
Un type brut retire une partie de l’information générique et peut déplacer un échec vers l’exécution. Un cast destiné seulement à faire compiler le code ne rétablit pas la relation réelle entre les types. Cherchez la frontière où l’information a été perdue et exprimez un contrat précis à cet endroit. Les avertissements et tests doivent aider à maintenir ce contrat, sans prétendre remplacer la validation d’entrée.
Erreur fréquente à comprendre Les types bruts donnent toujours plus de sûreté que les génériques.
Ils peuvent contourner une partie du contrôle statique.
Stabilité des clés de hachage
Une table de hachage organise ses entrées selon les propriétés de sa clé. Modifier après insertion un champ utilisé par equals ou hashCode peut rendre une recherche incohérente avec l’emplacement initial. Préférez une identité stable ou une stratégie de retrait puis réinsertion correctement maîtrisée. Testez le scénario de mutation, car une recherche immédiatement après insertion ne révèle pas la rupture introduite plus tard.
Erreur fréquente à comprendre Changer hashCode d’une clé insérée ne peut jamais affecter la recherche.
L’organisation dépend des propriétés de la clé.
Atelier guidé : lire, expliquer et vérifier le code
import java.util.List;
class Main {
public static void main(String[] args) {
var item = new StringBuilder("A");
var values = List.of(item);
item.append("B");
System.out.print(values.get(0));
}
}
List.of crée un conteneur non modifiable, mais la valeur item reste un StringBuilder mutable. La liste conserve cette référence, puis append ajoute B à son contenu. Lire le premier élément affiche donc AB. Le programme ne demande aucun add sur la liste : le refus de modification structurelle n’est pas mis en jeu ici.
Dans le laboratoire, essayez séparément un ajout au conteneur et une mutation de l’élément. Documentez les deux résultats afin d’éviter une conclusion générale sur l’immuabilité. Remplacez ensuite le StringBuilder par une valeur immuable et examinez ce que change ce choix pour le partage entre plusieurs consommateurs. Une API de lecture seule doit annoncer clairement si elle protège seulement la structure ou aussi les valeurs référencées.
Exemple commenté
var ids = java.util.List.of("R2", "R1");
var sorted = new java.util.ArrayList<>(ids);
sorted.sort(java.util.Comparator.naturalOrder());
System.out.println(sorted.getFirst());
La copie est modifiable et peut être triée sans changer la liste initiale. getFirst retourne R1 sur cette liste non vide.
Exercice de mise en pratique
Écrivez une méthode qui copie des Number d’un producteur vers un consommateur sans utiliser de types bruts.
Voir une proposition de correction
Utilisez une source List<? extends Number> et une destination List<? super Number>. Vérifiez la compilation avec des listes compatibles ; documentez les opérations d’ajout autorisées par chaque borne.
- Connaître ordre et unicité.
- Ne pas confondre covariance des tableaux et génériques.
- Tester collections vides et null.
Contrôle de connaissances
List.of permet-elle add après création ?
Réponse expliquée : Non, la liste est non modifiable — L’opération de modification est interdite.
Cours associé : Tableaux, génériques et collections ↗
Documentation de référence ↗ https://docs.oracle.com/en/java/javase/21/core/creating-immutable-lists-sets-and-maps.html
Module 4 / 8 · 90 min
Lambdas, streams et réductions
Construire des transformations sans état partagé caché.
Comprendre le mécanisme
Une lambda implémente une interface fonctionnelle et ne capture que des variables locales finales ou effectivement finales. Un stream décrit un pipeline : les opérations intermédiaires sont généralement paresseuses et une opération terminale déclenche le traitement nécessaire. Un stream consommé ne doit pas être réutilisé. map transforme chaque élément ; flatMap aplatit des flux imbriqués. Optional exprime une présence possible, sans autoriser un get aveugle.
Raisonner et vérifier
Une réduction parallèle exige notamment une combinaison associative et une identité adaptée. Modifier une liste extérieure dans forEach introduit un état partagé et peut rompre la sûreté du traitement. Préférez des collecteurs qui construisent le résultat. Ne faites pas reposer une obligation métier sur peek : certaines optimisations ou opérations terminales peuvent éviter le parcours imaginé.
Construire un pipeline sans le consommer
Des opérations intermédiaires comme filter et map décrivent un traitement qui peut être paresseux. L’exécution nécessaire se produit avec une opération terminale selon le contrat du stream. Une chaîne de méthodes ne prouve donc pas que tous les effets ont déjà eu lieu à la création. Pour comprendre un résultat, identifiez source, étapes intermédiaires et terminal, puis tracez les valeurs qui traversent réellement chaque étape.
Erreur fréquente à comprendre map exécute toujours immédiatement tous les traitements.
Le parcours dépend notamment d’une opération terminale.
Une consommation n’est pas une collection réutilisable
Un stream possède un cycle de vie et ne doit généralement pas être réutilisé après une opération terminale. Pour recommencer un parcours, créez un nouveau stream depuis la source ou fournissez un mécanisme qui le produit. Conserver une référence au stream ne le transforme pas en cache de résultats. Le test utile réalise deux parcours distincts depuis la source au lieu de répéter une consommation invalide.
Erreur fréquente à comprendre Le même stream peut toujours être consommé indéfiniment.
Le cycle de vie interdit cette réutilisation générale.
Capture locale et état effectivement final
Une lambda peut capturer une variable locale finale ou effectivement finale. Réaffecter ensuite cette variable rompt ce contrat ; la compiler n’est pas rendu possible parce que la lambda s’exécute plus tard. Cette règle ne gèle pas tous les objets référencés par la variable. Un objet mutable peut encore porter des effets partagés : l’absence de réaffectation de référence ne prouve pas une fonction pure.
Erreur fréquente à comprendre Toute locale capturée peut être réaffectée librement ensuite.
La capture obéit à cette contrainte du langage.
Réduire avec une loi adaptée
Une réduction parallèle peut regrouper les opérations dans des ordres différents. La combinaison doit donc être associative avec une identité adaptée au contrat choisi. Une soustraction arbitraire ne possède pas cette propriété : changer les regroupements peut changer le résultat. Écrivez la loi attendue sur un petit jeu de valeurs et comparez plusieurs regroupements avant de conclure que la parallélisation préserve votre calcul.
Erreur fréquente à comprendre Toute soustraction arbitraire est une réduction parallèle associative.
L’ordre de regroupement peut modifier le résultat.
Éviter les effets partagés non protégés
Écrire dans une ArrayList partagée depuis un stream parallèle n’acquiert pas automatiquement une protection de concurrence. Même lorsque le résultat semble correct lors d’un essai, des entrelacements différents peuvent perdre ou corrompre des opérations. Préférez une collecte et une combinaison adaptées au stream lorsque c’est le besoin. Les effets externes, comme envoyer des messages, demandent aussi une politique d’ordre, de reprise et de doublons.
Erreur fréquente à comprendre Un stream parallèle rend toute ArrayList partagée thread-safe.
Le parallélisme ne protège pas automatiquement l’état mutable.
Le terminal définit le résultat
Un terminal peut produire une somme, une liste, une valeur optionnelle ou un effet selon l’opération choisie. Il faut donc lire son contrat au lieu de supposer que toute chaîne renvoie une collection. Une somme après filtre ne porte que sur les éléments conservés. Tracez la cardinalité et le type après chaque transformation ; cette méthode permet de vérifier un résultat sans exécuter mentalement toutes les méthodes en bloc.
Erreur fréquente à comprendre Tous les terminaux retournent nécessairement une liste.
Le résultat dépend de l’opération choisie.
Représenter l’absence avec Optional
Un résultat Optional annonce qu’une valeur peut être absente. Il ne justifie pas un accès supposant systématiquement sa présence. Définissez comment le consommateur traite le cas vide : valeur de repli, autre traitement ou erreur explicitement choisie. Évitez un défaut qui transforme une absence significative en valeur métier trompeuse. Les tests doivent établir le comportement pour une source vide et pour un élément effectivement trouvé.
Erreur fréquente à comprendre Optional garantit toujours une valeur présente.
Le consommateur doit traiter le cas vide.
Ordre de rencontre et ordre des effets
L’ordre d’une source et les garanties de l’opération terminale influencent ce qui peut être observé. Le parallélisme ne promet pas que tout effet externe apparaisse dans un ordre intuitif. Si le contrat public exige une séquence, choisissez les opérations qui la préservent ou séparez le calcul de la publication. Une impression console nominale ne suffit pas à démontrer une garantie d’ordre sous toutes les exécutions.
Erreur fréquente à comprendre Un stream parallèle conserve toujours tout ordre d’effet externe.
Les garanties d’ordre doivent être choisies et comprises.
Mesurer avant de paralléliser
Le traitement parallèle introduit une distribution du travail et des coûts de coordination. Sur une petite collection ou une opération très courte, ces coûts peuvent dépasser le gain. La nature des dépendances et des ressources partagées compte aussi. Mesurez sur des données représentatives et conservez le même contrat de résultat. Le mot parallel ne crée ni cœurs supplémentaires ni réduction universelle de la latence.
Erreur fréquente à comprendre parallel réduit nécessairement la latence de tout petit calcul.
Surcoûts et caractéristiques du travail comptent.
Ne pas modifier la source pendant le parcours
Le contrat de non-interférence évite de transformer arbitrairement la source pendant que le pipeline la parcourt. Une mutation cachée dans un callback peut rendre le raisonnement dépendant d’un ordre ou d’une implémentation. Séparez préparation des données et traitement, ou utilisez un modèle explicitement conçu pour la concurrence nécessaire. Un test doit explorer les cas de source partagée au lieu de conclure depuis une seule exécution locale.
Erreur fréquente à comprendre Modifier arbitrairement la source pendant son parcours est toujours sûr.
Le contrat doit préserver la cohérence du traitement.
Atelier guidé : lire, expliquer et vérifier le code
import java.util.List;
class Main {
public static void main(String[] args) {
var source = List.of(1, 2, 3);
int sum = source.stream().filter(n -> n > 1).mapToInt(n -> n * 2).sum();
System.out.print(sum);
}
}
Le filtre de l’exercice retire 1 et conserve 2 puis 3. mapToInt transforme ces valeurs en 4 et 6, et sum déclenche le parcours utile avant d’additionner les résultats. La sortie vaut donc 10. Ni la somme de la source seule ni le double de toute la source ne respecte l’ordre des transformations montrées.
Rejouez le programme en retirant temporairement le terminal pour observer que la simple construction du pipeline n’est pas la même opération. Ajoutez ensuite une source vide et vérifiez le résultat défini par le terminal. Si vous envisagez une version parallèle, examinez séparément l’associativité de la réduction et les effets éventuels des callbacks ; la simplicité de cet exemple ne constitue pas une preuve pour tous les traitements.
Exemple commenté
var total = java.util.List.of(2, 5, 8).stream()
.filter(n -> n > 3)
.mapToInt(Integer::intValue)
.sum();
System.out.println(total);
Le filtre conserve 5 et 8 ; la réduction retourne 13. La liste source n’est pas modifiée.
Exercice de mise en pratique
Regroupez des réservations par produit et calculez le total de quantités sans map globale mutable.
Voir une proposition de correction
Utilisez groupingBy avec summingInt sur un record validé. Comparez le résultat séquentiel et parallèle sur les mêmes données et testez le flux vide.
- Identifier la terminaison.
- Éviter effets de bord dans les lambdas.
- Vérifier associativité et identité.
Contrôle de connaissances
Quand le pipeline filter/map réalise-t-il normalement son travail ?
Réponse expliquée : Lors de l’opération terminale nécessaire — Le traitement intermédiaire est paresseux.
Cours associé : Lambdas, streams et réductions ↗
Documentation de référence ↗ https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/package-summary.html
Module 5 / 8 · 90 min
Exceptions, fichiers et sérialisation
Attribuer la propriété des ressources et fermer les flux.
Comprendre le mécanisme
Les exceptions checked doivent être capturées ou déclarées selon le contrat d’appel. try-with-resources ferme les ressources AutoCloseable dans l’ordre inverse de leur déclaration. Si le corps échoue et que close échoue aussi, l’exception de fermeture peut être supprimée au profit de l’exception principale. Enfin, Path.normalize simplifie la représentation lexicale ; il ne vérifie pas les liens symboliques ni l’existence de la cible.
Raisonner et vérifier
Un Stream<String> renvoyé par Files.lines possède une ressource à fermer. Évitez de charger un fichier arbitrairement grand sans limite. Pour la sérialisation Java native, définissez une frontière de confiance et des filtres adaptés : un format d’objet n’est pas un schéma métier validé. Un chemin provenant du client doit être contraint au répertoire autorisé avec une stratégie compatible avec les liens et la concurrence.
Signaler une exception selon sa catégorie
Java distingue des catégories d’exceptions dont les règles de capture ou de déclaration diffèrent. Une exception vérifiée peut obliger le consommateur à traiter explicitement un échec possible. Cette obligation de compilation ne décide pas du résultat métier après une écriture. Choisissez la frontière capable d’annuler, traduire ou reprendre, et vérifiez les effets déjà réalisés avant l’exception. Un simple catch vide ne rend pas le parcours correct.
Erreur fréquente à comprendre Toute exception Java est non vérifiée.
La hiérarchie distingue plusieurs catégories.
Fermer les ressources du parcours
try-with-resources organise la fermeture des ressources compatibles avec son contrat. Le Stream obtenu par Files.lines est lié à une ressource de lecture et doit être fermé, par exemple dans ce bloc. La présence d’opérations de stream ne retire pas le besoin de gérer le fichier. Testez le traitement complet et une interruption précoce du parcours afin de vérifier le nettoyage prévu.
Erreur fréquente à comprendre Une ressource AutoCloseable reste toujours ouverte après ce bloc.
Le mécanisme organise sa fermeture.
L’ordre inverse correspond aux dépendances
Plusieurs ressources déclarées dans un même bloc sont fermées dans l’ordre inverse de leur déclaration. Cette règle convient notamment lorsque les ressources créées plus tard dépendent des précédentes. Tracez acquisition, travail puis fermeture comme trois phases séparées. Pour expliquer une sortie console, notez d’abord ce qui est affiché dans le corps avant d’ajouter les événements de nettoyage ; ils ne se produisent pas tous à la construction.
Erreur fréquente à comprendre L’ordre de fermeture est toujours celui de déclaration.
Le nettoyage suit l’ordre inverse de construction dans ce contrat.
Préserver la cause initiale
Une erreur pendant le travail peut être accompagnée d’une autre erreur pendant close. Le mécanisme de ressources peut conserver une exception comme supprimée plutôt que remplacer systématiquement la cause principale. Une investigation doit donc examiner les informations pertinentes de l’exception, pas seulement son message public. Les logs restent privés et minimisés ; présenter toute la pile à l’utilisateur n’est pas nécessaire pour signaler un échec contrôlé.
Erreur fréquente à comprendre Toute erreur de fermeture remplace nécessairement l’erreur initiale.
Le mécanisme conserve aussi des exceptions supprimées.
Un chemin reçu n’est pas une permission
Un client peut fournir un nom ou identifiant de document sans posséder le droit de lire le fichier résolu. Préférez un identifiant métier relié côté serveur à une ressource maîtrisée. Vérifiez l’autorisation avant ouverture et évitez les concaténations qui donnent au client tout l’espace de fichiers. Le test doit demander un document tiers et un chemin hors périmètre, puis observer un refus sans lecture interdite.
Erreur fréquente à comprendre Un chemin absolu client donne une permission de lecture.
Le système doit limiter les ressources accessibles.
Normalisation lexicale et résolution réelle
Path.normalize simplifie une représentation de chemin, par exemple des segments relatifs. Il ne prouve pas à lui seul que le fichier effectivement ouvert reste dans une racine, notamment avec des liens symboliques et des changements concurrents. Définissez le modèle de résolution et les garanties du stockage. Un test purement textuel ne couvre pas les conditions réelles du système de fichiers ni toutes les courses entre contrôle et utilisation.
Erreur fréquente à comprendre normalize seul prouve tout confinement sur le système réel.
La résolution effective peut introduire d’autres frontières.
Définir l’encodage du document
Des octets ne constituent pas automatiquement un texte conforme au format attendu. Choisissez le charset et la politique face à une séquence invalide avant de traiter une importation. Un résultat lisible sur une machine ne garantit pas une lecture identique dans un autre environnement configuré différemment. Les fixtures doivent contenir texte international, caractères limites et données invalides pour vérifier la propriété annoncée.
Erreur fréquente à comprendre Toute suite d’octets est un texte UTF-8 valide.
La validité d’encodage demande un contrat.
Borner le volume consommé
Une lecture complète en mémoire comme readAllBytes possède un coût lié au fichier. Un droit d’accès correct n’autorise pas une consommation illimitée. Définissez taille maximale, stratégie de lecture et comportement lorsque la limite est atteinte. Un flux progressif peut aider, mais seulement si le consommateur n’accumule pas ensuite tout le contenu. Mesurez la mémoire et le temps sur un fichier proche de la borne choisie.
Erreur fréquente à comprendre readAllBytes est sans coût mémoire quelle que soit la taille.
La méthode charge les octets en mémoire.
Nettoyage et succès sont distincts
Fermer une ressource correctement ne valide pas une transaction métier. Une opération peut avoir échoué puis libéré ses fichiers comme prévu. Le résultat public et le code de sortie doivent conserver cette distinction, faute de quoi une CI ou un service appelant peut interpréter le nettoyage comme un succès. Testez simultanément le diagnostic attendu et l’état final des ressources et données.
Erreur fréquente à comprendre Une fermeture réussie valide automatiquement la transaction.
Libération et réussite sont des propriétés différentes.
Donner un diagnostic sans dévoiler les chemins
Une erreur de fichier peut contenir une arborescence interne, un nom de compte ou d’autres informations sensibles. La réponse publique doit fournir un code utile et une corrélation maîtrisée. Le diagnostic privé peut conserver davantage de contexte selon les droits et la conservation prévus. Les tests d’erreur doivent examiner le corps retourné : le parcours exceptionnel ne doit pas devenir un canal de divulgation absent du parcours nominal.
Erreur fréquente à comprendre Une API doit afficher chaque chemin interne sur échec.
Le diagnostic détaillé doit rester contrôlé.
Atelier guidé : lire, expliquer et vérifier le code
class Resource implements AutoCloseable {
private final String name;
Resource(String name) { this.name = name; }
public void close() { System.out.print(name); }
}
class Main {
public static void main(String[] args) {
try (var a = new Resource("A"); var b = new Resource("B")) {
System.out.print("X");
}
}
}
Le corps du bloc affiche d’abord X. Les ressources ont été déclarées dans l’ordre A puis B ; la fermeture commence par B et se termine par A. La sortie est donc XBA. close ne se déclenche pas avant le travail du bloc et les ressources sont bien compatibles avec le mécanisme utilisé.
Ajoutez dans une copie de laboratoire une exception pendant le corps, puis une autre pendant une fermeture. Examinez la cause principale et les exceptions supprimées avec un test local expurgé. Remplacez enfin la ressource artificielle par une lecture de fichier contrôlée. La finalité reste de vérifier le cycle d’acquisition et de libération, sans confondre ce nettoyage avec la réussite de l’opération métier utilisant les données lues.
Spring : exception checked et règle de rollback
La présence d’une exception ne signifie pas que toute transaction Spring est automatiquement annulée. Dans le comportement standard du scénario, RuntimeException et Error déclenchent le rollback, tandis qu’une IOException checked qui s’échappe ne l’impose pas à elle seule.
Si les écritures ont réussi et qu’aucune autre cause n’a marqué la transaction rollback-only, elles peuvent donc être commitées malgré cette exception. Configurez rollbackFor pour la classe pertinente lorsque le contrat métier exige l’annulation, ou adoptez une politique globale explicite et revue. Les versions et options qui modifient les défauts doivent être prises en compte ; l’exercice les exclut précisément.
Testez depuis un appel externe qui traverse le proxy et inspectez le stockage après la fin de la transaction. Un test enveloppé dans sa propre transaction automatiquement annulée peut masquer le commit réel que vous cherchez à observer. Couvrez exception checked, runtime et chemin nominal avec les règles effectivement configurées.
Points de vigilance : Le comportement standard ne marque pas automatiquement le rollback pour toutes les exceptions checked. Une transaction locale n’annule pas arbitrairement sa dernière instruction. Sans autre cause de rollback, cette exception ne déclenche pas la règle standard ; rollbackFor peut l’inclure. Le type Java de l’exception n’est pas converti automatiquement ainsi.
Documentation de référence ↗
Exemple commenté
try (var lines = java.nio.file.Files.lines(java.nio.file.Path.of("reservations.csv"))) {
System.out.println(lines.filter(line -> !line.isBlank()).count());
}
Le flux est fermé après le bloc, y compris si le comptage échoue. L’exemple suppose un fichier de laboratoire déjà présent.
Exercice de mise en pratique
Lisez un CSV ligne par ligne, conservez le numéro des lignes invalides et garantissez la fermeture du flux.
Voir une proposition de correction
Entourez Files.lines d’un try-with-resources ; séparez parsing et validation. Testez fichier absent, ligne mal formée et erreur pendant la lecture.
- Fermer les ressources possédées.
- Distinguer normalisation et résolution réelle.
- Conserver l’exception principale.
Contrôle de connaissances
Faut-il fermer le Stream retourné par Files.lines ?
Réponse expliquée : Oui, par exemple avec try-with-resources — Il encapsule un fichier ouvert.
Cours associé : Exceptions, fichiers et sérialisation ↗
Documentation de référence ↗ https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Files.html
Module 6 / 8 · 90 min
Concurrence, exécuteurs et virtual threads
Distinguer visibilité, atomicité et capacité de traitement.
Comprendre le mécanisme
Des tâches concurrentes peuvent partager un état et entrelacer leurs opérations. volatile apporte des garanties de visibilité et d’ordre, pas l’atomicité d’une séquence comme count++. Utilisez verrou ou type atomique pour un invariant adapté. Un ExecutorService gère le lancement de tâches ; Future porte le résultat ou l’échec qu’il faut effectivement observer. Des collections concurrentes ne rendent pas automatiquement atomique une séquence de plusieurs appels.
Raisonner et vérifier
Les virtual threads de Java 21 aident à servir de nombreuses tâches principalement bloquantes. Ils ne multiplient pas le nombre de cœurs ni n’accélèrent magiquement un calcul CPU. Limitez l’accès aux ressources rares avec un mécanisme explicite. Dans Java 21, un bloc synchronized contenant une attente bloquante peut aussi être pertinent pour analyser le pinning ; raisonnez toujours avec la version ciblée.
Visibilité et atomicité ne se confondent pas
Un champ volatile apporte des garanties de visibilité et d’ordre définies par le modèle mémoire. Il ne transforme pas toute séquence en opération atomique. Un incrément comme count++ comporte une lecture, un calcul et une écriture ; deux threads peuvent lire le même ancien nombre. Définissez la propriété à protéger, puis choisissez une primitive atomique ou un verrou adapté, au lieu de supposer que la visibilité suffit.
Erreur fréquente à comprendre volatile rend automatiquement compteur++ atomique.
Lecture, calcul et écriture peuvent rester des étapes distinctes.
Protéger un invariant composé
Si un traitement doit vérifier un solde puis le modifier avec une autre donnée, protéger séparément chaque lecture ne rend pas automatiquement la transition entière cohérente. La zone de synchronisation doit correspondre à l’invariant partagé. Dans un service singleton, évitez aussi de conserver le marchand courant dans un champ mutable réutilisé par toutes les requêtes. Des paramètres locaux et immuables limitent ce partage involontaire de contexte.
Erreur fréquente à comprendre Un champ volatile suffit toujours à plusieurs mises à jour liées.
L’atomicité du groupe demande une stratégie adaptée.
Observer les résultats des tâches
Un Future peut représenter un résultat encore attendu, réussi ou en échec selon son cycle. Recevoir cet objet ne prouve pas que le travail métier a abouti. Le consommateur doit observer les résultats et les erreurs, puis appliquer une politique de reprise ou de refus. Abandonner silencieusement les références peut laisser des échecs invisibles tandis que l’appelant annonce déjà un succès.
Erreur fréquente à comprendre Obtenir un Future prouve déjà le succès du travail.
Le résultat peut être en attente ou en échec.
Gérer la durée des exécuteurs
Un exécuteur possède des ressources et un cycle de fermeture. En créer sans politique peut multiplier threads, files et travail restant. Définissez qui le possède et quand il cesse d’accepter des tâches. Un test doit couvrir la fin normale, une erreur et l’arrêt avec travail en attente. Le fait qu’un processus termine rapidement dans l’IDE ne prouve pas que toutes les ressources sont libérées proprement.
Erreur fréquente à comprendre Créer des pools indéfiniment ne consomme aucune ressource.
Threads et files demandent une gestion de durée de vie.
Annuler n’efface pas tout effet
Une demande d’annulation ne garantit pas un arrêt immédiat de chaque calcul ou de toute opération distante déjà validée. Le code doit coopérer selon le mécanisme utilisé et les dépendances peuvent continuer leur travail. Pour une mutation sensible, conservez une référence et un moyen de réconciliation. Un timeout ou une annulation côté client ne démontre pas que le service externe a laissé ses données intactes.
Erreur fréquente à comprendre cancel force toujours la fin immédiate de tout effet distant.
Le traitement et les dépendances peuvent continuer.
Traiter le signal d’interruption
L’interruption signale une demande à laquelle le code doit répondre selon son contrat. La capturer puis l’ignorer systématiquement peut empêcher un arrêt cohérent. Examinez la responsabilité du niveau courant et la façon de conserver ou propager l’information utile. Les tests doivent provoquer l’interruption pendant une attente contrôlée et vérifier que le traitement libère les ressources prévues sans annoncer un succès qui n’a pas eu lieu.
Erreur fréquente à comprendre Une interruption doit toujours être silencieusement ignorée.
Le code doit préserver une politique d’arrêt cohérente.
Threads virtuels et ressources réelles
Les threads virtuels Java 21 facilitent notamment de nombreuses tâches comportant des attentes bloquantes indépendantes. Ils ne créent pas de puissance CPU ni une capacité infinie chez un fournisseur. Pour limiter cinq opérations simultanées vers une ressource, un Semaphore peut représenter cette borne avec libération garantie. Définissez aussi la file et le délai : multiplier les tâches en attente ne retire pas le besoin de budgets.
Erreur fréquente à comprendre Un thread virtuel multiplie automatiquement la puissance CPU.
Ils ne créent pas de cœurs supplémentaires.
Opérations concurrentes et workflow composé
Une collection concurrente protège des opérations selon son contrat, pas nécessairement toute combinaison de plusieurs appels. Lire une valeur puis décider une écriture peut encore former une course. Cherchez une opération composée adaptée ou une synchronisation portant sur l’invariant entier. Un test utile interpose deux acteurs entre la lecture et l’écriture, au lieu de vérifier seulement plusieurs appels exécutés successivement.
Erreur fréquente à comprendre Deux opérations séparées deviennent atomiques car la Map est concurrente.
Un invariant composé peut demander une primitive spécifique.
Borner la file et la pression
Une file sans limite peut accumuler plus de travail que le service ne peut traiter dans le délai utile. La mémoire et la latence deviennent alors des ressources consommées avant même l’exécution. Choisissez capacité, refus et reprise selon le produit. Une limitation de concurrence sans limitation d’attente peut seulement déplacer le problème ; mesurez ensemble tâches actives, tâches en file et durée totale du parcours.
Erreur fréquente à comprendre Une file infinie est une garantie universelle de disponibilité.
L’accumulation peut épuiser la mémoire et la latence.
Tester les entrelacements pertinents
Un succès séquentiel ne démontre pas l’absence d’une course. Identifiez l’endroit où deux acteurs peuvent observer ou modifier le même état, puis contrôlez cette interposition avec des mécanismes de test appropriés. Évitez de dépendre uniquement de pauses aléatoires. Vérifiez l’invariant final, les erreurs et les effets externes ; augmenter seulement le nombre d’itérations ne remplace pas un scénario qui cible réellement la frontière.
Erreur fréquente à comprendre Un succès séquentiel prouve l’absence de tout interleaving dangereux.
Les exécutions concurrentes peuvent produire d’autres ordres.
Atelier guidé : lire, expliquer et vérifier le code
class Counter {
volatile int value;
void increment() { value++; }
}
Le champ volatile du compteur ne suffit pas à rendre l’incrément composé indivisible. Deux threads peuvent lire une même valeur, calculer chacun la suivante et écrire le même résultat, perdant ainsi une augmentation. Il ne s’agit pas d’une erreur de syntaxe ni d’une conversion implicite vers AtomicInteger : le champ reste un int et les étapes du calcul restent à analyser.
Dans un laboratoire, comparez une primitive d’incrément atomique avec un invariant portant sur plusieurs champs. La première peut convenir au simple compteur, alors que le second demande une protection de groupe. Ajoutez un scénario de service singleton avec un contexte marchand passé en paramètre. Vérifiez que deux appels concurrents ne se contaminent pas, même lorsque leurs étapes sont volontairement entrelacées.
Singleton Spring : état de requête et concurrence
@Service
public class PaymentService {
private String merchantId;
public void pay(String id) {
merchantId = id;
// ... utilise ensuite merchantId
}
}
Un bean singleton est partagé dans le conteneur concerné. Si un champ mutable currentMerchant reçoit le marchand de chaque demande, deux threads peuvent se remplacer cette valeur pendant leur traitement. Le second marchand peut alors être utilisé pour une opération du premier.
Conservez le contexte propre à l’appel dans une variable locale ou une commande immuable, puis transmettez-le explicitement aux dépendances. Ce modèle rend la relation visible et évite de stocker une identité ambiante dans un objet partagé. Changer simplement le nom du champ ou rendre sa lecture plus rapide ne résout pas la course.
Un test concurrent synchronisé peut suspendre A après l’affectation, faire avancer B, puis reprendre A pour révéler le mélange. Vérifiez références prestataire et tenant sur les appels factices. Une structure immuable reste insuffisante si elle contient un objet interne mutable partagé ; examinez les références qui composent réellement le contexte et gardez l’autorisation à la frontière métier.
Points de vigilance : L’état propre à une requête ne doit pas vivre dans un champ mutable partagé entre traitements. volatile apporte de la visibilité mémoire, pas une isolation par requête. Un champ static élargit le partage au lieu de le supprimer. L’isolation transactionnelle des données ne garantit pas celle des champs en mémoire.
Documentation de référence ↗
Exemple commenté
try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
var result = executor.submit(() -> "R1");
System.out.println(result.get());
}
Le résultat est attendu et les erreurs d’exécution ne sont pas ignorées. La fermeture de l’exécuteur traite sa terminaison selon son contrat.
Exercice de mise en pratique
Simulez cent appels bloquants, puis imposez au plus cinq accès simultanés à une dépendance limitée.
Voir une proposition de correction
Utilisez un Semaphore autour de l’accès rare avec libération dans finally. Lancez les tâches dans l’exécuteur, attendez leurs Future et vérifiez le maximum observé.
- Tester la concurrence avec plusieurs tâches réelles.
- Observer les exceptions des Future.
- Ne pas confondre visibilité et opération atomique.
Contrôle de connaissances
volatile int count rend-il count++ atomique ?
Réponse expliquée : Non, lecture-modification-écriture reste composée — volatile ne fusionne pas les étapes de l’incrément.
Cours associé : Concurrence, exécuteurs et virtual threads ↗
Documentation de référence ↗ https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html
Module 7 / 8 · 90 min
Temps, localisation et journalisation
Choisir une représentation temporelle et un format pour chaque frontière.
Comprendre le mécanisme
Instant représente un point sur la ligne du temps ; LocalDateTime représente une date et une heure sans fuseau. Un ZoneId fournit les règles nécessaires à une conversion locale. Une période calendaire et une durée en secondes ne sont pas interchangeables autour des changements d’heure. Les objets java.time sont immuables : une méthode d’ajout retourne une nouvelle valeur.
Raisonner et vérifier
Séparez représentation machine stable et affichage destiné à une locale. Une virgule décimale ou un mois traduit ne doit pas être réinterprété comme un protocole universel. Un ResourceBundle résout des ressources localisées avec une stratégie de repli. Les logs doivent utiliser une corrélation et des champs minimisés ; une API de journalisation ne dispense pas d’éviter les secrets.
Représenter un instant ou une date locale
Instant représente un point sur la ligne du temps, alors qu’une LocalDateTime décrit des composantes locales sans identifier seule un instant mondial unique. Le choix dépend du besoin : un anniversaire n’a pas le même modèle qu’une expiration de token. Définissez le sens de la donnée avant sa sérialisation. Ajouter un format d’affichage ne fournit pas le contexte de zone qui manque à une valeur locale.
Erreur fréquente à comprendre LocalDateTime identifie seul un instant mondial unique.
Il manque notamment le contexte de fuseau et ses règles.
Le fuseau est un ensemble de règles
ZoneId désigne des règles de zone qui peuvent dépendre de la date. Ne le réduisez pas à un décalage constant simplement parce qu’un exemple d’hiver utilise une valeur donnée. Pour convertir une date locale en instant, choisissez la zone de référence prévue par le contrat. Les échanges machine doivent porter suffisamment d’information pour éviter qu’un serveur et un client appliquent silencieusement deux contextes différents.
Erreur fréquente à comprendre Un nom de zone est toujours un décalage fixe éternel.
Les règles peuvent changer avec la date.
Heures absentes et heures répétées
Un changement d’heure peut créer une période locale absente ou répétée. Un rendez-vous représenté uniquement par des composantes locales peut donc demander une politique explicite pour être interprété. Testez une date ordinaire et une date proche d’une transition avec les règles de la zone ciblée. L’affichage plausible ne constitue pas une preuve : plusieurs interprétations peuvent exister selon la représentation et les choix du consommateur.
Erreur fréquente à comprendre Toute heure locale correspond toujours exactement à un instant.
Des chevauchements et intervalles absents sont possibles.
Rendre le temps testable
Une horloge injectée permet de vérifier une limite d’expiration sans attendre réellement son échéance. Le test peut se placer juste avant, exactement à la borne et juste après. Ces cas doivent correspondre à la règle du produit. Évitez de changer l’heure globale d’un environnement partagé pour un exercice : une dépendance temporelle contrôlée donne une preuve plus reproductible et un périmètre plus clair.
Erreur fréquente à comprendre Un test temporel doit toujours attendre en temps réel.
Le temps peut être une dépendance contrôlée.
Calendrier, durée et immuabilité
Period et Duration expriment des modèles différents, notamment unités calendaires et durée temporelle. Un mois n’est pas toujours trente jours. Les objets de date comme LocalDate sont immuables : plusDays retourne une nouvelle valeur et ne modifie pas l’objet initial. Conservez donc le résultat lorsque vous voulez le changement. Testez les fins de mois et les années bissextiles pour vérifier les hypothèses de votre calcul.
Erreur fréquente à comprendre Un mois calendaire égale toujours trente jours.
Les calendriers ont des longueurs variables.
La locale organise une présentation
La locale porte des conventions de langue et de formatage ; elle ne remplace pas un fuseau ni l’instant lui-même. Deux utilisateurs peuvent afficher différemment une même donnée machine. Gardez le protocole d’échange séparé de cette présentation pour éviter qu’une virgule, un nom de mois ou un ordre de composantes local change la valeur interprétée par un autre service.
Erreur fréquente à comprendre Locale change automatiquement le fuseau de chaque instant.
Les notions de langue et de zone sont distinctes.
Une grammaire d’échange explicite
Le format machine doit annoncer sa grammaire et les informations de zone ou décalage nécessaires. Une chaîne lisible par un humain n’est pas forcément non ambiguë pour tous les parseurs. Définissez la politique de refus d’une entrée invalide au lieu de tenter plusieurs interprétations au hasard. Les fixtures doivent couvrir des valeurs limites et des variantes qui semblent proches mais n’appartiennent pas au format accepté.
Erreur fréquente à comprendre Tout texte de date est interprété universellement pareil.
Les conventions et parseurs peuvent différer.
Représenter exactement les montants
Un float binaire ne garantit pas les calculs décimaux exacts d’un paiement. Pour un échange PHP et Java, choisissez unité, type, bornes et arrondi. Un PSP qui demande un entier dans l’unité mineure ne doit pas recevoir une conversion implicite depuis un affichage localisé. La devise et son contrat d’unité doivent accompagner le montant ; toutes les devises ne suivent pas nécessairement la même règle d’exposant.
Erreur fréquente à comprendre Un float binaire assure toute précision décimale financière.
Les besoins d’exactitude doivent être pris en compte.
Les règles peuvent évoluer
Les données de fuseau reflètent des règles civiles susceptibles d’être mises à jour. Une application doit donc suivre les versions et le sens de ses données historiques ou futures. Ne supposez pas qu’un décalage observé aujourd’hui restera la bonne règle pour un événement futur. Lors d’une mise à jour, testez les zones et dates importantes pour le produit plutôt que seulement une date locale proche de l’exécution.
Erreur fréquente à comprendre Une règle de zone est immuable pour toute politique future.
Les mises à jour peuvent refléter des changements civils.
Comparer dans un modèle cohérent
Pour une expiration, comparez des valeurs dans un modèle temporel commun, avec une horloge et une tolérance documentées lorsque le protocole l’exige. Trier des chaînes locales de zones différentes ne prouve pas l’ordre des instants. La décision doit aussi préciser si la borne est inclusive ou exclusive. Les tests aux frontières rendent cette règle visible et empêchent qu’un format d’affichage décide implicitement d’un droit d’accès.
Erreur fréquente à comprendre Comparer des textes locaux de zones différentes suffit toujours.
La représentation doit correspondre à la propriété recherchée.
Atelier guidé : lire, expliquer et vérifier le code
import java.time.LocalDate;
class Main {
public static void main(String[] args) {
var date = LocalDate.of(2024, 2, 28).plusDays(1);
System.out.print(date);
}
}
Le programme part du 28 février 2024 et produit une nouvelle LocalDate en ajoutant un jour. Cette année étant bissextile, le résultat affiché est le 29 février. L’objet initial n’est pas modifié par plusDays ; c’est la valeur retournée qui est affectée à date dans l’exemple. Aucune zone n’est nécessaire pour ce calcul de date calendaire.
Conservez séparément la date initiale et le résultat pour vérifier leur indépendance. Essayez ensuite une fin de mois ordinaire. Pour une expiration réelle, changez de modèle et utilisez un instant avec une horloge contrôlée : une réussite sur une date locale ne démontre pas la bonne interprétation d’une heure ambiguë dans un fuseau. Chaque représentation doit correspondre à la question temporelle effectivement posée.
Montants PHP/Java : exactitude et unité contractuelle
Un montant doit porter une devise et une unité explicites. Dans le contrat posé, le PSP attend un entier d’unité mineure ; multiplier toujours par cent est incorrect pour les devises ou opérations dont l’exposant contractuel diffère.
Utilisez une représentation exacte : entier borné dans l’unité définie, ou décimal exact à la frontière de conversion avec règle d’arrondi explicite. En Java, construisez BigDecimal depuis une chaîne décimale lorsque nécessaire et vérifiez la conversion exacte vers l’entier du contrat. En PHP, évitez de passer par float pour reconstruire les centimes d’une chaîne utilisateur.
Testez devise à deux décimales, devise sans décimales et dépassement de capacité. Les limites d’API et les règles particulières du fournisseur doivent être vérifiées pour le type d’opération concerné. Une valeur exacte dans la mauvaise devise reste erronée ; un montant positif ne prouve pas non plus que l’acteur a le droit de débiter ou rembourser cette somme.
Points de vigilance : Les flottants binaires et la règle universelle de deux décimales introduisent des erreurs. Sans devise, l’unité et les opérations autorisées deviennent ambiguës. Une représentation de présentation locale n’est pas un contrat machine universel. Des unités explicites et une arithmétique exacte rendent les conversions vérifiables ; gérer également arrondis et dépassements.
Documentation de référence ↗
Exemple commenté
var date = java.time.LocalDate.of(2026, 1, 31);
var next = date.plusDays(1);
System.out.println(date);
System.out.println(next);
La date initiale reste au 31 janvier ; next vaut le 1er février. Ignorer la valeur retournée aurait perdu le changement demandé.
Exercice de mise en pratique
Définissez le format d’un événement de réservation et son affichage dans deux locales sans altérer l’instant stocké.
Voir une proposition de correction
Transportez un Instant au format ISO et appliquez le fuseau et la locale uniquement à la présentation. Testez une transition d’heure et l’absence de traduction spécifique.
- Distinguer instant et date locale.
- Conserver les valeurs retournées par java.time.
- Exclure tokens et mots de passe des événements.
Contrôle de connaissances
LocalDateTime identifie-t-il à lui seul un instant mondial unique ?
Réponse expliquée : Non, il manque un contexte de fuseau ou décalage — La date locale ne contient pas de zone.
Cours associé : Temps, localisation et journalisation ↗
Documentation de référence ↗ https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/package-summary.html
Module 8 / 8 · 90 min
Modules, services, packaging et diagnostic
Définir des dépendances explicites et un artefact exécutable.
Comprendre le mécanisme
Un module nomme son unité de code et déclare ses dépendances. exports rend un package accessible selon les règles du système de modules ; opens concerne notamment l’accès réflexif profond. Une classe publique n’est pas pour autant accessible depuis tout module si son package n’est pas exporté. Le mécanisme uses/provides relie consommateurs et fournisseurs de services sans imposer une instanciation concrète dans le code appelant.
Raisonner et vérifier
Compilez avec une version cible explicite et vérifiez l’exécution depuis l’artefact, pas seulement depuis l’IDE. Un JAR modulaire et un JAR classique peuvent être lancés différemment. jlink construit une image d’exécution à partir de modules disponibles ; il ne remplace pas les tests ni la validation de configuration. Documentez aussi les annotations standard et la journalisation nécessaires au diagnostic de votre projet.
Déclarer les dépendances du module
Un module nommé peut décrire les modules qu’il lit et les packages qu’il expose. Cette structure aide à définir une frontière de code plus précise qu’une simple archive contenant des classes. Un JAR sans le même contrat de module ne doit pas être supposé équivalent dans tous les modes d’exécution. Lisez la configuration effective du lancement pour savoir comment les dépendances sont résolues.
Erreur fréquente à comprendre Un JAR quelconque possède nécessairement le même contrat module-info.
Les modes de packaging ont des caractéristiques différentes.
Public ne signifie pas exporté partout
Une classe public d’un package non exporté ne devient pas librement accessible à tous les modules. L’export participe à la frontière d’accès entre modules, avec les autres règles applicables. Concevez les packages d’API et d’implémentation selon les besoins des consommateurs. Un test réalisé dans un seul module peut masquer cette différence : vérifiez un consommateur séparé qui utilise réellement le contrat exporté.
Erreur fréquente à comprendre exports ouvre automatiquement toute réflexion profonde privée.
L’ouverture à la réflexion est une notion distincte.
Ouvrir la réflexion de manière ciblée
opens vise des usages de réflexion selon le contexte du module. Ce n’est pas la même déclaration que exports pour les accès ordinaires. Un framework peut demander une ouverture spécifique sans justifier une exposition générale de toute l’implémentation. Limitez le périmètre nécessaire et vérifiez la version ciblée. L’ouverture de code ne donne pas non plus une permission métier à l’utilisateur final du service.
Erreur fréquente à comprendre opens accorde automatiquement toutes les permissions métier.
L’encapsulation du code ne définit pas les droits utilisateurs.
Aligner compilation et runtime
Un code compilé avec un JDK récent peut utiliser une syntaxe ou une API absente du runtime ciblé. Le succès dans l’IDE ne suffit donc pas. Définissez la version de production et construisez l’artefact avec un contrat compatible. Exécutez ensuite ce livrable dans un environnement propre correspondant à la cible ; ce test couvre des hypothèses que la configuration confortable du poste peut masquer.
Erreur fréquente à comprendre Compiler sur un JDK récent garantit toute compatibilité ancienne.
La syntaxe et les API utilisées doivent être compatibles.
Le rôle exact de release et jlink
L’option --release aide à cibler une version de langage et d’API selon le compilateur, mais elle n’installe pas le runtime sur le serveur. jlink peut assembler une image d’exécution à partir de modules et de leurs dépendances. Cette image devient un artefact à maintenir et à tester. Conservez sa composition et vérifiez qu’elle contient les capacités réellement utilisées par l’application déployée.
Erreur fréquente à comprendre --release installe automatiquement le runtime de production.
Le flag de compilation ne provisionne pas le serveur.
Reproduire les dépendances résolues
Une référence flottante peut évoluer entre deux constructions. Conservez les versions résolues et le processus qui produit l’artefact afin de pouvoir comparer une panne à un contenu exact. La reproductibilité ne prouve pas l’absence de vulnérabilité ; elle permet de savoir ce qui a été construit. Une mise à jour doit donc combiner changement identifié, contrôles appropriés et vérification du comportement attendu.
Erreur fréquente à comprendre Une version flottante garantit toujours le même artefact.
La résolution peut évoluer.
Vérifier les ressources embarquées
La compilation ne prouve pas que tous les fichiers, services et ressources nécessaires sont présents au runtime. Une configuration oubliée ou un chemin dépendant du poste peut échouer après livraison. Lancez l’archive ou l’image réellement construite depuis un environnement sans les fichiers de développement. Contrôlez le parcours utile, pas seulement le démarrage du processus ou l’existence d’un fichier portant le bon nom.
Erreur fréquente à comprendre Un succès de compilation prouve toutes les ressources à l’exécution.
La compilation ne couvre pas tous les besoins déployés.
Un JAR distribué n’est pas un coffre
Les ressources contenues dans une archive distribuée peuvent être inspectées. Un credential embarqué ne devient pas secret parce qu’il se trouve dans un JAR. Séparez configuration sensible et artefact public selon le mode de déploiement. Testez le contenu final et les chemins de diagnostic ; retirer un secret d’une nouvelle version ne révoque pas les copies de l’ancienne déjà diffusées.
Erreur fréquente à comprendre Un fichier embarqué devient secret parce qu’il est dans un JAR.
Une archive distribuée peut être inspectée.
Mesurer la bonne santé du service
Un port ouvert démontre qu’un processus écoute, pas que le parcours métier fonctionne. Le contrôle de santé doit correspondre à une garantie définie : identité de version, démarrage applicatif et dépendances utiles selon son périmètre. Évitez les mutations réelles dans ce contrôle. Un résultat vert doit rester limité à ce qui a été observé et ne pas remplacer un test d’autorisation ou de restauration.
Erreur fréquente à comprendre Un port ouvert prouve tout fonctionnement métier.
L’observation doit correspondre au contrat du service.
Coordonner le retour arrière avec les données
Remplacer le JAR par une version antérieure ne restaure pas automatiquement la base ou les messages déjà publiés. Une migration peut rendre l’ancien code incompatible avec l’état courant. Définissez compatibilité, procédure de reprise et critères d’acceptation avant la bascule. Un exercice doit employer des données représentatives et vérifier les invariants, plutôt que conclure à la réversibilité parce qu’un ancien fichier existe encore.
Erreur fréquente à comprendre Remplacer le JAR restaure automatiquement toute base.
Les états persistants suivent un autre cycle.
Atelier guidé : lire, expliquer et vérifier le code
module lab.orders {
exports lab.orders.api;
}
La déclaration de l’exercice exporte le package API du module. Elle organise l’accès prévu des consommateurs qui lisent le module, mais ne fournit pas à elle seule une ouverture générale de tous les membres privés à la réflexion profonde. Le nom du module et celui du package ont des rôles distincts, à vérifier dans un projet où le package existe réellement.
Créez un petit consommateur séparé et essayez un accès à une classe d’API puis à une classe d’implémentation non exportée. Si un outil de réflexion est nécessaire, définissez et testez l’ouverture ciblée correspondante. Construisez ensuite le livrable et lancez-le hors de l’IDE. Vous distinguerez ainsi encapsulation du code, résolution des dépendances et disponibilité effective des ressources au runtime.
Exemple commenté
module reservations.core {
exports com.example.reservations.api;
}
// Compilation depuis la racine du projet de laboratoire :
// javac --release 21 -d out $(find src -name "*.java")
Le descripteur expose un package d’API et peut garder les implémentations internes non exportées. La commande commentée suppose une arborescence de laboratoire appropriée.
Exercice de mise en pratique
Découpez API, implémentation et application en modules puis testez un accès interdit à un package interne.
Voir une proposition de correction
Exportez seulement les types contractuels, déclarez requires côté consommateur et utilisez un service si l’implémentation doit être interchangeable. Vérifiez compilation et lancement du JAR hors IDE.
- Distinguer exports et opens.
- Tester l’artefact dans un environnement propre.
- Observer les diagnostics de compilation.
Contrôle de connaissances
Une classe public d’un package non exporté est-elle librement accessible à tous les modules ?
Réponse expliquée : Non, l’export du package compte aussi — L’accessibilité modulaire complète les modificateurs Java.
Cours associé : Modules, services, packaging et diagnostic ↗
Documentation de référence ↗ https://dev.java/learn/modules/