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
(
) !
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".
Ouvrez dans BlueJ le projet de l'archive tp33e.jar ci-jointe.
1) Voici 5 expressions mathématiques écrites en Java :
a. 2 + 3 / 4
b. 2.0 + 3 / 4
c. 2 + 30E-1 / 4
d. 11 % 4
e. Math.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) :
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) :
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()