Découvrir les opérateurs, les fonctions maths et le transtypage

Figure A - Conversions légales entre types numériques

Les opérateurs sont utilisés pour combiner les valeurs. Comme vous le verrez dans les sections suivantes, Java possède un ensemble riche d’opérateurs arithmétiques et logiques et de fonctions mathématiques.

Opérateurs arithmétiques

Les opérateurs arithmétiques habituels +, *, / sont utilisés en Java pour l’addition, la soustraction, la multiplication et la division. L’opérateur / indique une division entière si les deux arguments sont des entiers, et une division en virgule flottante sinon. Le reste entier (parfois appelé module) est noté en %.

Par exemple, 15 / 2 est 7, 15 % 2 est 1 et 15.0 / 2 est 7,5.

Notez que la division entière par 0 soulève une exception, alors que la division en virgule flottante par 0 donne un résultat infini ou NaN.

L’un des objectifs déclarés du langage de programmation Java est la portabilité. Un calcul devrait donner les mêmes résultats quelle que soit la machine virtuelle qui l’exécute.

Pour les calculs arithmétiques avec des nombres à virgule flottante, il est étonnamment difficile d’obtenir cette portabilité.

Le type double utilise 64 bits pour stocker une valeur numérique, mais certains processeurs utilisent des registres à virgule flottante de 80 bits.

Ces registres apportent une précision supplémentaire dans les étapes intermédiaires d’un calcul. Par exemple, considérons le calcul suivant :

double w = x * y / z;

De nombreux processeurs Intel calculent x * y, laissent le résultat dans un registre de 80 bits, puis divisent par z, et enfin tronquent le résultat à 64 bits. Cela permet d’obtenir un résultat plus précis et d’éviter les débordements d’exposants.

Mais le résultat peut être différent d’un calcul qui utilise 64 bits partout. Pour cette raison, la spécification initiale de la machine virtuelle Java exigeait que tous les calculs intermédiaires soient tronqués.

La communauté numérique détestait ça. Non seulement les calculs tronqués peuvent causer des débordements, mais ils sont en fait plus lents que les calculs plus précis parce que les opérations de troncature prennent du temps.

Pour cette raison, le langage de programmation Java a été mis à jour pour tenir compte des exigences en conflit pour des performances optimales et une parfaite reproductibilité.

Par défaut, les concepteurs des machines virtuelles Java ont autorisé la possibilité d’utiliser une précision étendue pour les calculs intermédiaires.

Cependant, les méthodes étiquetées avec le mot-clé strictfp doivent utiliser des opérations en virgule flottante strictes qui donnent des résultats reproductibles.
Par exemple, vous pouvez marquer la méthode main comme suit:

public static strictfp void main(String[] args)

Ensuite, toutes les instructions à l’intérieur de la méthode main utiliseront des calculs en virgule flottante stricts. Si vous marquez une classe par strictfp, alors toutes ses méthodes doivent utiliser des calculs en virgule flottante stricts.

Les détails effrayants sont très liés au comportement des processeurs Intel. Dans le mode par défaut, les résultats intermédiaires sont autorisés à utiliser un exposant étendu, mais pas une mantisse étendue.

Les puces Intel supportent la troncature de la mantisse sans perte de performance. Par conséquent, la seule différence entre le mode par défaut et le mode strict est que les calculs stricts peuvent déborder lorsque les calculs par défaut ne le font pas.

Le débordement à virgule flottante n’est pas un problème que l’on rencontre dans la plupart des programmes courants.

Fonctions et constantes mathématiques

La classe Math contient un grand choix de fonctions mathématiques dont vous pouvez avoir besoin à l’occasion, selon le type de programmation que vous faites.

Pour obtenir la racine carrée d’un nombre, utilisez la méthode sqrt :

double x = 4;
double y = Math.sqrt(x);
System.out.println(y); // imprime 2.0

Il y a une différence subtile entre la méthode println et la méthode sqrt. La méthode println fonctionne sur l’objet System.out. Mais la méthode sqrt de la class Math ne fonctionne sur aucun objet.

Une telle méthode s’appelle une méthode statique. Pour en savoir plus sur les méthodes statiques, consultez un article sur les objets et les classes sur notre site.

Le langage de programmation Java n’a pas d’opérateur pour élever une quantité à une puissance. Pour ce faire, vous devez utiliser la méthode pow de la classe Math. La déclaration

double y = Math.pow(x, a);

affecte à y le résultat de x à la puissance a (xa). Les paramètres de la méthode pow sont tous les deux de type double, et elle retourne aussi un double.

