:: Enseignements :: ESIPE :: E3INFO :: 2017-2018 :: Programmation Objet avec Java ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) | Iterateur, généricité, un peu de design et des entrées-sorties |
Exercice 1 - Pile de strings... ou d'autre choses
On souhaite écrire une structure de données LIFO (last in, first out)
de taille bornée permettant de stocker des messages sous forme de chaînes de caractères.
Les deux opérations principales de cette structure sont :
-
push permettant d'ajouter un élément sur le dessus de la pile,
-
pop permettant de supprimer et récupérer le dernier élément ajouté.
L'idée est d'utiliser un tableau sous-jacent, de taille fixe, stockant les messages,
ainsi qu'un pointeur de tête localisant le dernier élément ajouté.
- Ecrire une classe Stack dans le package fr.upem.collections
avec un constructeur prenant en paramètre le nombre maximum d'élements que peut stocker la structure de
données. Penser à vérifier les préconditions.
- Ecrire une méthode push ajoutant un message dans la stack.
Pensez à vérifier les préconditions sachant que l'on veut interdire de stocker null.
- Ecrire une méthode size renvoyant le nombre actuel de messages stockés.
- Ecrire une méthode pop supprimant et renvoyant le dernier message ajouté.
De même, pensez à vérifier les préconditions.
- Vérifiez que vos méthodes fonctionnent dans une méthode main avec le code suivant:
public static void main (String[] args) {
Stack s = new Stack(2);
s.push("message 1");
s.push("Message 2");
System.out.println(s.size()); // 2
System.out.println(s.pop()); // Message 2
System.out.println(s.size()); // 1
}
- On souhaite à présent parcourir notre stack à l'aide d'une boucle for-each.
Que manque-t-il au code suivant pour compiler?
public static void main (String[] args) {
Stack s = new Stack(2);
s.push("message 1");
s.push("Message 2");
System.out.println(s.size()); // 2
System.out.println(s.pop()); // Message 2
System.out.println(s.size()); // 1
s.push("message 3");
for(String msg : s) {
System.out.println(msg);
}
// Affiche:
// Message 3
// Message 1
}
Rappeler la différence entre Iterable<String> et Iterator<String>.
Note: On suppose ici que l'itération d'une Stack se fait dans l'ordre des "pop" (LIFO), même si
l'itération ne doit pas modifier l'état de la stack itérée.
- Comment faire en sorte que notre classe Stack ne soit plus exclusivement
dédiée au stockage des String, mais puisse fonctionner avec n'importe quel type:
public static void main (String[] args) {
Stack<Integer> si = new Stack<>(2);
si.push(1);
si.push(2);
for(Integer i : si) {
System.out.println(i);
}
Stack<String> ss = new Stack<>(2);
ss.push("msg1");
ss.push("msg2");
for(String msg : ss) {
System.out.println(msg);
}
}
- Considérez le bout de code suivant (à coller à la fin de votre main):
List<String> list = new LinkedList<>();
list.add("zero");
list.add("one");
list.add("two");
list.add("three");
list.add("four");
list.add("five");
list.add("six");
list.add("seven");
list.add("eight");
Stack<String> stack = new Stack<>(list.size()+1);
// What is the argument of this method call?
list.forEach(s->stack.push(s));
// WHY THIS FIRST LOOP SUCKS ?
for(int i=0; i<list.size(); i++) {
System.out.println(list.get(i));
}
// Why these two loops behave differently?
for(String msg : stack) {
if(msg.equals("two")) {
stack.push("two");
}
}
for(String msg : list) {
if(msg.equals("two")) {
list.add("two");
}
}
Quelle est la nature de l'argument de l'appel à la méthode forEach sur la liste?
Remplacez cet argument par une method reference.
Pourquoi la première boucle est une horreur? Quelle est sa complexité en taille de la liste?
Écrivez-en une version linéaire.
Quelle est la différence de comportement entre les deux dernière boucles?
Qu'est ce qu'un itérateur fail-fast?
Ajoutez ces lignes à votre main:
System.out.println(list);
list.sort(null);
System.out.println(list);
Que fait la méthode sort? Sur quel critère est triée la liste
Comment trier la liste par taille de mot croissante en une seule ligne?
Exercice 2 - Panier éléctronique
Voici le code du package
fr.umlv.shopping permettant de modéliser des livres numériques (avec un titre, un auteur et un prix) ainsi qu'un panier électronique dans lequel on peut ajouter ou retirer des livres. Il est possible d'afficher le panier et de calculer son prix. Les tests sont dans le package
fr.umlv.shopping.test.
La boutique qui vend les livres se diversifie et vend désormais d'autres types de contenu numérique: des jeux-vidéo et des cartes cadeau dont on vous fournit le code.
Les jeux vidéo ont un titre, un type de console et un prix. Les cartes cadeau ont une valeur (entière) et une durée de validité en semaines. Leur prix est calculé en fonction de ces 2 paramètres.
Le but de l'exercice est de transformer le code fourni pour que l'on puisse également ajouter ces nouveaux types de contenu au panier. Et en réalité, la boutique prévoit déjà de vendre de la musique et des films à l'avenir (dans un format qui reste à définir), on aimerait donc que l'ajout de nouveaux média se fasse aisément par la suite.
-
Modifier le code fourni pour que l'on puisse ajouter/retirer aussi bien des livres que des jeux vidéos et des cartes cadeau au panier.
-
Vérifier que la suppression fonctionne bien dans tous les cas.
-
Faire en sorte de minimiser la duplication de code.
Exercice 3 - Entrées-sorties
On veut maintenant pouvoir sauvegarder les articles d'un panier d'achats dans un fichier sous forme textuelle en
respectant un format particulier, afin de pouvoir les recharger en mémoire ultérieurement.
On retient le format de sauvegarde suivant (qui est volontairement un peu naïf). Chacun des
articles est représenté sur une seule ligne de texte. Cette ligne débute par
une chaîne de caractères précisant de quel type de contenu numérique il s'agit ("B" pour
un Book, "G" pour un VideoGame ou "P" pour une carte PrePaid).
Ensuite, séparés par un délimiteur (une chaîne de caractères, disons "#"), les différentes
caractéristiques de chacun de ces articles.
- En fonction de l'architecture de classes, classes abstraites et/ou
interfaces que vous avez mis en place pour l'exercice précédent, écrire
les méthodes toTextFormat() nécessaires pour que le code
suivant produise l'affichage indiqué en commentaire. Attention, vous devez
encore essayer d'avoir du code générique en minimisant la duplication de code.
En particulier, on souhaiterait que tout type de média dispose de cette méthode
toTextFormat() qui retourne une String.
public class SaverLoader {
// ...
public static void main(String[] args) {
Book sdb = new Book("S. de Beauvoir", "Mémoires d'une jeune fille rangée", 990);
System.out.println(sdb.toTextFormat());
// B#990#Mémoires d'une jeune fille rangée#S. de Beauvoir
VideoGame zelda = new VideoGame("The legend of Zelda", VideoGame.Console.WII, 4950);
System.out.println(zelda.toTextFormat());
// G#4950#The legend of Zelda#WII
PrePaid pp100 = new PrePaid(10000, 10);
System.out.println(pp100.toTextFormat());
// P#10000#10
}
}
Des constantes pour le délimiteur (dièse) et pour les types d'articles (B, G, P)
peuvent être définies dans une classe SaverLoader:
public class SaverLoader {
public static final String SEPARATOR = "#";
public static final String BOOK_TYPE = "B";
public static final String VIDEO_GAME_TYPE = "G";
public static final String PREPAID_TYPE = "P";
...
-
Écrivez maintenant dans la classe SaverLoader une méthode
saveInTextFormat qui prend en argument une liste d'articles numériques et un flot
d'écriture de caractères de type BufferedWriter; cette méthode écrit dans le flot les chaînes de caractères produites par les méthodes toTextFormat des différents articles de la liste, en les séparant par des retours à la ligne. Ainsi, les articles créés à la question
précédente doivent pouvoir sauvegardés dans un fichier par le code suivant (pensez à créer le chemin du fichier de sauvegarde):
list.add(sdb);
list.add(zelda);
list.add(pp100);
Path saveFilePath = ... ; // nom du fichier de sauvegarde: "saveFile.txt"
try(BufferedWriter bw = Files.newBufferedWriter(saveFilePath,
Charset.forName("UTF-8"),
StandardOpenOption.CREATE)) {
SaverLoader.saveInTextFormat(list, bw);
}
Vérifier ce qui a été écrit dans le fichier!
- Pour relire ce fichier, ou un autre formaté de la même manière, il nous faut maintenant une méthode
loadFromTextFormat qui prend en paramètre un flot de lecture de caractères et retourne une liste d'articles numériques construits à partir des indications lues dans le flot. Écrire cette méthode dans la classe SaverLoader.
Vous pourrez utiliser la méthode br.readLine() pour lire le flot ligne par ligne,
et la méthode split() de la classe String pour découper une ligne suivant
un séparateur donné passé en argument.
Testez-la sur le fichier créé précédemment, en utilisant le try-with-ressources.
- Le fichier ci-dessous contient des articles qui ont été sauvegardés en utilisant l'encodage
ISO-8859-15, extension de l'encodage Latin-1 qui inclut par exemple le symbole €.
Enregistrez ce fichier (en faisant clic-droit enregistrer-sous...) et réutilisez le code
que vous avez écrit à l'exercice précédent pour charger les articles de ce fichier.
Exercice 4 - Réductions
Il arrive que certains clients commandent des articles en plusieurs exemplaires (au moins deux). Dans ce cas, on applique une réductions des 5% à tous les articles concernés.
-
Comment peut-on faire pour gérer les multiplicités des articles de façon efficace?
-
Modifier le code pour que le calcul du prix du panier prenne en compte cette réduction.
-
Pour aller plus loin: utiliser un Stream pour le calcul du prix d'un panier. Vous verrez l'année prochaine comment optimiser également l'affichage.
© Université de Marne-la-Vallée