Master 2 BBSG POO, langage Java Laurent Tichit 5. Collections et autres structures de données 1. 2. 3. 4. 5. Listes triées Table associative : un annuaire Affichage de l’environnement d’un programme Arborescences Graphes AVERTISSEMENT. Les classes et interfaces liées aux collections, subissent à partir de la version 5 de Java une importante modification, puisqu’elles deviennent « génériques ». Cela se manifeste, notamment dans la documentation en ligne, par une notation spéciale : le signe <E> (comme « Element ») à côté du nom, qui représente un type (une interface, ou plus rarement une classe) : public interface Collection<E> { ... } A cause de la généricité, si vous utilisez des collections d' Object et non pas des collections génériques avec Java5 (et supérieur), vous risquez d’obtenir des avertissements concernant des opérations « incontrôlées » ou « dangereuses » : Note: MachinTruc.java uses unchecked or unsafe operations. Pour éviter ces messages, soit vous utilisez les collections génériques, soit (vous vous contentez de rester dans la préhistoire) il vous suffit de compiler votre fichier source en indiquant qu’il relève de Java 1.4 : javac -source 1.4 <fichiers_à_compiler> (la version 1.4 précède immédiatement la version 5). Utilisateurs d’Eclipse, pour obtenir le même résultat vous devez aller dans Project > Properties > Java Compiler, cocher la case Enable project specific settings et jouer sur l’indication du champ Compiler compliance level. POURQUOI ? Pour des raisons de sécurité, la machine virtuelle Java 1.4 (et les précédentes) teste les types des éléments insérés dans les collections (et extraits de celles-ci). Ces tests ont lieu à l'exécution (on parle alors de tests dynamiques) si on utilise Java 1.4 (ou inférieur) car à la compilation, il est impossible de connaître le type des données – elles sont vues simplement comme des Object. Si on utilise les version génériques (Java 5+), ces tests peuvent (enfin!) avoir lieu à la compilation (tests statiques) car on déclare le type des éléments => meilleures performances ! Si on utilise les collections non-génériques avec Java5+, il faut donc le déclarer ! 5.1. Listes triées Écrivez un programme qui construit une collection triée contenant n nombres entiers (représentés par des objets Integer) tirés au hasard dont la valeur est comprise entre 0 et 1000. A votre choix, la valeur de n est lue au début de l’exécution ou est un argument du programme. Ensuite, ce dernier affiche la collection construite afin qu’on puisse constater qu’elle est bien triée. A. Dans la première version du programme la collection est une sorte de List<Integer> (par exemple un ArrayList ou une LinkedList) que vous triez, après la construction, en utilisant une méthode statique ad hoc de la classe Collections. B. Dans une deuxième version, la collection est une sorte de Set<Integer> (c’est-à-dire un HashSet ou un TreeSet, mais avez-vous le choix ?) si bien qu’elle est constamment triée. 5.2. Table associative : un annuaire On vous demande d’écrire une classe Annuaire pour mémoriser des numéros de téléphone et d’adresses. Chaque entrée est représentée par une fiche à plusieurs champs : un nom, un numéro et une adresse. La structure des fiches est décrite par une classe Fiche que vous devez écrire. Écrivez également une classe Annuaire comportant une table (Map<String,Fiche>) qui sera faite d’associations ( un_nom , une_fiche ). associative A. Dans un premier temps, la table associative en question sera une instance de la classe HashMap. Écrivez un programme répétant les opérations suivantes • lecture d’une « commande » d’une des formes : +nom, ?nom, ! ou bye, • si la commande a la forme ?nom, recherche et affiche la fiche concernant le nom indiqué, • si la commande est de la forme +nom, saisie des autres informations d’une fiche associée à ce nom et insertion de la fiche correspondante dans l’annuaire, • si la commande est !, affichage de toutes les fiches de l’annuaire, • si la commande est . (un point), arrêt du programme. B. On constate que la commande ! produit l’affichage des fiches dans un ordre imprévisible. Que faut-il changer dans le programme précédent pour que les fiches apparaissent dans l’ordre des noms ? C. Faites en sorte qu’à la fin (resp. au début) du programme l’annuaire soit enregistré (resp. lu) dans un fichier nommé annuaire.obj. Utilisez des flux ObjectInputStream et ObjectOutputStream (que l’on doit créer à partir de flux FileInputStream et FileOutputStream préexistants). N’oubliez pas d’autoriser (par un énoncé « implements Serializable ») la « sérialisation » des objets qui doivent être écrits ou lus dans le fichier (voir cours). 5.3. Affichage de l’environnement d’un programme Les applications Java accèdent à un ensemble de « propriétés système » qui définissent des aspects de leur environnement d’exécution, comme la version de la machine Java, le système d’exploitation sous-jacent, le répertoire de travail, etc. Ces propriétés sont codées sous forme de couples de chaînes (clé, valeur), et on les obtient par un appel System.getProperties() qui renvoie un objet Properties qui est une variété de table associative (Map). En fait, de Map<Object, Object> depuis Java5. Donc il ne sert à rien de se servir des Generics ici. Pour connaître la valeur d’une System.getProperty(nom). propriété à partir de son nom on utilise De manière analogue, une application Java accède à l’ensemble de variables d’environnement (path, user, etc.) définies au niveau du système d’exploitation sous-jacent ; c’est encore une table associative (Map) qu’on obtient par un appel de System.getenv(). Cette fois-ci, c'est une Map<String, String>. Pour obtenir la valeur d’une variable d’environnement a partir de son nom on écrit System.getenv(nom). Écrivez une méthode (qui servira pour afficher les deux types de Map) static void afficherMap(Map tableAssoc); qui affiche les éléments d’une table associative sous la forme user.dir --> C:\_\JAtelier\Atelier java.vm.version --> 1.6.0-b18 os.name --> Windows XP user.home --> C:\Documents and Settings\Laulo etc. Servez-vous en pour obtenir la liste des propriétés système avec leur valeurs, puis des variables d’environnement avec leurs valeurs. N.B. Properties est une sous-classe de Hashtable, elle-même sous-classe de Dictionary. N’investissez pas dans Dictionary : elle est obsolète. 5.4. Arborescences Une arborescence (avec moins de rigueur on dit parfois arbre) est une structure de données formée d’un ensemble E et d’une relation qui à chaque élément de E – sauf un, appelé la racine de l’arborescence – associe un autre élément appelé son père. Les éléments d’une arborescence sont appelés nœuds ; à chaque nœud n est donc associée une liste, éventuellement vide, de nœuds dont n est le père ; on les appelle les fils de n. Lorsqu’un nœud n’a pas de fils, on dit que c’est une feuille. Pour fixer les idées nous supposerons ici que les informations portées par les nœuds sont des chaînes de caractères. A. Écrivez une classe Noeud pour représenter les [nœuds des] arborescences. Elle aura deux variables privées • info, de type String, pour représenter l’information portée par le nœud, • fils, de type ArrayList<Noeud>, pour représenter la liste des fils du nœud. et les méthodes publiques suivantes : • Noeud(String info) – construction d’un nœud portant l’information indiquée, • String info() – renvoie l’information portée par le nœud, • boolean estFeuille() – vrai si et seulement si le nœud n’a pas de fils, • void ajouterFils(Noeud fils) – ajoute le nœud indiqué comme fils du nœud en question, • Iterator fils() – renvoie un itérateur permettant de parcourir la liste des fils du nœud ; le comportement est indéfini si le nœud est une feuille, • void afficher() – affiche le nœud et tous ses descendants (ses fils, les fils de ses fils, etc.) à raison de un nœud par ligne ; la hiérarchie (c.-à-d. « qui est fils de qui ? ») est exprimée par une marge à gauche de, par exemple, trois espaces par niveau. B. Pour essayer la classe Noeud, écrivez une classe de test qui construit et affiche l’arborescence des fichiers et répertoires ayant pour racine le répertoire de travail. Indications. Comme cela a été vu à l’exercice précédent, le nom du répertoire de travail peut être obtenu par l’expression System.getProperty("user.dir"); Pour vous promener dans les fichiers et répertoires, les méthodes suivantes de la classe java.io.File vous seront utiles : getName(), isDirectory(), listFiles(). C. Pour améliorer votre conception, transformez Noeud en classe abstraite, et créez deux classes filles NoeudInterne et Feuille. Supprimez donc la méthode estFeuille() et ses appels, la méthode ajouterFils() est seulement présente dans un NoeudInterne. Modifiez éventuellement votre classe de test. 5.5. Représentation des graphes Un graphe non orienté G = (S, A) est déterminé par la donnée d’un ensemble S, dont les éléments sont appelés les sommets, et un ensemble A de paires de sommets dont les éléments sont appelés arêtes. Si a = {s1, s2} est une arête on dit que s1 et s2 sont les extrémités de a et donc que s1 et s2 sont deux sommets adjacents (on dit aussi voisins). Souvent on peut représenter S comme un ensemble de points du plan et alors on représente A comme un ensemble de segments ayant ces sommets pour extrémités (il découle de la définition que nous avons donnée qu’il ne peut pas y avoir deux arêtes distinctes ayant les mêmes extrémités). Dans certaines applications on associe un poids à chaque arête : par exemple, si le graphe représente un réseau routier, alors chaque arête correspond à un tronçon de route entre deux carrefours et est naturellement affectée d’un poids : la longueur du tronçon : Dans le programme réalisé ici nous allons coder le graphe en associant à chaque sommet s la liste des couples (s’, p) où s’ est un sommet adjacent à s et p le poids de l’arête correspondante. Par exemple, dans la figure ci-dessus, pour le sommet sa (le sommet étiqueté a) cette liste est [(sf, 5), (sb, 3)], pour le sommet sb la liste est [(sa, 3), (sf, 6), (se, 4 ), (sc, 1)] et ainsi de suite. Écrivez la classe Voisin dont les instances représentent les couples (sommet, poids). Faites simple, ce n’est qu’une classe auxiliaire. Écrivez la classe Sommet dont les instances représentent les sommet du graphe. Elle comporte deux variables d’instance privées, étiquette (de type String) et voisins (de type ArrayList) et des méthodes publiques • Sommet(String etiquette) – constructeur d’un sommet portant l’étiquette indiquée, • boolean estVoisin(Sommet sommet) – a.estVoisin(b) est vrai si et seulement si a et b sont voisins, • void ajouterVoisin(Sommet sommet, int poids) – ajout d’un élément à la liste des voisins du sommet en question, • Iterator<Voisin> voisins() – obtention d’un térateur pour parcourir la liste des voisins du sommet en question, • String toString() – obtention d’une forme textuelle du sommet (en fait, obtention de l’étiquette du sommet). Écrivez la classe Graphe, composée d’une table associative (Map) dont les valeurs sont les sommets du graphe et les clés les étiquettes correspondantes. Il y aura les méthodes publiques : • Sommet chercherSommet(String etiquette) – obtention du sommet ayant l’étiquette indiquée, ou null si un tel sommet n’existe pas, • Sommet obtenirSommet(String etiquette) – obtention d’un sommet ayant l’étiquette indiquée, nouvellement créé si nécessaire, • boolean ajouterArete(Sommet sommet1, Sommet sommet2, int poids) – ajouter l’arête ayant pour extremités les sommets indiqués • boolean ajouterArete(String etiquette1, String etiquette2, int poids) – même chose que la précédente, mais à partir des étiquettes des sommets, • void chemins(String depart, String arrivee) – affichage de tous les chemins joignant les sommets ayant les étiquettes indiquées. Pour essayer tout cela, écrivez un programme principal qui crée un graphe analogue à celui de la figure ci-dessus et, par exemple, affiche tous les chemins joignant le sommet a au sommet j. Indication. Pour la fonction chemins vous pouvez écrire une fonction récursive, auxiliaire private void chemins(Sommet depart, Sommet arrivee, Stack<Sommet> pile); qui, si le problème n’est pas résolu (i.e. si départ n’est pas égal à arrivée), parcourt les sommets voisins de départ et se rappelle elle même avec chacun de ces sommets pour nouveau départ. La pile donnée représente le bout de chemin déjà construit ; elle sert à l’affichage de la solution et aussi à vérifier que le chemin construit n’a pas de cycles. Enfin, en lisant la doc de Stack, vous vous rendez-compte qu'elle est obsolète. Remplacez là par une Double-Ended Queue Deque.