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)
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".
1) Quelle est la valeur des 5 expressions Java suivantes,
c'est-à-dire Qu'afficherait System.out.println(expression); ? (sans calculatrice et sans programmer)
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)
- 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
.
(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) :
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 (*).
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()