TP 3.3 2425v1

Les parties I. à IV. du TP 3.2 doivent être entièrement terminées et testées avant de commencer ce TP.

Ce TP a ensuite pour but :
- de comprendre la différence de 'dangerosité' d'utilisation des types primitifs pour les nombres entiers et réels
- d'apprendre à utiliser et créer des méthodes statiques (← nouveau)
- de commencer les exercices du projet (de la liste officielle)

A. LIRE EN DÉTAIL (les IV parties, et poser des questions si tout n'est pas clair)

I. Les types primitifs

I.1 Pour l'instant, nous ne connaissons que le type boolean (dont les variables ne peuvent contenir que les valeurs true ou false) et le type int (abréviation de integer, entier en anglais, dont les variables sur 32 bits ne peuvent contenir que les valeurs -231 à +231-1, c'est-à-dire environ -2 milliards à +2 milliards).
+ Pour information, il existe 3 autres types entiers sur 8, 16, et 64 bits que nous n'apprendrons pas pour le moment. Au fait, que se passe-t-il si on ajoute 1 à une variable qui contient déjà le plus grand entier d'un type entier donné ?

I.2 Comment représenter des nombres qui ne sont pas entiers, plus précisément des nombres réels ?
+ On utilise généralement le type double (qui signifie double precision en anglais, même s'il existe aussi un type réel simple précision) qui occupe 64 bits. Les variables de ce type peuvent bien sûr valoir 0.0, ou bien contenir un nombre entre -10+308 et -10-323 ou entre 10-323 et 10+308 (bornes approximatives, pour donner un ordre de grandeur).

+ Mais attention ! Ce type est dangereux pour au moins deux raisons :
I.2.1 On ne peut pas représenter n'importe quel nombre réel positif entre 10-323 et 10+308. En effet, entre 1,8 et 1,9 par exemple, il y en a une infinité, et nous n'avons qu'un nombre fini de bits. Nous avons droit à 16 chiffres significatifs (c'est-à-dire hors zéros initiaux ou finaux), mais du fait du mode de représentation interne choisi (une somme d'inverses de puissances uniques de 2), on ne peut même pas représenter de façon exacte un nombre aussi simple que 0,1 ( 124+125+128+129+) !
I.2.2 La précision de calcul étant limitée, les résultats sont souvent inexacts à 10-16 près ; cela peut paraître peu, mais cette erreur peut s'aggraver au fil des calculs ! À part pour la valeur particulière 0.0, on ne doit pas utiliser == ou != qui réalisent une comparaison binaire (ne pas utiliser non plus <= ou >=). Il faut donc systématiquement employer une formule du style |x-y|<ε, ε étant réglable par le programmeur en fonction de ses exigences de précision.

Pour en savoir plus, lire ce document (even more explanations here ou la théorie ici) -> à faire en dehors du TP.

I.3 + Les valeurs littérales du type double peuvent se noter de 2 façons différentes :
I.3.1 Soit avec un point décimal (équivalent de notre virgule française), par exemple : 0.12  34.5  6. ou 6.0
I.3.2 Soit avec un e qui signifie "... multiplié par 10 puissance ...", par exemple 1e6 (= un million), 5e-3 (= cinq millièmes), ou 12.34e10 (Attention ! 10e-2 ça ne fait pas un centième !)

I.4 Les opérations habituelles fonctionnent (+, -, *, /, avec la même priorité qu'en mathématiques), mais pas d'opérateur carré ni élévation à la puissance : il faudra utiliser la fonction mathématique pow.
Il faut bien comprendre la différence entre la division réelle et la division euclidienne (dans les entiers), toutes deux déclenchées par / :
+ La division euclidienne ne peut s'exécuter que s'il y a un entier des 2 côtés du / (ou du % si on veut obtenir le reste) ; donc, s'il y a un réel (voire 2) avant et/ou après le /, c'est la division réelle qui est effectuée. Par exemple, 1/2 vaut 0, alors que 1/2.0 ou 1.0/2 valent bien 0.5.  

II. Les conversions de types primitifs (cast)

+ Il est possible de convertir une valeur (pas une variable !) d'un type numérique en une valeur d'un autre type numérique (indiqué juste devant entre parenthèses), avec ou sans perte d'information, selon le sens de la conversion :
- int vEnt = (int)vX;  permet de mettre dans vEnt la partie entière de vX (variable réelle ayant déjà été définie)
- double vY = vEnt1 / (double)vEnt2; permet de mettre dans vY le résultat de la division réelle de deux entiers vEnt1et vEnt2 (ayant déjà été définis)
Attention !  Dans le premier exemple, vX ne change pas de type, et dans le second exemple, vEnt2 ne change pas de type !
Cela influe non seulement sur le type de la valeur retournée, mais pour la division, cela peut aussi changer la valeur du résultat.

III. Les fonctions mathématiques

III.1 Maintenant que nous connaissons les nombres réels (double), il est intéressant de connaître les différentes fonctions mathématiques qu'offre la classe Math de java pour faire des calculs avec des nombres réels, telles que racine carrée, sinus/cosinus, logarithmes, etc.
Comme pour la classe String, il n'est pas nécessaire d'importer la classe Math pour pouvoir s'en servir, car elle fait partie des bases du langage (on verra plus tard que ces classes sont stockées dans un dossier spécial nommé java.lang).

Si ces fonctions étaient des méthodes "classiques", il faudrait écrire pour calculer la racine carrée (car jusqu'ici on a toujours eu besoin d'un objet pour appeler une méthode) :
Math vM = new Math(); double vRacineDe2 = vM.sqrt( 2.0 ); ce qui est un peu lourd ... (sqrt : square root)

+ III.2 Heureusement, il est possible d'appeler cette méthode directement sur la classe :
double vRacineDe2 = Math.sqrt( 2.0 );
tout simplement parce-que cette méthode (comme toutes les autres fonctions mathématiques) est déclarée static dans la classe Math.
Ces méthodes sont dites "de classe" (car elles sont appelées directement sur la classe) alors que jusqu'à présent, nous n'avions vu que des méthodes dites "d'instance" (car elles doivent être appelées sur une instance = un objet).

III.3 Comment peut-on le savoir ?
Il suffit de regarder la javadoc (le mot public n'apparaît pas dans la javadoc ci-dessous, mais il existe dans le code java !) :


+ III.4 À signaler une autre utilisation intéressante d'une méthode statique du JDK :
il existe une classe Integer (que nous verrons plus en détail lors de la séquence sur les collections) qui contient une méthode statique parseInt pour extraire d'une String qu'on lui passe en paramètre le nombre entier correspondant.
Par exemple, si la String vaut "123", cette méthode retournera l'entier 123.
Saurez-vous l'appeler correctement pour mettre dans une variable entière vN le nombre entier correspondant à la String vS (supposée contenir des chiffres correspondant à un nombre entier) ?  

IV. Les méthodes statiques

+ IV.1 On peut bien sûr déclarer ses propres méthodes statiques lorsqu'on veut permettre de les appeler sans avoir à créer d'objet. Il suffit pour cela d'ajouter le mot static dans la signature de la méthode, juste après public ou private.

+ IV.2 Attention ! Dans ce cas, comme la méthode sera appelée directement sur la classe, il n'y aura pas d'objet courant. Il sera donc interdit d'utiliser this, que ce soit pour les attributs ou les méthodes.
Rappel : m() signifie Classe.m() si m est statique, sinon m() signifie this.m(). C'est pour cela qu'il vaut mieux être explicite et ne jamais utiliser la forme m().

IV.3 Un cas fréquent d'utilisation est pour essayer/tester une classe, par exemple la classe Rationnel. On crée alors une classe Utilisation, sans attribut ni constructeur, qui ne contient qu'une procédure statique essai, dont le rôle sera d'instancier (*) quelques rationnels et d'appeler des méthodes dessus.
(*) Rappel : Instancier une classe veut dire créer une nouvelle instance de cette classe, c'est-à-dire un nouvel objet de cette classe. Cet anglicisme est très utilisé en java, et le compilateur vous parlera d' instance variables (variables d'instance) qui ne sont autres que les attributs que nous avons vus, ou attributs d'instance.

IV.4 Une méthode statique peut aussi être utile lorsqu'on vous demande juste d'écrire une fonction de calcul : on crée alors une classe (qui ne sera jamais instanciée), sans attribut ni constructeur, qui ne contient que la fonction, déclarée statique, qui sera donc "pratique à utiliser".


B. EXERCICES

Ouvrez dans BlueJ le projet de l'archive tp33e.jar ci-jointe.

1) Voici 5 expressions mathématiques écrites en Java :
a2 + 3 / 4
b2.0 + 3 / 4
c2 + 30E-1 / 4
d11 % 4
eMath.cos(Math.PI/4) == Math.sin(Math.PI/4)
1.1 Dans ce projet tp33e, créez une classe Calc sans attribut dans laquelle vous créerez uniquement une procédure expressions, "pratique à utiliser", sans paramètre, qui ne contiendra pour l'instant que les 5 lignes ci-dessus en commentaires.
1.2 Calculez (sans calculatrice !) les 5 résultats (voir I.4 et I.2) et ajoutez-les à la fin de chaque commentaire.
1.3 Ajoutez maintenant les 5 instructions nécessaires pour afficher le résultat des 5 expressions. Compilez, exécutez.
1.4 Comparez avec vos calculs précédents et si le résultat du e. vous surprend, affichez séparément chaque côté du == ; appelez un intervenant si vous ne comprenez pas un résultat.
Lorsque vous n'avez plus d'erreur de compilation, clic-droit sur la classe CalcTest et exécuter test_expressions_1().
Si vous voyez apparaître tout en bas la mention 'test_expressions_1 succeeded', vous pouvez passer à la suite.
Sinon, vous devez voir apparaître une fenêtre 'BlueJ: Test Results'. Cliquez sur la ligne 'v1.CalcTest.test_expressions_1' pour voir le message d'erreur sous la barre rouge.

2) Dans cette classe Calc, créez à partir de maintenant uniquement des méthodes "pratiques à utiliser".
La deuxième sera une fonction racEnt qui calcule/retourne la partie entière de la racine carrée du nombre réel passé en paramètre (voir II. et III.2).
Lorsque vous n'avez plus d'erreur de compilation, clic-droit sur la classe CalcTest et exécuter test_racEnt_2().
Si vous voyez apparaître tout en bas la mention 'test_racEnt_2 succeeded', vous pouvez passer à la suite.
Sinon, cliquez sur la ligne 'v1.GameTest.test_racEnt_2' pour voir le message d'erreur sous la barre rouge.

3) afficheMoities
En utilisant la boucle appropriée, écrivez une procédure afficheMoities qui affiche le nombre entier (strictement positif) passé en paramètre, suivi de ses moitiés successives jusqu'à arriver à 1, c'est-à-dire tant qu'on n'est pas arrivé à 1 (ce que l'on peut aussi reformuler «jusqu'à atteindre la valeur 1»). (voir les rappels sur la boucle while au TP précédent)
- On entend par "moitié" le résultat de la division entière par 2, c'est-à-dire que 5 est la moitié de 11. Compilez. Testez.
- Par exemple, si le paramètre vaut 45, on affichera à raison d'un nombre par ligne : 45, 22, 11, 5, 2, 1.
Aide : Si a et b sont deux entiers, a/b calcule bien la division entière (euclidienne) de a par b.
Contrainte : Laissez le paramètre final, et n'écrivez qu'une seule instruction d'affichage.
Lorsque vous n'avez plus d'erreur de compilation, clic-droit sur la classe CalcTest et exécuter test_afficheMoities_3().
Si vous voyez apparaître tout en bas la mention 'test_afficheMoities_3 succeeded', vous pouvez passer à la suite.
Sinon, cliquez sur la ligne 'v1.GameTest.test_afficheMoities_3' pour voir le message d'erreur sous la barre rouge.

4) sontProches
Ajoutez une fonction booléenne qui vérifie si ses 2 paramètres réels sont suffisamment proches (à une certaine précision près, figée dans la fonction, ici 10-9). Vérifiez que les 2 valeurs 0.123456784 et 0.123456786 ne sont pas suffisamment proches ... (voir I.2.2 et I.3)
Et comment l'écrire sans aucun if (ni boucle, bien sûr) ?
+ Math.abs() retourne bien la valeur absolue (aussi bien d'un réel que d'un entier, contrairement au langage C qui nécessite une fonction abs pour les entiers et une fonction fabs pour les réels).
Lorsque vous n'avez plus d'erreur de compilation, clic-droit sur la classe CalcTest et exécuter test_sontProches_4().
Si vous voyez apparaître tout en bas la mention 'test_sontProches_4 succeeded', vous pouvez passer à la suite.
Sinon, cliquez sur la ligne 'v1.GameTest.test_sontProches_4' pour voir le message d'erreur sous la barre rouge.

