Cours BUT Info

TD 1 — Set et Map

Ensembles aléatoires, intersection, union et réductions sur un Set, puis un annuaire de téléphones en Map — corrigé complet, compilé et exécuté, avec les traces d'exécution.

Ce premier TD met en pratique les deux grandes interfaces du chapitre 1 : l'interface Set (exercice 1) et l'interface Map (exercice 2). Aucune correction n'était fournie avec le sujet : les solutions ci-dessous ont été écrites, compilées et exécutées — les sorties affichées sont de vraies traces d'exécution (pour les tirages aléatoires, elles changent à chaque lancement).

Comment utiliser cette page

Cherchez d'abord par vous-même : le corrigé n'a d'intérêt qu'après avoir essayé. Les numéros des questions sont ceux du sujet, et chaque exercice se termine par la classe complète, prête à copier dans votre IDE.

Exercice 1 — l'interface Set

Le sujet rappelle d'abord les méthodes de l'interface Set :

MéthodeRôle
boolean add(Object o)ajoute l'élément s'il n'est pas déjà présent
boolean addAll(Collection c)ajoute tous les éléments non déjà présents
boolean isEmpty()vrai si l'ensemble est vide
boolean contains(Object o)vrai si l'élément est contenu
boolean containsAll(Collection c)vrai si tous les éléments y sont
Iterator iterator()renvoie un itérateur sur les éléments
boolean remove(Object o)supprime l'élément s'il est présent
boolean removeAll(Collection c)supprime tous les éléments de c
boolean retainAll(Collection c)ne retient que les éléments de c
int size()nombre d'éléments

Q1 — deux ensembles de 5 entiers aléatoires

Ensembles.java
Random r = new Random();
Set<Integer> ens1 = new HashSet<>();
Set<Integer> ens2 = new HashSet<>();
for (int i = 0; i < 5; i++) {
    ens1.add(r.nextInt(100) + 1);   // nextInt(100) donne 0..99 → +1 donne 1..100
    ens2.add(r.nextInt(100) + 1);
}

Le premier piège

Deux tirages peuvent tomber sur la même valeur : le Set refuse alors le doublon, et l'ensemble se retrouve avec moins de 5 éléments. La question suivante règle le problème avec une boucle qui attend d'en avoir le compte.

Un tirage possible :

ens1 = [82, 54, 39, 91, 47]
ens2 = [97, 5, 55, 9, 91]

Q2 — 980 entiers différents entre 1 et 1000

Deux approches, toutes les deux correctes :

  1. Tirer au hasard et compter sur le refus des doublons : on boucle tant que l'ensemble n'a pas 980 éléments ;
  2. Mélanger la liste 1..1000 avec Collections.shuffle, puis garder les 980 premiers.
// approche 1 — la boucle « tirage avec refus »
Set<Integer> grand = new HashSet<>();
while (grand.size() < 980) {
    grand.add(r.nextInt(1000) + 1);
}

// approche 2 — le mélange
List<Integer> tous = new ArrayList<>();
for (int i = 1; i <= 1000; i++) {
    tous.add(i);
}
Collections.shuffle(tous);
Set<Integer> grand2 = new HashSet<>(tous.subList(0, 980));
tirage  : grand.size() = 980
mélange : grand2.size() = 980

L'approche 1 est la plus directe ; l'approche 2 garantit un coût linéaire, quelle que soit la chance aux tirages.

Q3 — afficher un ensemble, avec un itérateur puis un foreach

Le format attendu est : le nombre d'éléments, puis chaque élément.

System.out.println(ens1.size());     // 5

Iterator<Integer> it = ens1.iterator();
while (it.hasNext()) {
    System.out.print(it.next() + " ");
}
System.out.println();

for (Integer v : ens1) {             // même parcours, écriture plus courte
    System.out.print(v + " ");
}
System.out.println();

System.out.println(ens1);            // le toString fait le parcours pour nous
5
itérateur : 82 54 39 91 47
foreach   : 82 54 39 91 47
toString  : [82, 54, 39, 91, 47]

