TP 6   2425
 
A) LIRE EN DÉTAIL (et poser des questions !)

Les collections

  1. Idée générale
    Comment stocker ensemble et manipuler tout un tas d'objets lorsque les caractéristiques figées d'un tableau ne conviennent pas ? Réponse : il faut utiliser la collection la mieux adaptée.
    Vous connaissez déjà grâce au projet la HashMap (tableau associatif), le Set (ensemble), et la Stack (pile, ex.7.26). Mais il y a aussi la List (liste).
    L'intérêt des collections est qu'elles fonctionnent toutes à peu près de la même manière, ce qui facilite leur apprentissage et permet une certaine interchangeabilité. D'autre part, elles ont le bon goût de s'agrandir au fur et à mesure des besoins (contrairement aux tableaux !).
    Ce que l'on appelle le Collection Framework ("cadre de travail" sur les collections) proposé par le JDK comporte :
    - des interfaces (pour définir les comportements communs à plusieurs implémentations),
    - des classes abstraites (pour fournir des implémentations partielles à compléter),
    - des classes (implémentations complètes prêtes à l'emploi),
    - et des algorithmes (méthodes performantes déjà écrites et testées pour résoudre un certain nombre de problèmes classiques).
    Tout cela figure dans le paquetage java.util et il faudra importer chaque classe et chaque interface que vous utiliserez :
    import java.util.UneClasseUtile; au tout début du fichier.    

  2. Classes enveloppes et généricité
    2.1 Une collection en Java ne peut contenir que des références vers des objets, donc pas des valeurs de type primitif.
    Bien que cela ne soit pas utile dans ce TP, comment faire pour manipuler des collections de nombres, par exemple ?
    Pour chaque type primitif, le JDK nous fournit une classe enveloppe qui permet de créer un objet contenant pour seul attribut une valeur du type primitif correspondant (et qui fournit aussi quelques méthodes intéressantes).
    Par exemple, un Double contient un double et un Integer contient un int.
    S'il fallait sans arrêt convertir le type primitif en objet et vice-versa, ce serait fastidieux, mais heureusement, Java (à partir de la version 5) sait faire le l'auto-(un)boxing, c'est-à-dire mettre ou enlever automatiquement la "boîte" (box !) autour de la valeur de type primitif.
    Sans cette automatisation (donc avec le JDK 1.4.2 encore utilisé dans les entreprises), il faudrait écrire Integer vOI = new Integer(12); au lieu de simplement Integer vOI=12;ou bien int vPI = vOI.intValue(); au lieu de simplement int vPI=vOI;
    2.2 Tous les éléments d'une collection doivent être du même type et il faut le préciser, entre < et >, à la déclaration ET à la création :
    Stack<Double> vSI = new Stack<Double>();
    si on veut créer une pile de nombres réels,
    ou bien HashMap<String, Room> vHSR = new HashMap<String, Room>();
    (dans ce dernier cas, il faut préciser le type des clés ET le type des valeurs).
    Si vous oubliez de le préciser entre < et > à l'un et/ou l'autre de ces endroits, vous obtiendrez un avertissement (warning) de compilation vous informant que vous utilisez des opérations non sûres (unsafe). Il ne faut pas ignorer ces avertissements, car ils vous informent que le compilateur n'a pas pu vérifier les types que vous manipulez et que des erreurs à l'exécution peuvent survenir.
    Cette façon de "paramétrer" une classe ou une interface avec le type de ses éléments est appelée généricité, car la classe ou l'interface devient alors générique (inutile de développer une classe différente pour chaque type d'élément !).          

  3. Collection
    L'interface de plus haut niveau que respecte toute collection est Collection<E> (elle contient donc uniquement des éléments de type E, E pouvant être n'importe quelle classe). Cette interface nous garantit notamment la disponibilité des méthodes :
    • int size() qui nous retourne le nombre d'éléments dans la collection
    • boolean contains(final Object pElt) qui permet de savoir si la collection contient ou pas l'élément pElt
    • boolean add(final E pElt) qui ajoute à la collection l'élément pElt (soit à la fin si la collection est ordonnée, soit n'importe où si elle ne l'est pas). Elle retourne VRAI si la collection a bien été modifiée par cet appel, c'est-à-dire dans la plupart des cas. Elle retournerait FAUX sinon, par exemple si la collection contenait déjà l'objet (dans le cas d'un ensemble), ou bien s'il n'y avait plus de place (dans le cas d'une collection de taille bornée).
    • Iterator<E> iterator() qui nous fournit un itérateur pour parcourir cette collection (voir point 6.)
    • boolean remove(final Object pElt)est parfois disponible, mais ne doit pas être utilisée pendant un parcours, car elle peut perturber ce parcours de la collection ; pour être tranquille, il suffit d'utiliser la méthode remove de l'itérateur (voir point 6.)      

  4. List et ArrayList
    Pour pouvoir essayer concrètement les collections, nous allons prendre l'exemple d'une liste d'objets (stockés les uns derrière les autres).
    4.1  List est l'interface qui spécifie ce que devront savoir faire toutes les sortes de listes, notamment gérer le numéro de chaque objet (comme dans un tableau) ou retourner une sous-liste de tel numéro à tel numéro.
    Il est conseillé de toujours déclarer une variable liste comme suit : List<E> vListe; (plutôt que ArrayList<E> vListe;) pour que le programme fonctionne ensuite quelle que soit l'implémentation choisie, par exemple : = new ArrayList<E>();.
    4.2 ArrayList est l'implémentation (complète) la plus courante de l'interface List (mais il y en a d'autres dans le JDK !) et c'est celle que nous utiliserons dans ce TP.
    Comme son nom l'indique, elle se comporte comme un tableau (mais de taille extensible) en fournissant notamment les méthodes
    E set(final int index, final E pElt) et E get(final int pIndex) ; mais ne pas oublier que toutes les méthodes de Collection sont évidemment utilisables. Si on comprend facilement ce que peut retourner la méthode get, que peut bien retourner la méthode set ?    

  5. Parcours d'une collection
    5.1  Quand le parcours est "simple", c'est-à-dire du début à la fin sans modification de la collection et indépendamment d'une autre collection, il faut utiliser la boucle POUR CHAQUE (apparue avec Java 5 et appelée for each en anglais bien que le mot each ne fasse pas partie de sa syntaxe !). Par exemple :
    for (E vElement : vMaCollection) { ... }permet de faire prendre à la variable vElement (déclarée de type E dans les parenthèses) successivement les valeurs de tous les éléments de vMaCollection (si elle respecte bien l'interface Collection<E>). Elle se lit en français : POUR CHAQUE élément vElement de la collection vMaCollection FAIRE ... (Remarque : cette boucle fonctionne aussi pour les tableaux)
    5.2 Quand le parcours n'est pas "simple", c'est-à-dire de la fin vers le début ou en supprimant des éléments ou en parcourant parallèlement une autre collection, il faut utiliser un itérateur (voir les détails au point 6). Par exemple :
    Iterator<Double> vIt = vMaCollection.iterator();
    while ( vIt.hasNext() ) {
      if ( vIt.next() < 0.0 )  vIt.remove();
    } // supprime tous les nombres négatifs de vMaCollection    

  6. Iterator
    Un objet Iterator<E> permet de parcourir une Collection<E> et fournit les méthodes suivantes :
    • boolean hasNext() qui retourne VRAI s'il y a encore un élément à parcourir (FAUX si on est à la fin de la collection)
    • E next() qui retourne à chaque appel l'élément suivant et fait donc avancer d'un cran le parcours de la collection (ne pas le faire à la fin de la collection, sinon NoSuchElementException !)
    • void remove() (sans aucun paramètre !) qui supprime l'élément qui vient d'être retourné par next(), d'où l'obligation de toujours faire précéder un remove() d'un next(), sinon IllegalStateException !            

  7. HashMap
    Un objet HashMap<K,V> n'est pas à proprement parler une Collection puisqu'elle utilise 2 collections, mais elle permet de stocker et gérer plusieurs objets comme une Collection, en fournissant les méthodes suivantes :
    • V get(K) qui retourne la valeur associée à la clé passée en paramètre (ou null si cette clé n'est pas trouvée)
    • V put(K,V) qui ajoute l'association (clé,valeur) dans la HashMap (que peut-elle bien retourner ? *)
    • Set<K> keySet() qui retourne l'ensemble des clés de la HashMap (pourquoi un ensemble ici ?)
    • List<V> values() qui retourne la liste des valeurs de la HashMap (pourquoi une liste ici ?)
    • V remove(K) qui supprime l'élément associé à la clé passée en paramètre (que peut-elle bien retourner ? *)
  8. * Les deux fonctions put et remove sont souvent utilisées comme des procédures (quand on n'a pas besoin de la valeur de retour), mais il faut quand-même savoir qu'elles retournent la valeur précédemment associée à la clé. Et elles retournent null si la clé n'a pas été trouvée, ou bien si celle-ci était associée à la valeur null.

B) L'objectif des exercices suivants est d'écrire une classe assurant la gestion d'une collection d'Items, puis de comprendre pourquoi la HashMap est préférable à l' ArrayList dans ce cas.

Chaque mot compte dans les énoncés ci-dessous, y compris les aides, conseils, questions ... Il est indispensable de lire chaque exercice (par exemple II.2) dans son intégralité avant de commencer à taper du code.


I. La classe Item
Cette classe vous est fournie dans le fichier tp61e.jar ci-joint, mais elle sera à compléter seulement à l'exercice II.7.
Si c'est le paquetage [veref] qui s'ouvre, double-cliquez sur le rectangle <go up>, puis fermez la précédente fenêtre veref.
Cliquez sur Tout tester dans la classe ItemTest ; tout est vert ?


II. La classe Inventaire

II.1 Complétez cette classe avec 2 attributs : une liste d'Items aListe et un entier aPrixTotal. (relire cours 1, 2.2, 4.1)
Compilez (ni erreur ni warning ?) puis essayez test_new().

II.2 Écrivez le constructeur par défaut (donc, combien de paramètres ?) : la liste devra être créée (choisissez la seule implémentation indiquée dans la partie A), mais sera évidemment vide lorsqu'on crée l'inventaire ; le prix total de cet inventaire doit pouvoir se déduire facilement. (relire cours 2.2, 4.2)
Aide : combien d'affectations est censé contenir tout constructeur de cette classe ?
Rappel : Ni les classes abstraites ni les interfaces ne peuvent être instanciées.
Compilez (ni erreur ni warning ?) puis essayez test_constructeur_3() ; si une erreur de compilation survient dans InventaireTest, c'est probablement par ce que le constructeur n'est pas correct.

II.3 Écrivez une fonction getItem qui parcourt la liste et qui retourne l'Item qui porte le nom passé en paramètre, ou bien null si elle ne le trouve pas. (relire cours 5.1)
Compilez, puis essayez test_getItem_4(), puis testez avec n'importe quel nom (le cas où on trouve l'Item ne pourra être testé qu'à partir du III.5).

II.4 Écrivez une fonction booléenne contientItem qui retourne vrai si la liste contient l'Item qui porte le nom passé en paramètre (et faux sinon !).
Contrainte : évitez la duplication de code ! (n'a-t-on pas déjà écrit une méthode qui nous dit si un item n'a pas été trouvé dans la liste ?)
Compilez, puis essayez test_contientItem_5(), puis testez avec n'importe quel nom (le cas true ne pourra être testé qu'à partir du III.5).
Au fait, aucun if n'est nécessaire dans contientItem. Si vous ne vous rappelez plus comment faire, demandez !

II.5 Écrivez une procédure ajouteItem qui, comme son nom l'indique, ajoute un Item à la liste. Elle prendra 2 paramètres (nom et prix) pour pouvoir d'abord créer l' Item. (relire cours 3)
Aide : combien d'attributs de l'inventaire devront être modifiés ?
Contrainte : l'Item ne doit pas être ajouté s'il en existe déjà un de même nom. (sans duplication de code !)
Compilez, puis essayez test_ajouteItem_6(), puis testez à la main ajouteItem, contientItem, et getItem.

II.6 En supposant qu'on ait écrit ce qu'il faut pour avoir une représentation sous forme de String d'un Item (ce qui sera fait au II.7), redéfinissez la fonction toString pour qu'elle retourne :
- la représentation sous forme de String de la liste (Aucun parcours de la liste n'est nécessaire ! Une méthode bien connue de la classe Object et présente dans toutes les classes du JDK ne serait-elle pas utile ici ?)
- suivie de " : " puis du prix total.
Compilez, puis essayez test_toString_8(), puis testez à la main toString et ajouteItem.
Le prix total est-il correct ? Sinon corrigez, puis voir le point suivant.

II.7 L'affichage des éléments de la liste constaté au point précédent vous convient-il ?
Mais comment Java pourrait-il savoir comment afficher un Item ?
On pourrait très bien vouloir l'afficher comme ça : "***Lampe : 10€***", mais finalement non :
Redéfinissez la fonction toString dans la classe Item pour qu'elle retourne le nom suivi de "(" puis du prix puis de ")".
Compilez et testez toString de la classe Item. Retestez toString et ajouteItem de la classe Inventaire (sans y faire de modification).
Vous pouvez décommenter les 6 lignes dans la méthode test_toString_5() de la classe ItemTest et lancer cette méthode.


III. La classe Utilisation

III.1 Créez cette classe qui ne servira qu'à contenir une procédure essai pour mettre en oeuvre les 2 classes précédentes.
Donc pas d'attribut, donc pas de constructeur, donc on ne l'instanciera pas. Donc ?
Cette procédure devra :

III.2 Déclarer/créer un objet Inventaire et l'afficher.
Compilez et testez.

III.3 Ajouter un item X et réafficher l'inventaire, puis ajouter un item du même nom avec un prix différent et réafficher l'inventaire.
Compilez et testez.

III.4 Ajouter un 2ème item Y et réafficher l'inventaire.
Compilez et testez.


IV. Complétez la classe Inventaire pour pouvoir enlever des items

IV.1 Écrivez une procédure enleveItem qui supprime de la liste l'Item dont le nom est passé en paramètre. (relire cours 5.2)
Compilez (ni erreur ni warning ?), puis essayez test_enleveItem_7().

IV.2 Enrichissez la fin d'Utilisation.essai ; cette procédure devra aussi :
- enlever un item non existant dans la liste et réafficher l'inventaire,
- enlever l'item X et réafficher l'inventaire,
- enlever l'item Y et réafficher l'inventaire,
- enlever à nouveau l'item Y et réafficher l'inventaire.


V. La classe InventaireH

V.1 Dupliquez le contenu de la classe Inventaire dans la classe InventaireH.

V.2 Remplacez List par Map et ArrayList par HashMap. Les clés seront des String et les valeurs des Item.

V.3 Récrivez la fonction getItem sans boucle !

V.4 Modifiez juste le nécessaire dans la procédure ajouteItem.

V.5 Récrivez la procédure enleveItem sans itérateur ni boucle !

V.6 Modifiez juste le nécessaire dans la fonction toString pour qu'elle affiche les valeurs de la HashMap (toujours aucun parcours nécessaire !).
Compilez (ni erreur ni warning ?). Vous pouvez tout décommenter dans InventaireHTest et Tout tester.

V.7
Dupliquez les instructions dans Utilisation.essai pour tester la classe InventaireH juste avant la classe Inventaire (peut-être en ajoutant "H: " avant chaque affichage).
Tout compile et fonctionne comme avant car nous n'avons changé que la représentation interne de l'inventaire.


VI. Pour ceux qui ont terminé (ou sinon, en travail personnel) , complétez la classe Inventaire pour pouvoir trier les items

VI.1 Écrivez dans la classe Inventaire une procédure trieC qui trie la liste dans l'ordre Croissant (c'est l'ordre de tri par défaut : "natural ordering") en essayant d'appeler la procédure sort spécifiée dans l'interface List.
Aide : il faut lire la javadoc (notamment "parameters:") de cette procédure pour savoir quel paramètre lui passer. Lire à ce propos l'échange avec un étudiant, tout en bas de cet énoncé.
Compilez (ni erreur ni warning ?).

VI.2 Testez maintenant la procédure précédente : un problème ?  Le compilateur vous dit que les items ne sont pas comparables, donc comment pourrait-on les trier ?
Ajoutez ce qu'il faut dans la classe Item (comme vu au TP et au TD précédents).
Contrainte : un Item sera considéré inférieur à un autre si son prix est inférieur ; en cas d'égalité, on retiendra l'ordre alphabétique des String.
Aide : La classe Integer est la classe enveloppe du type primitif int, comme la classe Double est l'enveloppe de double. Elle possède donc un constructeur naturel et une fonction compareTo.
Compilez et retestez trieC.

VI.3 Enrichissez la fin d'Utilisation.essai ; cette procédure devra aussi :
- ajouter 4 items  "ananas",7  puis  "abricots",7  puis  "cerises",9  puis  "bananes",8  et réafficher l'inventaire (l'ordre devra être ananas,abricots,cerises,bananes),
- trier la liste puis réafficher l'inventaire (l'ordre devra être abricots,ananas,bananes,cerises).
Compilez et testez.

VI.4 Ajoutez maintenant une procédure trieD qui trie la liste dans l'ordre Décroissant en appelant toujours la procédure sort, mais cette fois en lui passant en paramètre un comparateur qui indique l'ordre décroissant : Collections.reverseOrder(). La classe Collections contient des méthodes statiques utiles pour manipuler les collections (pour plus de détails, voir sa javadoc).
Compilez (ni erreur ni warning ?).

VI.5 Enrichissez la fin d'Utilisation.essai ; cette procédure devra aussi trier la liste par ordre décroissant et réafficher l'inventaire (l'ordre devra être cerises,bananes,ananas,abricots).
Compilez et testez.


VII. Pour ceux qui ont terminé 
(ou sinon, en travail personnel) :
- redéfinissez la méthode equals dans Item
- redéfinissez la méthode equals dans Inventaire
- testez les 2 méthodes dans Utilisation.essai
- complétez Utilisation.essai pour tester ces nouvelles possibilités.

 

Avatar Denis BUREAU
Un étudiant a écrit :

Je sui en train de finir le tp6 mais je ne comprends pas comment faire la méthode trieC(), je ne trouve pas les informations sur sort().

par Denis BUREAU, mardi 25 février 2020, 12:23
 

L'énoncé dit "Aide : il faut lire la javadoc (notamment "Parameters:") de cette procédure pour savoir quel paramètre lui passer."

Dans google, taper java 11 list sort

Le premier lien doit vous mener à la javadoc sur le site d'Oracle (version JDK 11 qui est celle incorporée dans BlueJ) de la méthode sort de l'interface List.

Au paragraphe Parameters:, vous pourrez lire ce qu'il faut passer comme paramètre à cette procédure.