Vous êtes sur la page 1sur 11

Notes de cours de Java - Cours 7

8 novembre 2023

Table des matières

1 Les Collections et les Maps 1

1 Les Collections et les Maps


Les listes, les ensembles, les piles, les files d’attente sont des objets qui re-
groupent plusieurs éléments en une seule entité. On appelle ce type d’objet
des collections. Ces objets ont beaucoup de choses en commun. En les mani-
pulant, on va notamment se poser souvent les mêmes questions (e.g., est-ce
qu’elles contiennent des éléments ? combien ?) et réaliser les mêmes opéra-
tions (e.g., on peut ajouter ou enlever un élément à la structure, on peut vider
la structure. On peut aussi parcourir les éléments contenus dans la structure).
Ces collections ont aussi des comportements ou des propriétés différentes.
Enfin pour chacune d’elles, nous avons des implémentations différentes.
Pour organiser toutes ces structures différentes, Java utilise une hierar-
chie d’interfaces comme représentée dans la Figure 1.
L’interface Collection regroupe les méthodes de base pour parcourir,
ajouter, enlever des éléments. C’est l’interface dont héritent toutes les col-
lections. L’interface Set représente un ensemble, et donc, ce type de col-
lection n’admet aucun doublon et n’est pas ordoné. L’interface SortedSet
est la version ordonnée d’un ensemble. L’interface List représente une sé-
quence d’éléments. L’ordre d’ajout ou de retrait des éléments y est important
est les doublons sont possibles. L’interface Queue permet de representer des
files d’attente. Il y a l’élément en tête et ceux qui suivent. L’ordre d’ajout ou
de retrait des éléments est important et les doublons sont possibles. Le pre-
mier élément à sortir de la structure est le premier élément à y être rentré.

1
Iterable<E>

Collection<E> Map

Set<E> List<E> Queue<E> Deque<E> SortedMap

SortedSet<E>

F IGURE 1 – Hierarchie d’interfaces pour organiser les collections et les maps.

L’interface Deque permet de représenter des files d’attentes où les éléments


peuvent rentrer et sortir des deux cotés de la structure.
L’interface Map permet de représenter une relation binaire : chaque élé-
ment est associé à une clé. Chaque clé est unique mais on peut avoir des dou-
blons pour les éléments. , Une map n’est pas une collection. SortedMap est
la version ordonnée d’une relation binaire ou les clés sont ordonnées.
Toures ces interfaces sont génériques : on peut leur donner un paramètre
pour indiquer qu’on a une collection de Velo, de Integer, de String,
etc...

Implémentations. Pour chacune des interfaces, il existe plusieurs implé-


mentations comme indiqué dans les Figures 2 et 3. Par exemple, on observe
que l’interface List peut être implémentée à l’aide d’une liste doublement
chaînée (classe LinkedList) ou d’un tableau dynamique (classe ArrayList).

Collection

Set SortedSet List Queue Deque

et t t e t
t hS et Lis is qu is
Se a s
e S y d L De dL
sh ed
H
re rra ke ay ke
Ha n k T A Lin
Ar r
L in
Li

F IGURE 2 – Plusieurs implémentations sont possibles pour utiliser chaque


type de collection. On représente ici quelques implémentations possibles.

2
Map

ap
p p hM
Ma Ma s
a SortedMap
sh ee dH
Ha Tr nk
e
Li

F IGURE 3 – Plusieurs implémentations sont possibles pour utiliser chaque


type de map. On représente ici quelques implémentations possibles.

Parcourir une collection


La boucle for each. Comme indiqué dans la Figure 1, toutes les collec-
tions héritent de l’interface Iterable. Tout objet implémentant cette inter-
face peut utiliser une boucle “for each”.

1 Collection<E> maCollection;
2 ...
3 for (E <nom> : maCollection)
4 // block d'instructions

Dans le code ci-dessus, nous avons une une collection maCollection qui
contient des objets de type E. On va accéder à chaque élément de la collection
maCollection en utilisant une boucle “for each”. Chaque élément sera
stocké dans une variable <nom> de type E (évidemment).
Un autre exemple où le type E est remplacé par le type Velo est donné
ci-dessous.

1 List<Velo> velos =
2 new ArrayList<Velo>();
3
4 velos.add(new Velo("Poulidor"));
5 velos.add(new Velo("Merckx"));
6 velos.add(new Velo("Hinault"));
7 velos.add(new Velo("Jalabert"));
8 velos.add(new Velo("Alaphilippe"));
9
10 for (Velo v: velos)
11 System.out.println(v);

3
L’interface Iterator. Un objet qui implémente l’interface Iterator est dé-
dié au parcourt d’éléments dans une collection. On obtient un tel objet en uti-
lisant la méthode iterator() d’un objet implémentant l’interface Iterable.