Question subsidiaire : En utilisant cette fonction, 10-15 et 10-12 seront-ils considérés comme proches ou pas ?  Que faudrait-il modifier pour obtenir un résultat plus sensé ?

5) [EN TRAVAIL PERSONNEL sauf si vous avez tout terminé] Créez une classe Utilisation (qui n'a pas vocation à être instanciée, donc comment allez-vous faire pour appeler son unique méthode ?) ne contenant qu'une procédure essai (sans paramètre) qui appellera les 3 méthodes précédentes, afin de vérifier leur bon fonctionnement sur quelques exemples (couvrant les principaux cas particuliers). (voir IV.4)

6) Méthodes simples sur les tableaux (créez une classe Tabs dans le même projet BlueJ) (voir les rappels sur les tableaux au TP précédent) :

  1. Écrivez une procédure affTab qui devra afficher le tableau d'entiers passé en paramètre (supposé avoir été par ailleurs rempli de valeurs), à raison d'une valeur par ligne, chaque ligne étant de la forme tab[indice ] = valeur de la case vTab[indice], exemple : tab[3] = 6.
    AIDE : Pour essayer cette procédure interactivement sous BlueJ, on peut lui passer un tableau sous la forme { 10, 20, 30, 40 } par exemple.
    QUESTION : Que se passera-t-il si le tableau est vide (0 cases) ?

  2. Écrivez une procédure affTabInv presqu'identique (ne rien modifier dans l'instruction d'affichage !) qui devra afficher le tableau d'entiers passé en paramètre, mais à l'envers (en commençant par la fin !).

  3. Écrivez une procédure initTab qui devra initialiser le tableau d'entiers passé en paramètre, avec dans chaque case, 2 fois la valeur de l'indice de la case (il y aura donc la valeur 6 dans la 4ème case !).
    + Quand on passe un tableau p en paramètre, même final, on peut modifier la valeur de chacune de ses cases. Par contre, on ne peut toujours pas écrire p = quelqueChose;

  4. Si vous n'avez pas effectué le 5) ci-dessus, créez la classe Utilisation comportant juste une procédure essai vide.
    Complétez la procédure essai sans paramètre qui crée un tableau d'entiers (avec un nombre de cases que vous devrez fixer), puis appelle successivement initTab, affTab, et affTabInv.

  5. Écrivez une fonction somme qui calcule la somme des valeurs du tableau d'entiers passé en paramètre (et qui évidemment n'affiche rien), puis l'essayer dans la procédure essai.

