Les entiers naturels - Une interface, plusieurs implémentations
Table of Contents
Session 1 - Travaux pratiques - A rendre pour le 12 octobre 2016, 8h00
Ce TP initie à la conception des interfaces et des classes d'implémentation. Partant d'un type abstrait de données représentant des entiers naturels, nous réaliserons deux implémentations, correspondant respectivement à une restriction du type primitif int et à une définition récursive. Nous continuerons par des manipulations de ce type de données. Finalement, nous définirons une nouvelle implémentation, une variante utilisant la récursion de manière intensive.
Nous chercherons à rendre le code réutilisable, en le rendant le plus possible indépendant de la classe d'implémentation, précisément de ses constructeurs et de la représentation de l'état. C'est une illustration de la méthode recommandée pour la conception d'une classe d'implémentation.
Une interface pour les entiers naturels
Notre intérêt pour les entiers naturels est de nature algébrique puisqu'un des objectifs des TP est de définir une bibliothèque de structures algébriques, la plus générale possible. L'ensemble des entiers naturels \(\{ 0, 1, \ldots \}\) peut être considéré comme un semi-anneau unitaire, soit un monoïde additif et un monoïde multiplicatif, une fois muni de l'addition et de la multiplication.
Nous allons représenter le type de données correspondant aux entiers naturels par une interface qu'on appellera Nat. Comment définir l'interface Nat ?
- Comme le type vérifie des propriétés algébriques, l'interface Nat héritera d'interfaces génériques spécifiant ces propriétés. On utilisera les interfaces du paquet hierarchie fourni dans l'archive.
- Le type doit aussi permettre d'accéder à la valeur d'un entier naturel, suivant les deux points de vue considérés, correspondant chacun à un mode de construction particulier. D'une part, un entier naturel est aussi un entier relatif, représenté par un int en Java. D'autre part, un entier naturel possède une représentation décimale sous la forme d'une suite de chiffres. Ainsi, l'interface Nat déclarera les accesseurs correspondant à ces points de vue.
- Un objet de type Nat est immutable, en lecture seulement donc : il ne peut pas être modifié après construction. Cette propriété permet de l'utiliser comme une valeur d'un type numérique primitif (int ou double), comme vous pourrez l'expérimenter. C'est la raison pour laquelle les accesseurs ne permettent pas de modifier l'état d'un entier naturel : ils ne comportent que des "getters", et aucun "setter". L'immutabilité a aussi pour conséquence la nécessité de construire de nouveaux entiers naturels lors de chaque calcul. Plutôt que d'utiliser un appel à un constructeur, on préfère appeler des méthodes particulières, les fabriques : elles permettent de s'affranchir de la dépendance vis-à-vis des classes d'implémentation. L'ensemble de ces fabriques sera déclaré dans une interface dont héritera l'interface Nat. Cette interface exprimant une propriété, la capacité à construire des entiers naturels, elle sera générique.
Accesseurs
Les accesseurs permettent d'observer un entier naturel suivant les deux points de vue, celui de la restriction d'une part, de la définition récursive d'autre part.
int val()
Méthode renvoyant l'entier relatif égal à l'entier naturel.
int taille()
Méthode renvoyant le nombre de chiffres (aucun 0 superflu en tête).
int chiffre(int i)
Méthode renvoyant le ième chiffre, les unités en 0, les dizaines en un, etc.
Services universels
Ces deux méthodes sont déclarées dans la classe mère Object.
String toString()
Méthode permettant d'obtenir une représentation de l'entier naturel, en base dix.
boolean equals(Object o)
Méthode testant l'égalité entre l'objet cible et l'objet o passé en argument ; renvoie true si o pointe vers un entier naturel n et si l'entier naturel cible et n sont égaux (au sens mathématique), renvoie false sinon.
Interfaces parentes
Toutes ces interfaces sont génériques car elles expriment des propriétés.
FabriqueNaturels<T>
Cette interface déclare les fabriques (méthodes) qu'on souhaite utiliser.
T creerNatAvecValeur(int val)
Méthode renvoyant un entier naturel de valeur l'argument val si val est positif ou nul, lançant une exception java.lang.IllegalArgumentException sinon.
T creerNatAvecRepresentation(String repDecimale)
Méthode renvoyant un entier naturel de représentation décimale l'argumant repDecimale si celui-ci est une suite non vide de chiffres (après retrait des zéros superflus en tête), renvoyant zéro si la représentation est vide, ou lançant une exception java.lang.IllegalArgumentException sinon.
SemiAnneauUnitaireEuclidien<T>
Cette interface générique déclare (par héritage) les services correspondant aux constantes et aux opérations unaires ou binaires qu'on souhaite utiliser avec les entiers naturels. Notez que chaque opération est représentée par une méthode qui possède un paramètre implicite, l'entier naturel cible référencé par this.
T somme(T n)
Méthode renvoyant un nouvel entier naturel de valeur la somme de l'entier naturel cible et de l'argument n.
T zero()
Méthode renvoyant l'élément neutre de l'addition, soit zéro.
T produit(T n)
Méthode renvoyant un nouvel entier naturel de valeur le produit de l'entier naturel cible et de l'argument n.
T un()
Méthode renvoyant l'élément neutre de la multiplication, soit un.
T modulo(T x)
Méthode renvoyant le reste de la division euclidienne de l'entier naturel cible par le diviseur x.
div(T x)
Méthode renvoyant le quotient de la division euclidienne de l'entier naturel cible par le diviseur x.
Une première implémentation
La classe NatParInt implémente l'interface Nat en suivant le premier point de vue. Cette classe est une enveloppe des entiers relatifs de type int, vérifiant une propriété restreignant leur domaine : les entiers naturels sont des entiers relatifs positifs ou nuls. Nous imposons que tout objet de la classe NatParInt est immutable, en lecture seulement : il ne peut pas être modifié après construction. Précisément, les constructeurs initialisent les attributs que les méthodes, ensuite, ne modifient plus. Voici une spécification de cette classe.
Etat
int val
Attribut privé ayant pour valeur un entier relatif positif ou nul.
Constructeur
NatParInt(int val)
Constructeur privé initialisant l'attribut à l'argument val supposé correct.
Fabrique
static final FabriqueNaturels<Nat> FAB
Fabrique constante initialisée par une invocation du constructeur.
Méthodes
Tous les calculs seront effectués en utilisant les int. On appliquera de manière systématique la méthode décomposant la couche en différentes couches : revoir sa description.
Une seconde implémentation
La seconde classe d'implémentation NatDecimal, suivant un point de vue habituel, correspond à la représentation décimale des entiers naturels : un entier naturel est représenté par une suite de chiffres entre 0 et 9, sous la forme d'une String.
Etat
String chiffres
Attribut privé ayant pour valeur une suite non vide de chiffres entre 0 et 9, ne commençant pas par 0 lorsque sa longueur est strictement supérieure à un.
Constructeurs
NatDecimal(String rep)
Constructeur privé initialisant l'attribut à l'argument rep supposé correct.
Fabrique
static final FabriqueNaturels<Nat> FAB
Fabrique constante initialisée par une invocation du constructeur.
Méthodes
Le code de la méthode somme est fourni.
public Nat somme(Nat x) { int t = this.taille() < x.taille() ? x.taille() : this.taille(); String rep = ""; int retenue = 0; for(int i = 0; i < t; i++){ int chiffre = this.chiffre(i) + x.chiffre(i) + retenue; if(chiffre > 9){ chiffre = chiffre - 10; retenue = 1; }else{ retenue = 0; } rep = Integer.toString(chiffre) + rep; } rep = retenue == 0 ? rep : 1 + rep; return this.creerNatAvecRepresentation(rep); }
Tous les autres calculs (produit, division euclidienne) seront effectués en utilisant la somme. Le produit par 10 et la division euclidienne par 10 seront traités comme des cas particuliers : en effet, avec une représentation décimale, les calculs sont alors très simples.
Comme pour l'autre classe, on appliquera de manière systématique la méthode décomposant la classe en différentes couches : revoir sa description.
Travail à faire
Dans le projet dédié au développement, recopier le paquet hierarchie du projet dédié aux corrections : ce paquet contient les interfaces décrivant les structures algébriques. Créer un nouveau paquet session1.tp dans le projet de développement. Les développements suivants seront réalisés dans ce paquet. Voici en résumé le diagramme des types à obtenir.
Définition de l'interface Nat
Définir l'interface Nat.
Première implémentation de l'interface Nat
Pour la première implémentation, on procède par restriction. Implémenter l'interface Nat par la classe NatParInt, en procédant suivant la méthode rappelée ci-dessous.
- Définir l'état.
- Implémenter les constructeurs en accédant directement aux attributs de l'objet cible pointé par this.
- Implémenter les accesseurs en accédant aux attributs de l'objet cible pointé par this directement ou indirectement via d'autres accesseurs.
- Implémenter les fabriques en accédant aux constructeurs directement ou indirectement via d'autres fabriques.
- Implémenter les services en utilisant les seuls accesseurs et fabriques. Précisément, les implémenter en utilisant l'accesseur val() et la fabrique creerNatAvecValeur(int), autrement dit en ramenant les calculs sur les entiers naturels à des calculs sur des int.
- Pour vérifier que la méthode a été suivie correctement, déplacer les services dans l'interface Nat, sans oublier de les qualifier par default.
Seconde implémentation de l'interface Nat
Pour la seconde implémentation, on utilise la représentation décimale. Procéder ainsi pour chaque classe.
- Définir l'état.
- Implémenter les constructeurs en accédant directement aux attributs de l'objet cible pointé par this.
- Implémenter les accesseurs en accédant aux attributs de l'objet cible pointé par this directement ou indirectement via d'autres accesseurs.
- Implémenter les fabriques en accédant aux constructeurs directement ou indirectement via d'autres fabriques.
- Implémenter les services. Recopier le code la méthode somme. Pour les autres opérations algébriques (produit et division euclidienne), utiliser un algorithme naïf fondé sur la somme, en distinguant cependant le cas de la multiplication ou de la division par 10.
- Pour vérifier que la méthode a été suivie correctement, déplacer les services dans l'interface Nat, sans oublier de les qualifier par default.
Test
Les tests sont à placer dans la fonction principale main d'une classe appelée Test. Pour chaque test, il est demandé d'afficher tout d'abord le résultat que vous prévoyez, appelé oracle, puis effectivement le résultat calculé, sur le modèle suivant.
... // Calcul du résultat : rep // Résultat prévu : oracle System.out.println("oracle ? " + rep);
Remarque - Si rep est un objet, un appel implicite à la méthode toString() a lieu dans l'expression "oracle ? " + rep, équivalente à "oracle ? " + rep.toString(). La méthode toString() étant très pratique, tout objet en possède une. Par défaut, la méthode toString() renvoie une représentation de l'adresse de l'objet en mémoire.
- Déclarer une fonction void test(FabriqueNat<Nat> fab) prenant en argument une fabrique d'entiers naturels de type Nat.
- Implémenter dans la fonction test le scénario suivant.
- Créer l'entier zéro à partir de la fabrique et l'affecter à une variable zero.
- Afficher sa valeur.
- Tester l'égalité entre zero et zero.zero().
- Créer l'entier un à partir de la fabrique et l'affecter à une variable un.
- Afficher sa valeur.
- Tester l'égalité entre un et un.un().
- Créer l'entier 5 à partir de la fabrique et l'affecter à une variable cinq.
- Afficher sa valeur.
- Créer l'entier 6 à partir de la fabrique et l'affecter à une variable six.
- Calculer la somme de 5 et 6 et afficher le résultat.
- Calculer le produit de 5 et 6 et afficher le résultat.
- Calculer le quotient et le reste de la division euclidienne de 33 par 6.
- Calculer la somme de 2\000\000\000 avec lui-même. Rattraper l'exception possiblement lancée. Voir les tests du paquet session1.demo1.v2 pour un exemple de rattrapage.
- Compléter la fonction principale en appelant la fonction test deux fois, avec des fabriques de type dynamique NatParInt et NatDecimal.
Utilisation de la multiplication à la russe
Dans la fonction de test, tester la fonction fournie Outils.produitRusse, en refaisant le produit de 5 et 6. Etudier sa définition et son utilisation.