1 public interface Iterator<E> {


2 boolean hasNext();
3 E next();
4 void remove(); //optional
5 }

L’interface générique Iterator a trois méthodes :


— hasNext() retourne un boolean qui indique s’il reste des éléments
à visiter ou non.
— next() donne accès à l’élément suivant.
— remove() permet d’enlever l’élément courant de la collection.
L’utilisation d’objets Iterator a plusieurs avantages. Ils permettent sou-
vent de meilleurs compléxités (linéaires) pour le parcours d’une collection. Ils
permettent d’enlever un élément d’une collection lors d’un parcours. Ils per-
mettent également de parcourir plusieurs collections en parallèle.
Le code ci-dessous illustre l’utilisation d’un Iterator pour parcourir
une liste de Velo.

1 List<Velo> velos
2 = new ArrayList<Velo>();
3 velos.add(new Velo("Poulidor"));
4 velos.add(new Velo("Merckx"));
5 velos.add(new Velo("Hinault"));
6 velos.add(new Velo("Jalabert"));
7 velos.add(new Velo("Alaphilippe"));
8
9 Iterator<Velo> it = velos.iterator();
10 while (it.hasNext()){
11 Velo v = it.next();
12 if (v.getName().equals("Merckx"))
13 it.remove();
14 else
15 System.out.println(v);
16 }

Parcours d’un Map. Un Map n’est pas une collection. C’est donc un ob-
jet plus difficile à parcourir. , En effet, Map n’est pas une sous interface de
Iterable, donc on ne peut pas parcourir un Map avec une boucle for

4
each ni avec un Iterator ! Pour présenter un peu ce type d’objet, voici
quelques descriptions des méthodes qu’il peut utiliser.

Interface Map<K,V>
V get(Object key)
Returns the value to which the specified key is map-
ped, or null if this map contains no mapping for the
key.
V put(K key, V value)
Associates the specified value with the specified key
in this map. If the map previously contained a map-
ping for the key, the old value is replaced by the spe-
cified value.
V remove(Object key)
Removes the mapping for a given key if it is present.
default V replace(K key, V value)
Replaces the entry for the specified key only if it is
currently mapped to some value. Returns the value
to which this map previously associated the key, or
null if the map contained no mapping for the key.
Pour parcourir un Map, on peut se ramener à une Collection en utilisant
certaines méthodes. En effet, on peut obtenir l’ensemble des clés, l’ensemble
des valeurs, et l’ensemble des paires (clé,valeur) grace aux méthodes suivantes :
— Set<K> keySet() donne l’ensemble des clés ;
— Collection<V> values() donne la collection des valeurs ;
— Set<Map.Entry<K,V» entrySet() donne l’ensemble des paires
clé-valeur.

L’interface Map.Entry. L’interface Map.Entry désigne une interface Entry


qui est interne à l’interface Map. Cette interface permet de représenter une
paire clé-valeur d’un Map. Voici la description de certaines de ces méthodes.

Interface Map.Entry<K,V>
K getKey()
Returns the key corresponding to this entry.
V getValue()
Returns the value corresponding to this entry.
V setValue(V value)
Replaces the value corresponding to this entry with the
specified value (optional operation).

5
Grâce à l’interface Map.Entry nous pouvons parcourir un Map comme
illustré ci-dessous. Dans cet exemple, on utilise la méthode entrySet pour
se ramener à une collection et utiliser une boucle for each.

1 Map<Velo,Marque> catalogue = new HashMap<>();


2 ...
3 for (Map.Entry<Velo,Marque> paire:
4 catalogue.entrySet()){
5 Velo v = paire.getKey();
6 Marque m = paire.getValue();
7 if (m.getName().equals("Giant"))
8 System.out.println(v);
9 }

Dans cet exemple, on part d’une Map qui associe à chaque velo sa marque et
on affiche seulement les velos qui sont de marque Giant.

Performances sur des opérations. En fonction de l’implémentation que vous