La méthode floorMod vise à résoudre un problème avec les restes entiers. Considérons l’expression n % 2. Tout le monde sait que c’est 0 si n est pair et 1 si n est impair. Sauf, bien sûr, quand n est négatif. Alors c’est -1.

Pourquoi ? Quand les premiers ordinateurs ont été construits, quelqu’un a dû faire des règles pour savoir comment la division entière et le reste devraient fonctionner pour les opérandes négatifs.

Les mathématiciens connaissaient la règle optimale (ou “euclidienne”) depuis quelques centaines d’années : toujours laisser le reste ≥ 0. Mais, plutôt que d’ouvrir un manuel de mathématiques, ces pionniers ont proposé des règles qui semblent raisonnables, mais qui sont en fait peu pratiques.

La méthode floorMod facilite les choses : floorMod(entier1, entier2) donne toujours une valeur entre 0 et entier2-1 (Malheureusement, floorMod donne des résultats négatifs pour les diviseurs négatifs (entier2<0)).

La classe Math fournit les fonctions trigonométriques habituelles :

  • Math.sin
  • Math.cos
  • Math.tan
  • Math.atan
  • Math.atan2

et la fonction exponentielle avec son inverse, le logarithme naturel, ainsi que le logarithme décimal :

  • Math.exp
  • Math.log
  • Math.log10

Enfin, deux constantes représentent les approximations les plus proches possibles des constantes mathématiques π et e :

  • Math.PI
  • Math.E

Vous pouvez éviter le préfixe Math pour les méthodes et constantes mathématiques en ajoutant la ligne suivante en haut de votre fichier source :

import static java.lang.Math.*;

Par exemple :

System.out.println("La racine carrée de \u03C0 est " + sqrt(PI)) ;

Les méthodes de la classe Math utilisent les routines de l’unité à virgule flottante de l’ordinateur pour des performances optimales. Si des résultats complètement prévisibles sont plus importants que la performance, utilisez plutôt la classe StrictMath.

Il implémente les algorithmes de la librairie “Freely Distributable Math Library“, garantissant des résultats identiques sur toutes les plateformes.

La classe Math fournit plusieurs méthodes pour rendre l’arithmétique des nombres entiers plus sûre. Les opérateurs mathématiques renvoient discrètement des résultats erronés lorsqu’un calcul déborde.

Par exemple, un milliard fois trois (1000000000 * 3) évalue à -1294967296 parce que la plus grande valeur int est juste au-dessus de deux milliards. Si vous appelez Math.multiplyExact(1000000000, 3) à la place, une exception est générée.

Vous pouvez attraper cette exception ou laisser le programme se terminer plutôt que de continuer tranquillement avec un mauvais résultat. Il existe aussi des méthodes addExact, soustractExact, incrémentExact, decrementExact, decrementExact, negateExact, toutes avec des paramètres int et long.

Conversions entre types numériques

Il est souvent nécessaire de passer d’un type numérique à un autre.

La figure A illustre les conversions légales.

Figure A - Conversions légales entre types numériques

Figure A – Conversions légales entre types numériques

Les six flèches pleines de la figure A indiquent les conversions sans perte d’information. Les trois flèches pointillées indiquent les conversions qui peuvent perdre en précision.

Par exemple, un grand nombre entier tel que 123456789 a plus de chiffres que le type des flottants ne peut représenter. Lorsque l’entier est converti en un flottant, la valeur résultante a la grandeur correcte mais perd une certaine précision.

int n = 123456789;
float f = n; // f est 1.23456792E8

Lorsque deux valeurs sont combinées avec un opérateur binaire (par exemple n + f où n est un entier et f est une valeur en virgule flottante), les deux opérandes sont convertis en un type commun avant que l’opération soit effectuée.

Si l’un des opérandes est de type double, l’autre sera converti en double.

Sinon, si l’un des opérandes est de type float, l’autre sera converti en float.

Sinon, si l’un des opérandes est de type long, l’autre sera converti en long.

Sinon, les deux opérandes seront convertis en int.

Transtypage (Casts)

Dans la section précédente, vous avez vu que les valeurs int sont automatiquement converties en valeurs doubles si nécessaire. D’un autre côté, il y a évidemment des moments où vous voulez considérer un double comme un entier.

Les conversions numériques sont possibles en Java, mais bien sûr des informations peuvent être perdues. Les conversions dans lesquelles la perte d’informations est possible se font au moyen du transtypage.

La syntaxe de ce dernier est de saisir le type cible entre parenthèses, suivi du nom de la variable. Par exemple :

