← Java SE 21 — préparation OCP

Module 3 / 8 · 90 min

Tableaux, génériques et collections

Choisir le conteneur et comprendre les bornes de types.

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

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.

Points de vérification

  • Connaître ordre et unicité.
  • Ne pas confondre covariance des tableaux et génériques.
  • Tester collections vides et null.

Contrôle de connaissances

Cours associé : Tableaux, génériques et collections ↗
List.of permet-elle add après création ?

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 →