Notes S3
Notes sur les Collections Java
1. Introduction aux Collections
Une collection est un objet qui groupe plusieurs éléments en une seule unité.
- Listes (
List): Collection ordonnée qui autorise les doublons.- Exemples :
ArrayList,LinkedList.
- Exemples :
- Ensembles (
Set): Collection non ordonnée qui n’autorise pas les doublons.- Exemples :
HashSet,TreeSet.
- Exemples :
- Tables associatives (
Map): Groupe d’associations clé/valeur. Les clés sont uniques.- Exemples :
HashMap,TreeMap.
- Exemples :
2. L’interface Collection<E>
C’est l’interface racine de la hiérarchie des collections. Elle est générique, E représentant le type des éléments.
Attention: ne pas confondre l’interface Collection (sans s) et la classe utilitaire Collections (avec un s).
public interface Collection<E> extends Iterable<E> {
// Méthodes de consultation
int size();
boolean isEmpty();
boolean contains(Object o);
boolean containsAll(Collection<?> c);
// Méthodes de modification
boolean add(E e);
boolean addAll(Collection<? extends E> c);
boolean remove(Object o);
boolean removeAll(Collection<?> c);
boolean removeIf(Predicate<? super E> filter);
boolean retainAll(Collection<?> c);
void clear();
// Itération
Iterator<E> iterator();
}- Pourquoi
add(E e)retourne-t-il unboolean?- La méthode retourne
truesi la collection a été modifiée suite à l’appel. - Pour une
List, c’est (presque) toujourstrue. - Pour un
Set, si l’élément est déjà présent (doublon),addne fait rien et retournefalse.
- La méthode retourne
UnsupportedOperationException: Certaines méthodes de modification peuvent lancer cette exception si la collection n’est pas modifiable (p.ex., une liste créée viaCollections.unmodifiableList).
Les méthodes qui modifient une collection (add, remove, etc.) sont “optionnelles”: une collection peut choisir de les supporter ou de lever UnsupportedOperationException.
3. Les Listes (List<E>)
Une liste est une collection ordonnée (une séquence). On peut accéder aux éléments par leur index.
Implémentations principales
ArrayList: Basée sur un tableau redimensionnable.- Accès direct (get/set) : Très rapide, en temps constant O(1).
- Ajout/Suppression : Lent, surtout au début ou au milieu, car il faut décaler les éléments. Temps linéaire O(n).
LinkedList: Basée sur une liste doublement chaînée (chaque élément a une référence vers le suivant et le précédent).- Accès direct (get/set) : Lent, car il faut parcourir la liste depuis le début ou la fin. Temps linéaire O(n).
- Ajout/Suppression : Très rapide (si on a déjà une référence à l’élément), surtout en début ou fin de liste. Temps constant O(1).
Quand choisir ?
ArrayList: Le choix par défaut. Idéal si vous accédez souvent aux éléments par leur index.LinkedList: Idéal si vous faites de nombreux ajouts/suppressions en début/fin de liste.
Attention: l’ajout/suppression en LinkedList est O(1) seulement si on est déjà au bon endroit (via un ListIterator) ou en tout début/fin.
Méthodes notables de List<E>
public interface List<E> extends Collection<E> {
// Accès par index
E get(int index);
E set(int index, E element);
// Ajout/Suppression par index
void add(int index, E element);
E remove(int index);
// Recherche
int indexOf(Object o);
int lastIndexOf(Object o);
// Vues
List<E> subList(int fromIndex, int toIndex);
}4. Itération
À éviter: parcourir une LinkedList avec get(i) dans une boucle for → coût total O(n²). Préférer for-each/itérateur → O(n).
Iterable et Iterator
- L’interface
Iterable<E>oblige une classe à fournir unIterator. Toutes les collections standard sontIterable. - Elle permet d’utiliser la syntaxe “for-each” :
for (Element e : collection) { ... }.
L’interface Iterator<E> fournit une manière standard de parcourir une collection :
public interface Iterator<E> {
boolean hasNext(); // Y a-t-il un élément suivant ?
E next(); // Retourne l'élément suivant et avance
void remove(); // Supprime le dernier élément retourné par next()
}For-each vs. Iterator
La boucle for-each est réécrite par Java en utilisation d’un Iterator: même complexité, mais plus lisible.
Règle: utiliser for-each sauf si on a besoin d’accéder directement à l’itérateur (p.ex. pour supprimer pendant le parcours).
- Boucle
for-each: Plus simple et lisible. À utiliser dans la majorité des cas.java for (String s : myList) { System.out.println(s); } Iteratorexplicite: Nécessaire quand on doit modifier la collection pendant l’itération.- Utiliser
iterator.remove()est la seule manière sûre de supprimer un élément d’une collection pendant qu’on la parcourt. Tenter de le faire aveccollection.remove()dans une boucle for-each lèvera uneConcurrentModificationException.
Iterator<String> it = myList.iterator(); while (it.hasNext()) { String s = it.next(); if (s.isEmpty()) { it.remove(); // OK } }- Utiliser
5. Les Ensembles (Set<E>)
Un Set est une collection qui ne contient aucun élément en double.
HashSet:- Stocke les éléments dans une table de hachage.
- Performances: Très rapide (temps constant O(1) en moyenne) pour
add,remove, etcontains. - Ordre: Ne garantit aucun ordre d’itération.
TreeSet:- Stocke les éléments dans un arbre équilibré (rouge-noir).
- Performances: Moins rapide (O(log n)) pour
add,remove, etcontains. - Ordre: Maintient les éléments triés selon leur ordre naturel ou un
Comparatorfourni.
6. Piles, Files et Deques
- Pile (
Stack): Logique LIFO (Last-In, First-Out). On ajoute (push) et retire (pop) des éléments au même bout (le sommet). - File (
Queue): Logique FIFO (First-In, First-Out). On ajoute (offer) à une extrémité et on retire (poll) de l’autre. - Deque (
Deque): “Double-ended queue”. Combine les deux : on peut ajouter et retirer des deux côtés (addFirst,removeFirst,addLast,removeLast).
L’implémentation ArrayDeque est la classe à privilégier pour implémenter une pile ou une file. Elle est plus performante que les anciennes classes Stack (qui est synchronized) et LinkedList.
Règle: pile/file/deque → ArrayDeque. Liste “générale” → plutôt ArrayList (si get/set dominent ou ajouts en fin), sinon LinkedList.
7. Vues sur les collections
Une vue est un objet qui offre une représentation alternative d’une autre collection. Les modifications via la vue modifient la collection d’origine, et vice-versa.
subList(b, e): Crée une vue sur une portion d’une liste.- Toute modification structurelle (ajout/suppression) sur la liste d’origine invalide la
subList.
- Toute modification structurelle (ajout/suppression) sur la liste d’origine invalide la
Arrays.asList(a): Crée une vue de typeListsur un tableau.- On peut modifier les éléments (
set), mais pas la taille (pas deadd/remove). Le tableau sous-jacent est modifié.
- On peut modifier les éléments (
Collections.unmodifiableList(l): Crée une vue non modifiable d’une liste.- Toute tentative de modification via cette vue (
add,set,remove) lèvera uneUnsupportedOperationException. - Attention: Si la liste d’origine est modifiée, les changements sont visibles dans la vue non modifiable.
List.copyOf(...)(copie immuable) et pas seulementunmodifiableList(vue).- Toute tentative de modification via cette vue (
8. Cas pratique : LongestWord
Problème: Charger un dictionnaire de mots dans une liste, puis trouver le mot le plus long.
- Avec
ArrayListetaddFirst:- Si on charge les mots un par un en les ajoutant au début (
list.add(0, word)), chaque ajout coûte O(n) car il faut décaler tout le tableau. Le chargement total est très lent : O(n²).
- Si on charge les mots un par un en les ajoutant au début (
- Avec
LinkedList:- Le chargement avec
addFirst(ouaddLast) est instantané : O(1) pour chaque mot. Le chargement total est donc rapide : O(n). - MAIS, si on cherche ensuite le mot le plus long avec une boucle
forclassique et des appels àget(i):java // ANTI-PATTERN pour LinkedList for (int i = 0; i < list.size(); i++) { if (list.get(i).length() > maxLength) { /* ... */ } }Chaque appel àlist.get(i)coûte O(n). La boucle entière devient O(n²).
- Le chargement avec
- La bonne solution (
LinkedList+ for-each):- On utilise une
LinkedListpour le chargement rapide O(n). - On parcourt la liste avec une boucle
for-each, qui utilise un itérateur en coulisses.java // Solution efficace for (String word : list) { if (word.length() > maxLength) { /* ... */ } } - Le parcours avec un itérateur est linéaire. La recherche du mot le plus long coûte donc O(n).
- Le coût total (chargement + recherche) est O(n).
- On utilise une