Peut-on se passer d'itérateur et de foreach ? Non : un Set n'a pas d'indice, donc pas de get(i) comme sur une liste. Le toString() (ou forEach(), ou un Stream) fonctionne, mais c'est toujours un parcours délégué — on ne fait que déplacer l'itération ailleurs.

Q4 — l'intersection

// avec un foreach
Set<Integer> inter = new HashSet<>();
for (Integer v : ens1) {
    if (ens2.contains(v)) {          // contains s'appuie sur equals
        inter.add(v);
    }
}

// sans foreach : retainAll, sur une copie d'ens1
Set<Integer> inter2 = new HashSet<>(ens1);
inter2.retainAll(ens2);
intersection (foreach)   = [91]
intersection (retainAll) = [91]

retainAll modifie l'ensemble

inter2.retainAll(ens2) garde dans inter2 les éléments présents dans ens2 — c'est une modification sur place. Sans la copie new HashSet<>(ens1), c'est ens1 lui-même qui aurait été amputé.

Q5 — l'union

Set<Integer> union = new HashSet<>(ens1);
union.addAll(ens2);                  // les doublons éventuels sont ignorés
union = [97, 82, 5, 54, 39, 55, 9, 91, 47]

Q6 — un Stream filtré

Stream<Integer> sup5 = ens1.stream().filter(v -> v > 5);
sup5.forEach(v -> System.out.print(v + " "));
éléments > 5 : 82 54 39 91 47

Comment afficher les éléments du Stream ? Avec forEach — c'est l'opération terminale qui consomme le flux. Attention : un Stream ne se parcourt qu'une fois ; le réafficher demande un nouveau ens1.stream() (ou de passer par collect pour le matérialiser).

Q7 — max, min et somme

// avec un itérateur
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
int somme = 0;
Iterator<Integer> it = ens1.iterator();
while (it.hasNext()) {
    int v = it.next();
    if (v > max) max = v;
    if (v < min) min = v;
    somme += v;
}

// avec un foreach : même logique, écriture plus courte
for (int v : ens1) {
    max = Math.max(max, v);
    min = Math.min(min, v);
    somme += v;
}

// avec un Stream : un calcul par ligne
int maxS = ens1.stream().max(Integer::compare).orElseThrow();
int minS = ens1.stream().min(Integer::compare).orElseThrow();
int sommeS = ens1.stream().mapToInt(Integer::intValue).sum();
boucle : max = 91, min = 39, somme = 313
stream : max = 91, min = 39, somme = 313
Collections.max/min : 91 / 39

Pourquoi mapToInt ?

sum() n'existe pas sur un Stream<Integer> : il faut d'abord le convertir en IntStream avec mapToInt. Sur ce flux numérique, sum(), average(), max() et min() sont disponibles directement. Et pour le max/min d'une simple collection, la classe Collections fait le travail : Collections.max(ens1).

Exercice 2 — l'interface Map

On stocke le numéro de téléphone d'amis identifiés par leur nom, dans un dictionnaire (une Map). Le sujet précise que l'on dispose d'un type Telephone, muni d'un constructeur prenant 5 Integer, d'une méthode d'affichage, d'equals et de hashCode — et effectivement, sans equals/hashCode corrects, les recherches dans la Map ne fonctionneraient pas (voir le chapitre 1). Le petit type utilisé pour tester est fourni en bas d'exercice.

Q1 — les types de la clé et de la valeur

  • la clé K est un String : le nom de l'ami ;
  • la valeur V est un Telephone : son numéro.

Q2 — instancier la Map

Map<String, Telephone> annuaire = new HashMap<>();

Comme toujours : l'interface en type de variable, l'implémentation à l'instanciation.

Q3 — insérer Pierre

annuaire.put("Pierre", new Telephone(6, 54, 67, 34, 12));

Q4 — afficher tous les numéros

for (Telephone t : annuaire.values()) {
    System.out.println(t);
}
07 77 88 99 11
06 54 67 34 12
06 11 22 33 44
06 55 44 33 22