7) valeurMinimale
- Ajoutez une fonction qui retourne la valeur minimale du tableau de notes (resteront entre 0 et 20, mais 12,75 est possible) passé en paramètre., et testez-la dans la procédure essai.
Contrainte : Utilisez la boucle for exposée en partie II. du TP 3.2.
Remarque : La création et le remplissage du tableau avec des notes sont supposés avoir été faits par ailleurs et ne concernent pas cet exercice.
- Si vous avez utilisé 0 ou 20 comme valeur particulière dans votre fonction, que faut-il modifier si on suppose maintenant que le tableau peut contenir autre chose que des notes, donc des valeurs quelconques (éventuellement toutes hors de l'intervalle [0,20]) ?
- Exemple pour vérifier «à la main» votre algorithme :
33,32,37,35,38,34,29,36,37,28,29,30,36,37,31,29,28,30,28,29,28,37,31,30,33

8) indiceDuMinimum
Ajoutez une fonction qui retourne l'indice de la valeur minimale d'un tableau de valeurs réelles quelconques passé en paramètre. Si cette valeur apparaît plusieurs fois dans le tableau, retourner le plus petit indice.
Remarque: Le remplissage du tableau avec au moins une note est supposé avoir été fait par ailleurs et ne concerne pas cet exercice.
- Que faut-il modifier dans la fonction pour retourner (si la valeur minimale apparaît plusieurs fois dans le tableau) le plus grand indice, tout en n'affectant pas plusieurs fois l'indice du minimum dans ce cas ?
Si vous ne trouvez pas, demandez-vous précisément ce qui fait que votre fonction trouve le plus petit indice du minimum et non le plus grand indice du minimum ...
- Complétez la procédure essai pour utiliser cette fonction sur des cas significatifs.