allez choisir, les opérations que vous allez réaliser seront plus-ou-moins effi-
caces. Vous devez donc bien chosir vos implémentations car, même pour de
larges volumes de données, les méthodes usuelles (add, remove, contains,
size) devraient être rapides.
Comparons les compléxités des opérations add et set des implémenta-
tions LinkedList et ArrayList de l’interface List.
add set autre
LinkedList temps constant n
ArrayList temps constant? temps constant n
La classe LinkedList stock les éléments d’une liste dans une liste dou-
blement chainée. Ajouter un élément au début de la liste, à la fin de la liste
ou enlever un élément (une fois trouvée) se fait en temps constant. Cepen-
dant, changer la valeur d’un élément à un indice particulier demande de se
rendre à cet indice ce qui a un coût en O(n). La classe ArrayList utilise un
tableau. Ainsi ajouter un élément ou changer la valeur à un indice particulier
se fait en O(1). En fait, ce n’est pas tout à fait vrai car si en ajoutant un élément
on dépasse la taille du tableau alors il faudra reallouer un tableau plus grand
ce qui à un coût en O(n). Enlever un élément d’un ArrayList ou rajouter
un élément à un indice donné a également un coût en O(n) car il faut alors
décaler une partie du tableau. Voici quelques exemples des différences entre
ArrayList et LinkedList.

Comparons les compléxités des opérations add, remove et contains


des implémentations HashSet et TreeSet de l’interface Set.

6
add remove contains
? ?
HashSet temps constant temps constant temps constant?
TreeSet log(n) log(n) log(n)
La classe TreeSet stock les éléments d’un ensemble (ordonné) dans une
structure de donnée appelée arbre rouge-noir. Un arbre rouge noir permet
des compléxités logarithmiques en n pour les opérations usuelles du Set où
n est le nombre d’éléments dans l’ensemble. La classe HashSet utilise une
table de hachage. En pratique les opérations sont très rapides. Ajouter un élé-
ment ou enlever un élément se fait en temps constant (une fois l’opération
de recherche effectuée). Cependant, la compléxité pire cas de l’opération de
recherche va dépendre du facteur de charge comme nous allons l’expliquer
maintenant.

Table de hachage et gestion des collisions. Supposons que nous voudrions


stocker un ensemble de k éléments qui ont une clé unique. Les clés peuvent
prendre valeur dans un univers U . Si U est grand, il n’est pas pratique, voir
impossible, de faire un tableau de taille U pour stocker un élément qui a pour
clé n dans la n i eme case du tableau. Si k << |U |, on peut utiliser une fonction
de hachage h et l’élément qui a pour clé i sera stocké dans la case h(i ). On
dit aussi que h(i ) est la valeur de hachage de i . , Malheureusement, deux clés
peuvent avoir la même valeur de hachage. On a alors une collision.

Une solution pour résoudre ces collisions consiste à utiliser une liste dou-
blement chainée : on stocke tous les éléments qui ont une même valeur de
hachage dans une liste chaînée. Chaque case de la table de hachage contient
alors une référence vers la tête de la liste. En fait à partir de Java 8, quand
une case de la table de hachage contient trop d’éléments, cette liste chainée
est remplacée par une arbre binaire de recherche.
Supposons qu’on ait k valeurs de hachage et n éléments. Dans le pire cas,
les n éléments ont la même valeur de hachage, on aura alors simplement une
longue liste chainée. D’un point de vue algorithmique la table de hachage
perd alors tout intérêt ! Dans la situation idéale, les n éléments sont répartis
uniformément sur les k valeurs de hachage. Si on récapitule.
— l’insertion d’un élément se fait donc en O(1) (il faut d’abord vérifier que
l’élément n’est pas contenu si on a un Set.)
— enlever un élément se fait en O(1) si la liste est doublement chaînée.
— cherche un élément se fait en Θ(1+α) où α est le nombre moyen d’élé-
ments stockés dans une chaîne. α est aussi appelé facteur de charge.

7
Pour que notre table de hachage soit la plus performante possible, on vou-
drait une fonction qui distribue de manière uniforme toutes les clés dans cha-
cun des k valeurs possibles de hachage. Cependant, cette tâche est difficile
car on ne connait que rarement la probabilité qu’on ait un élément avec une
certaine clé. Il existe des techniques pour générer de bonnes fonctions de ha-
chage, mais ce n’est pas l’objet de ce cours.

Retour sur HashCode() de la classe Object Pour rappel, la classe Object


possède une méthode hashCode() qui retourne un int. La méthode hashCode
est la fonction de hachage qui sera utilisée par la classe ! , Si vous utilisez un
HashSet ou un HashMap, l’implémentation de hashCode() est très im-
portante.
Quelques conseils :
— Si vous pouvez vous ramener à des fonctions de hachage codées dans
Java, cela pourra avoir de bonnes performances. Par exemple, la classe
String possède une bonne méthode de hachage.
— Si votre fonction de hachage est constante, vous perdez tout bénéfice
d’utiliser ces structures.
— Si votre fonction de hachage n’est pas cohérente par rapport à votre
méthode equals, vous risquez de ne pas trouver un élément pourtant
présent ! Si this.equals(obj) retourne vrai, on doit aussi avoir
this.hashCode()==obj.hashCode().
— Certains IDEs (comme eclipse) peuvent générer des méthodes de ha-
chage.
— Il existe une méthode Objects.hash(Object... values) qui
calcule la valeur de hachage des ses arguments et les combine automa-
tiquement. Cette méthode est pratique pour écrire de nouvelles fonc-
tions de hachage.

