Programmation orientée objet TP no 8 : Collections Calculatrice en notation polonaise inverse La notation polonaise inverse est une façon d'écrire les expressions arithmétiques de manière nonambigüe sans parenthèse. On peut implémenter une calculatrice basée sur cette notation en utilisant uniquement une pile comme structure de données. Le but de cet exercice est de réaliser une telle calculatrice qui lira ses arguments sur l'entrée standard. En notation polonaise inverse, l'opérateur suit ses opérandes. Par exemple, l'opération 5 + 12 s'écrit 5 12 +. A quelles expressions en notation classique correspondent les expressions en notation polonaise inverse qui suivent. Remarquez que les parenthèses deviennent nécessaires. • 5 12 4 + 3 − × • 12 4 + 5 3 − × L'implémentation de la calculatrice sera basée sur une pile. On rappelle qu'une pile est représentée par l'interface Deque. On se limite aux quatre méthodes push, pop, peek et isEmpty. Utilisez l'algorithme qui suit pour réaliser la calculatrice. Pour chaque argument de la ligne de commande, on appelle une méthode entreeSuivante. La méthode entreeSuivante(String arg) agit sur la pile : Si arg est un nombre, on l'empile. Si arg est un opérateur : On dépile autant de valeurs que l'arité de l'opérateur. On applique l'opérateur (attention à l'ordre des opérandes). On empile le résultat. Sinon arg est un symbole inconnu et on lance une exception. Une fois tous les arguments traités, on ache le résultat. Les opérations suivantes doivent être implémentées : Addition, symbole "+", Soustraction, symbole "-", Multiplication, symbole "x", "X" ou "*", Division, symbole "/", Passage à l'opposé (moins unaire), symbole "~". Parcours en largeur Classes Noeud et Arbre Un noeud d'un arbre quelconque est caractérisé par l'étiquette qui lui est associée (pour nous, ce sera une chaîne de caractères) et par ses ls (en nombre quelconque). Proposez une classe Noeud qui devra disposer : d'un constructeur prenant son étiquette et un nombre variable de noeud ls, d'accesseurs pour l'étiquette et les ls, d'une méthode estFeuille() qui renvoie true si et seulement si le noeud n'a pas de ls, et d'une méthode ajouteFils(Noeud) qui ajoute un noeud ls au noeud courant. 1 Quelle structure de données choisir pour stocker les ls d'un noeud? Assurez qu'ils ne soient pas modiables en dehors de la méthode ajouteFils(). Un arbre est de manière usuelle représenté par son noeud racine. Donnez la classe Arbre. Le parcours Ajoutez une méthode parcoursEnLargeur() dans la classe Arbre. On rappelle que le parcours en largeur nécessite l'usage d'une le. Quel est le type de retour de la méthode parcoursEnLargeur() ? Exemple d'utilisation : Noeud racine = new Node("AbstractCollection", new Node("AbstractSet", new Node("HashSet", new Node("LinkedHashSet")), new Node("AbstractSortedSet", new Node("TreeSet"))), new Node("AbstractList", new Node("ArrayList"), new Node("AbstractSequentialList", new Node("LinkedList"))), new Node("ArrayDeque")); Arbre arbre = new Arbre(racine); for (String etiquette : arbre.parcoursEnLargeur()) { System.out.println(etiquette); } L'exécution de ce code ache à l'écran : AbstractCollection AbstractSet AbstractList ArrayDeque HashSet AbstractSortedSet ArrayList AbstractSequentialList LinkedHashSet TreeSet LinkedList Calculatrice programmable On reprend la calculatrice développée en première partie, le but est de la rendre programmable. Initialement, la calculatrice ne supportera aucune opération. On représente une opération par une instance de l'interface Operation. public interface Operation { int arite(); double calcule(double... args); } Pour programmer la calculatrice, il faudra passer par une méthode ajouterOperation qui prend une Operation en argument suivie d'un nombre quelconque de symboles qui lui sont associés. Quelle structure de données faut-il mettre en place pour retrouver l'opération associée à un symbole? Implémentez la méthode ajouterOperation. 2 Maintenant que la calculatrice est programmable, il faut généraliser la méthode entreeSuivante pour qu'elle supporte n'importe quelle opération et donc des opérations d'arité quelconque. Apportez les modications nécessaires. Pour retrouver la calculatrice initiale, il faudra la paramétrer de la manière suivante : Calculatrice calc = new Calculatrice(); calc.ajouterOperation(new Operation() { public int arite() { return 2; } public double calcule(double... args) }, "+"); calc.ajouterOperation(new Operation() { public int arite() { return 2; } public double calcule(double... args) }, "-"); calc.ajouterOperation(new Operation() { public int arite() { return 2; } public double calcule(double... args) }, "*", "x", "X"); calc.ajouterOperation(new Operation() { public int arite() { return 2; } public double calcule(double... args) }, "/"); calc.ajouterOperation(new Operation() { public int arite() { return 1; } public double calcule(double... args) }, "~"); { return args[0] + args[1]; } { return args[0] - args[1]; } { return args[0] * args[1]; } { return args[0] / args[1]; } { return -args[0]; } Vers Java 8 L'exemple précédent montre qu'il est fastidieux de paramétrer notre calculatrice. Il faut créer des classes (éventuellement de manière anonyme comme dans l'exemple) qui implémentent l'interface Operation pour chaque opération. On souhaite que l'utilisateur puisse paramétrer la calculatrice de manière plus naturelle à l'aide des lambda-expressions de Java 8. Calculatrice calc = new Calculatrice(); calc.ajouterOperation((x, y) -> x + y, "+"); calc.ajouterOperation((x, y) -> x - y, "-"); calc.ajouterOperation((x, y) -> x * y, "*", "x", "X"); calc.ajouterOperation((x, y) -> x / y, "/"); calc.ajouterOperation(x -> -x, "~"); Apportez les modications nécessaires à votre code. Pour cela, vous devrez spécialiser l'interface en fonction de l'arité de l'opération (limitez-vous aux cas unaires et binaires). Il vous faudra également surcharger judicieusement la méthode ajouterOperation. Operation 3