← Java SE 21 — préparation OCP

Module 2 / 8 · 90 min

Objets, héritage, records et interfaces

Expliquer surcharge, redéfinition et immutabilité des objets.

Votre progression apparaîtra ici lorsque JavaScript est actif.

La progression et le meilleur score sont enregistrés uniquement dans ce navigateur. Aucun compte n’est nécessaire ; effacer les données du navigateur les supprime. Les durées de modules sont indicatives et incluent les exercices.

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.

Points de vérification

  • Distinguer surcharge et redéfinition.
  • Inspecter les références mutables des records.
  • Utiliser Override pour vérifier l’intention.

Contrôle de connaissances

Cours associé : Objets, héritage, records et interfaces ↗
Un record contenant une List mutable est-il profondément immuable par défaut ?

Documentation de référence ↗

Validation du module

101 questions, objectif pédagogique de 75 %. Durée conseillée de 150 minutes, sans limite imposée. Les questions et les choix sont mélangés ; le corrigé apparaît après validation. Le contrôle rapide de la leçon et l’examen final du cours restent des exercices distincts.

Commencer ou reprendre les 101 questions →