Un mot sur la boucle for each.

1 for (Velo v : velos)


2 System.out.println(v);

La boucle for each est la plus simple à utiliser pour écrire rapidement
une boucle. Lors de la compilation, Java va traduire cette ligne en utilisant
un Iterator. , Attention à l’intérieur de la boucle, on ne peut pas modifier
la collection (par exemple avec remove()). Si vous modifiez la collection en
cours de parcours, cela causera une ConcurrentModificationException.
La bonne façon de procéder consiste alors à utiliser un Iterator.

8
Ordre
L’interface Comparable. L’interface Comparable contient une seule mé-
thode :

1 public int compareTo(T o)

Cette méthode retourne 1) un entier négatif si l’objet est plus petit que
l’objet passé en paramètre ; 2) zéro s’ils sont égaux ; 3) un entier positif si l’ob-
jet est plus grand que l’objet passé en paramètre. Les classes String, Integer,
Double, Date, GregorianCalendar et beaucoup d’autres implémentent
toutes l’interface Comparable.
Par exemple, en TD vous avez creez une classe Box qui implémente l’in-
terface Comparable.

1 public class Box


2 implements Comparable<Box>{
3 private int absHG ;
4 private int ordHG ;
5 ...
6
7 public int compareTo(Box b) {
8 return this.ordHG - b.ordHG;
9 }

L’interface Comparator. Une classe qui implémente l’interface compa-


rator représente une notion d’ordre / un critère d’ordre. A priori, on n’a be-
soin d’implémenter une seule méthode, la méthode pour comparer deux élé-
ments.

1 public interface Comparator<T> {


2 int compare(T o1, T o2);
3 boolean equals(Object obj);
4 }

Par exemple, Pour comparer des Box, selon ordHG, on peut écrire la classe
suivante :

1 public class OrdreOrdHG implements Comparator<Box> {


2 public int compare(Box b1, Box b2){
3 return (b1.getOrdHG() < b2.getOrdHG()) ? -1:
4 ((b1.getOrdHG() == b2.getOrdHG()) ? 0 : 1);

9
5 }
6 }

Trier les éléments d’une collection. Pour trier une collection, différentes
méthodes sont possibles. Premièrement, nous pouvons utiliser la méthode
sort de l’interface List (si nous avons une List). Cette méthode prend en
paramètre un objet Comparator.

1 void sort(Comparator<? super E> c)

Description : Sorts this list according to the order induced by the specified
Comparator. All elements in this list must be mutually comparable using
the specified comparator. If the specified comparator is null then all ele-
ments in this list must implement the Comparable interface and the ele-
ments’ natural ordering should be used.

Ainsi si on appel cette méthode avec le paramètre null, il vaudrait mieux


que la liste contienne des instances d’une classe implémentant l’interface Comparable,
sinon, Java ne va pas savoir comment comparer deux éléments ! En utilisant
une instance d’une classe implémentant l’interface Comparator en para-
mètre, on donne directement le critère pour comparer les éléments.

Une deuxième méthode consiste à utiliser certaines méthodes de la classe


Collections (attention avec un s). En effet, deux méthodes de tri y sont
implémentées.
— La première méthode a un seul argument : une collection d’instance
d’une classe qui implémente l’interface Comparable. Le tri se fait
donc en utilisant la méthode compareTo codée dans la classe T.
— La seconde méthode nécessite deux arguments : la collection d’ins-
tance d’une classe T et une notion d’ordre sur la classe T. Le tri se
fera donc en utilisant la méthode compare codée dans la classe im-
plémentant Comparator.
Voici les signatures de ces méthodes :

1 // classe Collections
2 public static <T extends Comparable<? super T>>
3 void sort(List<T> list)
4 public static <T> void sort
5 (List<T> list, Comparator<? super T> c)

Et, pour finir, voici un exemple d’utilisation de ces méthodes :

10
1 public static void main(String[] args){
2 List<Velo> velos = new ArrayList<>();
3 ...
4
5 for (Velo v: velos)
6 System.out.println(v.presentation());
7 velos.sort(null);
8 //ou Collections.sort(velos);
9 for (Velo v: velos)
10 System.out.println(v.presentation());
11
12 Comparator<Velo> ordre = new OrdrePuissance();
13 velos.sort(ordre);
14 //ou Collections.sort(velos,ordre);
15 for (Velo v: velos)
16 System.out.println(v.presentation());
17 }

11

Vous aimerez peut-être aussi