Quand tous les tests passent sans problème dans les classes de test et dans la classe Utilisation, fermez le projet tp33e.


C. Exercices du projet, dans cet ordre (à terminer en Résa, puis en travail personnel) :

  1. Voici la procédure play() que vous avez dû écrire dans la classe Game, avec des commentaires à chaque ligne pour expliquer son fonctionnement.
    Vous devez la lire attentivement et en détail. Si vous ne comprenez pas 100% de ce qui est ci-dessous, demandez à l'intervenant.

    public void play() // le moteur du jeu
    {
      this.printWelcome(); //
    affiche un message de bienvenue une seule fois au début
      boolean vFinished = false; //
    affirme que le jeu n'est pas terminé

      while ( vFinished == false ) { // TANT QUE le jeu n'est pas terminé, FAIRE :
        Command vCommand = this.aParser.getCommand(); //
    récupère la commande tapée par le joueur
        vFinished = this.processCommand( vCommand ); //
    exécute la commande et mémorise si le joueur
      } //
    fin du TANT QUE                                                                                                                                veut terminer le jeu

      System.out.println( "Thank you for playing. Good bye." ); // affiche le message une seule fois à la fin
    } // play()

  2. Terminer la partie V. du TP 3.2.
  3. Faire le point 4 de l'exercice 7.4.
  4. Faire le point 3 de l'exercice 7.4.
  5. Faire le point 2 de l'exercice 7.4 (lecture).
  6. Commencer à améliorer la programmation de votre jeu en faisant l'exercice 7.5.