L'ordre n'est pas celui d'insertion : un HashMap range les couples selon les hachages des clés, et rien ne garantit cet ordre.

Q5 — les numéros des amis en « D »

for (Map.Entry<String, Telephone> couple : annuaire.entrySet()) {
    if (couple.getKey().startsWith("D")) {
        System.out.println(couple.getKey() + " : " + couple.getValue());
    }
}
Dassis : 07 77 88 99 11
Dupont : 06 11 22 33 44

C'est le parcours par entrySet() — celui qui donne accès aux deux à la fois, sans double recherche. La variante keySet() + get existe aussi (une recherche par clé).

Q6 — la méthode qui retrouve un numéro

static Telephone telephoneDe(Map<String, Telephone> annuaire, String nom) {
    Telephone t = annuaire.get(nom);
    if (t == null) {
        throw new NoSuchElementException("Pas de numéro pour " + nom);
    }
    return t;
}

Le contrat de get

get renvoie null quand la clé est absente (c'est dans la JavaDoc rappelée par le sujet) — d'où le test à null avant de lever l'exception demandée, NoSuchElementException.

Q7 — les noms passés en arguments

for (String nom : args) {
    System.out.println(nom + " -> " + telephoneDe(annuaire, nom));
}

Exécution réelle de java Annuaire Pierre Martin Inconnu :

Pierre -> 06 54 67 34 12
Martin -> 06 55 44 33 22
Exception in thread "main" java.util.NoSuchElementException: Pas de numéro pour Inconnu
	at Annuaire.telephoneDe(Annuaire.java:15)
	at Annuaire.main(Annuaire.java:49)

C'est le comportement demandé : dès qu'un nom est inconnu, l'exception interrompt le programme. (En production, on choisirait plutôt d'afficher le message et de continuer — mais le sujet demande bien de lever.)

Q8 — plusieurs numéros par ami

Il faut changer le type de la valeur : V passe de Telephone à List<Telephone> (un Set<Telephone> marcherait aussi). La clé ne bouge pas. On obtient :

Map<String, List<Telephone>> annuaire = new HashMap<>();

Q9 — la réécriture

L'insertion n'est plus un simple put : il faut retrouver la liste de l'ami et la créer au premier numéro.

static Map<String, List<Telephone>> annuaire = new HashMap<>();

static void ajouter(String nom, Telephone numero) {
    List<Telephone> numeros = annuaire.get(nom);
    if (numeros == null) {
        numeros = new ArrayList<>();
        annuaire.put(nom, numeros);
    }
    numeros.add(numero);
}

L'écriture condensée équivalente, avec computeIfAbsent : annuaire.computeIfAbsent(nom, k -> new ArrayList<>()).add(numero);. Pour la question 6, « un des téléphones possibles » :

static Telephone telephoneDe(String nom) {
    List<Telephone> numeros = annuaire.get(nom);
    if (numeros == null || numeros.isEmpty()) {
        throw new NoSuchElementException("Pas de numéro pour " + nom);
    }
    return numeros.get(0);   // on choisit le premier de la liste
}
— Q9·4 : tous les numéros —
07 77 88 99 11
06 54 67 34 12
06 12 34 56 78
06 11 22 33 44
— Q9·5 : les amis en 'D' —
Dassis : [07 77 88 99 11]
Dupont : [06 11 22 33 44]
— Q9·6 —
Pierre -> 06 54 67 34 12
Exception in thread "main" java.util.NoSuchElementException: Pas de numéro pour Inconnu
	at AnnuaireListes.telephoneDe(AnnuaireListes.java:29)
	at AnnuaireListes.main(AnnuaireListes.java:60)

Le piège du put

Pour ajouter un second numéro, un put(nom, numero) écraserait la liste existante — put remplace la valeur associée à la clé et renvoie l'ancienne. L'ajout doit se faire dans la liste, pas dans la Map.


td1-collections.pdf

R3.02 · TD 1 — Set et Map · 2 pages

Consultation seule — téléchargement désactivé

Chargement du document…

Dernière mise à jour le

Sur cette page