TP 3.3 2324v1

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

1) Quelle est la valeur des 5 expressions Java suivantes, c'est-à-dire  Qu'afficherait System.out.println(expression); ? (sans calculatrice et sans programmer)
a2 + 3 / 4
b2.0 + 3 / 4
c2 + 30E-1 / 4
d11 % 4
eMath.cos(Math.PI/4) == Math.sin(Math.PI/4)
- Après avoir écrit les 5 réponses sur papier (voir I.4 et I.2),
- créez
un nouveau projet tp33 et
- programmez les 5 affichages dans une classe Calc (sans attribut) avec une seule procédure expressions (sans paramètre) "pratique à utiliser", puis
- vérifiez les résultats.
Si le résultat du e. vous surprend, affichez séparément chaque côté du ==.
Si vous ne comprenez pas un résultat, demandez à un intervenant.

2) Dans cette classe Calc, créez maintenant uniquement des méthodes "pratiques à utiliser", dont la deuxième sera une fonction racNeg un peu bizarre (et mathématiquement contestable) qui, en plus de calculer/retourner la racine carrée quand son paramètre réel est positif, est aussi capable de calculer/retourner la racine carrée d'un nombre négatif x, par la formule x. (voir III.2)

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. (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.

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).
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) 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. 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.

9) La séance Résa 3.3 est obligatoire si vous n'avez pas terminé tous les exercices de la partie B (*).

C. Exercices du projet, dans cet ordre (à commencer en TP, et à 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.
    Si vous pensez que votre façon d'écrire était tout aussi bonne que celle ci-dessous (hors commentaires), envoyez-la par mail à Denis BUREAU avec vos arguments.

    public void play() // le moteur du jeu (version non graphique)
    {
      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 veut terminer le jeu
      } //
    fin du TANT QUE

      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 de la Liste officielle des exercices.
  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.
    Et prenez l'habitude de créer une nouvelle version pour chaque nouvel exercice.
  7. La séance Résa 3.3 est obligatoire si vous n'avez pas terminé tous les exercices de la partie C (*).
(*) Même si vous avez terminé tous les exercices et que vous n'avez pas l'intention de participer à cette séance Résa, vous devez quand-même cliquer sur le lien pour savoir ce qu'il faut y faire ; il peut y avoir (comme pour cette fois) une tâche à accomplir en plus de terminer les TP.