double x = 9.997;
int nx = (int) x;

Maintenant, la variable nx a la valeur 9 parce que le transtypage d’une valeur en virgule flottante en un entier élimine la partie fractionnaire.

Si vous voulez arrondir un nombre à virgule flottante au nombre entier le plus proche (ce qui est dans la plupart des cas une opération plus utile), utilisez la méthode Math.round :

double x = 9.997;
int nx = (int) Math.round(x);

Maintenant la variable nx a la valeur 10. Vous devez toujours utiliser le transtypage (int) lorsque vous appelez la méthode round.

La raison en est que la valeur de retour de la méthode round est de type long, et bien sûr une valeur de type long ne peut être assignée à un int qu’avec un transtypage explicite car il y a la possibilité de perte d’information.

Si vous essayez de convertir un nombre d’un type à un autre qui est hors de la plage des valeurs supportées par le type cible, le résultat sera un nombre tronqué qui a une valeur différente. Par exemple, (byte) 300 est en fait 44.

Vous ne pouvez pas convertir entre des valeurs booléennes et n’importe quel type numérique. Cette convention permet d’éviter les erreurs courantes.

Dans le cas rare où vous voulez convertir une valeur booléenne en un nombre, vous pouvez utiliser une expression conditionnelle telle que b ? 1 : 0.

Combinaison de l’affectation et des opérateurs

Il existe un raccourci pratique pour utiliser des opérateurs binaires dans une affectation. Par exemple,

x += 4 ;

est équivalent à

x = x + 4 ;

(En général, placez l’opérateur à gauche du signe =, tel que *= ou %=).

Si l’opérateur donne une valeur dont le type est différent de celui du côté gauche, il est automatique ajusté. Par exemple, si x est un int, alors l’instruction

x += 3.5 ;

est valide, en réglant x sur (int)(x + 3.5).

En effet, le code suivant affiche 5 comme valeur de x au lieu de 5.5.

int x=2;
x += 3.5 ;
System.out.println(x);

Opérateurs d’incrémentation et de décrémentation

Les programmeurs, bien sûr, savent que l’une des opérations les plus courantes avec une variable numérique est d’ajouter ou de soustraire 1.

Java, en suivant les traces de C et C++, a des opérateurs d’incrémentation et de décrémentation : n++ ajoute 1 à la valeur courante de la variable n, et n– soustrait 1 de celle-ci. Par exemple, le code

int n = 12 ;
n++ ;

change la valeur de n à 13. Comme ces opérateurs modifient la valeur d’une variable, ils ne peuvent pas être appliqués aux nombres eux-mêmes. Par exemple, 4++ n’est pas une déclaration légale.

Il y a deux formes de ces opérateurs ; vous venez de voir la forme postfixe de l’opérateur qui est placée après l’opérande. Il y a aussi la forme préfixe ++n.

Les deux modifient la valeur de la variable de 1, la différence entre les deux n’apparaît que lorsqu’elles sont utilisées à l’intérieur d’expressions. La forme préfixe fait l’addition d’abord; la forme postfixe évalue d’abord la valeur de la variable et ensuite la change.

int m = 7;
int n = 7;
int a = 2 * ++m; // maintenant a est 16, m est 8
int b = 2 * n++; // maintenant b est 14, n est 8

Nous déconseillons l’utilisation d’expressions internes ++ car cela conduit souvent à du code déroutant et à des bogues gênants.

Opérateurs relationnels et booléens

Java dispose de l’ensemble des opérateurs relationnels. Pour tester l’égalité, utilisez un double signe égal, ==. Par exemple, la valeur de 3 == 7 est False.

Utilisez != pour inégalité. Par exemple, la valeur de 3 != 7 est True.

Enfin, vous avez les opérateurs habituels < (inférieur à), > (supérieur à), <= (inférieur ou égal) et >= (supérieur ou égal).

Java, en suivant C++, utilise && pour l’opérateur logique “ET” et || pour l’opérateur logique “OU”. Comme vous pouvez facilement vous en rappeler de l’opérateur !=, le point d’exclamation ! est l’opérateur de négation logique.

Les opérateurs && et || sont évalués en “court-circuit” : Le deuxième argument n’est pas évalué si le premier paramètre détermine déjà la valeur.

Si vous combinez deux expressions avec l’opérateur &&, expression1 && expression2
et que la valeur de vérité de la première expression a été déterminée comme étant fausse, alors il est impossible que le résultat soit vrai.

Ainsi, la valeur de la deuxième expression n’est pas calculée. Ce comportement peut être exploité pour éviter les erreurs. Par exemple, dans l’expression

