Ch. 7-8 — Interfaces et collections
Le contrat d'interface et l'implémentation multiple contre le problème du diamant, List/Set/Map et leurs complexités, le contrat equals/hashCode, les parcours avec Iterator, Comparable/Comparator et final sous toutes ses formes.
Le problème du diamant
Un Frogosaur héritant à la fois de Frog et de Dinosaur, eux-mêmes héritiers de
Animal qui déclare talk() : quelle définition exécuter ? Ce « losange de la mort »
rend l'héritage multiple de classes impraticable — et le transtypage explicite ne sauve
rien. La réponse de Java : héritage simple de classes, implémentation multiple
d'interfaces.
Une interface est un type
Une interface est un ensemble de déclarations de signatures de méthodes publiques. Une classe qui l'implémente doit définir un comportement pour chacune — et ses instances peuvent alors être vues comme du type de l'interface :
public interface Recyclable { public void recycle(); }
public class Paper implements Recyclable, Flammable {
public void tear() { /* ... */ }
public void recycle() { /* ... */ }
public void burn() { /* ... */ }
}Trois conséquences, toutes illustrées par le cours :
- une référence typée interface n'autorise que les méthodes du contrat : après
Recyclable r = new Paper(),r.recycle()compile maisr.tear()est refusé, même si l'objet réel sait déchirer — le compilateur ne laisse rien passer ; instanceofest vrai pour toutes les interfaces implémentées, comme pour toute la branche de classes (Paper,Recyclable,Flammable,Objectrépondent toustrue) ;- une méthode paramétrée par l'interface —
aMethod(Recyclable r)— accepte tout type implémentant, là où la surcharge dupliquerait le code et où une surcharge surObjectimposerait un downcast à chaque appel.
Pour invoquer le spécifique (tear() sur une référence Recyclable), un downcast
explicite est nécessaire : code compilable, mais pas forcément exécutable — c'est le
late binding du chapitre 4 qui revient.
List, Set, Map : choisir sa structure
Collection<E> est le socle commun (implémentée par 33 classes) : add, contains,
remove, size, isEmpty, toArray… Map<K,V> vit à part : association clé unique →
valeur, avec put, get, containsKey, keySet, entrySet, values. Aucune de ces
interfaces ne s'instancie — on instancie leurs implémentations. Et ne pas confondre
Collection (l'interface) avec Collections, la boîte à outils de méthodes statiques :
sort, reverse, shuffle, frequency, disjoint…
| Interface | Implémentations | À retenir |
|---|---|---|
List | ArrayList, LinkedList | doublons autorisés, ordonnée (≠ triée), accès par index |
Set | HashSet, LinkedHashSet, TreeSet | unicité, ni doublons ni indice ; add sur un doublon n'a aucun effet |
Map<K,V> | HashMap, LinkedHashMap, TreeMap | association clé → valeur ; ordre : aucun / insertion / naturel |
Le choix de l'implémentation suit les opérations dominantes — les complexités du cours
tranchent. Côté listes : get() en O(1) pour ArrayList contre O(n) pour LinkedList,
mais remove(0) en O(1) pour LinkedList contre O(n) — on choisit ArrayList quand les
ajouts se font en fin de collection, LinkedList quand ils se font à position aléatoire.
Côté ensembles et tables : HashSet/LinkedHashMap sont en O(1) quand
TreeSet/TreeMap paient leur tri en O(log n).
Le contrat equals/hashCode
Les structures HashXXX rangent les objets d'après hashCode(). Le contrat : deux
objets égaux doivent retourner le même hashcode — l'inverse n'est pas vrai.
Concrètement, equals() et hashCode() doivent être définis de manière coordonnée,
sur les mêmes attributs, sans quoi deux objets égaux atterrissent dans deux cases
différentes et les recherches échouent.
Parcours : for-each, Iterator et Iterable
Le for-each parcourt listes, ensembles et files — pas les Map, qu'on traverse via
keySet() (les clés) ou entrySet() (les couples Map.Entry<K,V>). Sa limite :
modifier la structure pendant le parcours lève une
ConcurrentModificationException. L'itérateur explicite résout le cas :
Iterator<Integer> it = myList.iterator();
while (it.hasNext()) {
int tmp = it.next();
if (tmp % 2 == 0) { it.remove(); }
}Les itérateurs sont fail-fast : toute modification de la collection autrement que par
leurs méthodes dédiées est signalée. ListIterator ajoute le parcours arrière
(previous, hasPrevious, indices) et l'insertion (add, set, remove). Rendre un
objet itérable se fait en implémentant Iterable<T> et sa méthode iterator() —
le cours le fait sur Student implements Iterable<Double>, itérable sur ses notes :
for (Double grade : s) fonctionne alors sans exposer la liste interne.
Comparable et Comparator
Comparable<T> et sa méthode compareTo fixent l'ordre naturel d'une classe : c'est
lui qu'invoque Collections.sort(list) sans second argument — l'exemple du cours trie
des ComparablePerson par nom. Quand un seul critère ne suffit pas (parfois le nom,
parfois l'âge), Comparator<T> et sa méthode compare(o1, o2) définissent un critère
externe : ForenameComparator, AgeComparator, passés à
sort(list, new AgeComparator()). Les méthodes des boîtes à outils sont surchargées pour
accepter les deux.
final et héritage d'interfaces
final revient sous toutes les formes : constante initialisée à la déclaration ou au
constructeur ; paramètre non réaffectable (on peut toujours modifier le contenu de
l'objet ou du tableau reçu, jamais le réallouer) ; méthode impossible à redéfinir —
utile pour empêcher qu'une sous-classe ne double les crédits d'un compte ; classe
impossible à dériver, dont toutes les méthodes sont alors implicitement final (comme
les méthodes private, que « redéfinir » crée sans aucun lien).
Entre interfaces, l'héritage est possible et multiple : une interface peut étendre
plusieurs interfaces. Hériter plusieurs fois des mêmes signatures par des chemins
différents est possible mais risqué (conflits, ambiguïtés) : à éviter. Depuis Java 8,
les méthodes default offrent enfin une implémentation par défaut, redéfinissable ou
non — à manier avec prudence mélangée à l'implémentation multiple.
A retenir
- interface = contrat + type ; héritage simple de classes, implémentation multiple d'interfaces ;
- List/Set/Map : le choix suit les opérations dominantes (accès direct ? unicité ? association clé → valeur ?) ;
hashCodecohérent avecequals, sur les mêmes attributs ;- itérateur fail-fast,
it.remove()pour supprimer en parcourant ; - Comparable = ordre naturel, Comparator = critère alternatif.
Parce que le compilateur ne connaît que le type de la référence : Recyclable
déclare seulement recycle(). Le spécifique exige un downcast explicite
((Paper) r).tear() — avec le risque d'échouer à l'exécution si l'objet réel n'est
pas un Paper.
Ajouts/suppressions essentiellement en fin de collection et accès direct fréquent :
ArrayList. Insertions/suppressions à position aléatoire ou en tête : LinkedList,
qui met à jour un chaînage au lieu de décaler des éléments.
La boucle s'appuie sur un itérateur interne qui détecte les modifications faites hors
de lui et lève ConcurrentModificationException plutôt que risquer l'incohérence —
les itérateurs sont fail-fast. Utilisez it.remove() dans une boucle explicite.
La première invoque compareTo — l'ordre naturel défini par la classe elle-même
via Comparable ; la seconde applique un critère externe via Comparator, sans
toucher à la classe triée — pratique quand plusieurs tris coexistent.
coursOO_07.pdf
R2.01 · Cours 7 — Héritage, interface et structure de données · 38 diapositives
Chargement du document…
coursOO_08.pdf
R2.01 · Cours 8 — Interface, comparabilité, itérabilité · 33 diapositives
Chargement du document…
Dernière mise à jour le
Ch. 5-6 — Héritage et mécanismes fins
extends, super et protected pour factoriser l'état et le comportement, type réel contre type effectif, lookup dynamique et masquage d'attributs — jusqu'aux classes abstraites.
Ch. 9 — Entrées / sorties
Les flux de java.io : lire la nomenclature Reader/Writer et InputStream/OutputStream, tamponner, écrire avec PrintWriter, sérialiser des objets et analyser avec Scanner.