x != 0 && 1 / x > x + y      // aucune division par 0

la deuxième partie n’est jamais évaluée si x est égal à zéro.

Ainsi, 1 / x n’est pas calculé si x est nul, et aucune erreur de division par zéro ne peut se produire.

De même, la valeur de expression1 || expression2 est automatiquement vraie si la première expression est vraie, sans évaluer la seconde expression.
Enfin, Java supporte l’opérateur ternaire ? : qui est parfois utile. L’expression

condition ? expression1 : expression2

retourne la première expression si la condition est vraie, à la seconde expression sinon. Par exemple, x < y ? x : y donne le plus petit de x et y.

Opérateurs bit-à-bit

Pour tous les types d’entiers, vous avez des opérateurs qui peuvent travailler directement avec les bits qui les composent. Cela signifie que vous pouvez utiliser des techniques de masquage pour obtenir des bits individuels dans un nombre. Les opérateurs bit-à-bit sont les suivants

& (“et”), | (“ou”), ^ (“xor”), ~ (“négation”)

Ces opérateurs travaillent sur des modèles binaires. Par exemple, si n est une variable entière, alors

int fourthBitFromRight = (n & 0b1000) / 0b1000;

vous donne un 1 si le quatrième bit à partir de la droite dans la représentation binaire de n est 1, et 0 sinon. L’utilisation de & avec la puissance appropriée de 2 vous permet de masquer tout sauf un seul bit.

Lorsqu’ils sont appliqués aux valeurs booléennes, les opérateurs & et | donnent une valeur booléenne. Ces opérateurs sont similaires aux opérateurs && et ||, sauf que les opérateurs & et | ne sont pas évalués en “court-circuit”, c’est-à-dire que les deux arguments sont évalués avant que le résultat soit calculé.

Il y a aussi des opérateurs >> et << qui décalent un motif binaire vers la droite ou vers la gauche. Ces opérateurs sont pratiques lorsque vous avez besoin de construire des chaînes de bits pour faire du masquage de bits :

int fourthBitFromRight = (n & (1 << 3)) >> 3;

Enfin, l’opérateur >>> remplit les bits supérieurs avec zéro, contrairement à >> qui étend le bit de signe dans les bits supérieurs. Il n’y a pas d’opérateur <<<.

L’argument de droite des opérateurs de décalage est réduit modulo 32 (sauf si l’argument de gauche est un long, auquel cas l’argument de droite est réduit modulo 64). Par exemple, la valeur de 1 << 35 est la même que 1 << 3 ou 8.

En C/C++, il n’y a aucune garantie que >> effectue un décalage arithmétique (extension du bit de signe) ou un décalage logique (remplissage avec des zéros). Les responsables de la mise en œuvre sont libres de choisir celle qui est la plus efficace.

Cela signifie que l’opérateur C/C++ >> peut donner des résultats dépendants de l’implémentation pour des nombres négatifs. Java élimine cette incertitude.

Parenthèses et priorité des opérateurs

Le tableau A montre l’ordre de priorité des opérateurs. Si aucune parenthèse n’est utilisée, les opérations sont effectuées dans l’ordre de priorité indiqué. Les opérateurs de même niveau sont traités de gauche à droite, à l’exception de ceux qui sont associatifs à droite, comme indiqué dans le tableau.

Par exemple, && a une priorité plus élevée que ||, donc l’expression a && b || c signifie (a && b) || c.

Puisque += est associatif de droite à gauche, l’expression a += b += c signifie a += (b += c). C’est-à-dire que la valeur de b += c (qui est la valeur de b après l’addition) est ajoutée à a.

Tableau A – Priorité de l’opérateur

OpérateurAssociativité
[] . () (appel de méthode)De gauche à droite
! ~ ++ — + (unaire) – (unaire) () (cast) newDe droite à gauche
* / %De gauche à droite
+ –De gauche à droite
<< >> >>>De gauche à droite
< <= > >= instanceofDe gauche à droite
== !=De gauche à droite
&De gauche à droite
^De gauche à droite
|De gauche à droite
&&De gauche à droite
||De gauche à droite
?:De droite à gauche
= += -= *= /= %= &= |= ^= <<= >>= >>>=De droite à gauche

Contrairement à C ou C++, Java n’a pas d’opérateur de virgule. Cependant, vous pouvez utiliser une liste d’expressions séparées par des virgules dans la première et la troisième partie d’une instruction for.

LAISSER UN COMMENTAIRE

Please enter your comment!
Please enter your name here