int j = i + tab[i] + fonction() ;Le langage C ne spécifie pas dans quel ordre les opérations sont effectuées. Supposons que l'on se trouve dans une situation où
fonction()
modifie l’espace mémoire occupé par
tab
. Suivant que
fonction()
est appelé en début ou en fin de calcul, le résultat de l’opération pourra donc être différent. Ce type de problème se présente fréquemment sur des machines multiprocesseurs ou multicoeurs, où le compilateur peut effectuer des calculs en parallèle.
On peut retrouver d'autres problèmes dans ce type d'expression :
float a = (b + c)/d ;Dans ce calcul, toutes les variables sont de type
float
. En C rien ne spécifie l'ordre dans lequel les opérations doivent s'effectuer. Un compilateur pourra donc calculer
(b + c)/d
comme indiqué, alors qu'un autre calculera
(b/d) + (c/d)
. En théorie les deux résultats sont égaux, et ils le sont effectivement à peu de choses près. En toute rigueur, il existe une différence pouillesque entre les deux résultats. Et comme chacun sait, ce sont les différences pouillesques qui forment les bugs les plus jolis à corriger !
Java lève ces ambiguïtés en précisant que le résultat du calcul doit être celui qui correspond à une exécution de gauche à droite des expressions. Le ‘comme si’ permet en fait aux programmeurs de compilateur de mener toutes les optimisations qu’ils jugent utiles. L’ordre dans lequel doivent se faire les calculs n’est pas imposé, en revanche, le résultat l’est.
Les spécifications nous disent :
=
.
[]
) sont calculés en premier.
objet.methode(argument)
,
objet
est calculé en premier, puis
methode
, puis
argument
. Ce point est utilisé lorsque les mécanismes d’héritage sont utilisés.
Tableau 3. Symboles
Symbole | Note | Priorité | Associativité |
---|---|---|---|
++a --a
|
Préincrémentation, prédécrémentation | 16 | Droite à gauche |
a++ a--
|
Postincrémentation, postdécrémentation | 15 | Gauche à droite |
~
|
Inversion des bits d’un entier | 14 | Droite à gauche |
!
|
Non logique pour un booléen | 14 | Droite à gauche |
- +
|
Moins et plus unaire | 14 | Droite à gauche |
(type)
|
Conversion de type (cast) | 13 | Droite à gauche |
* / %
|
Opérations multiplicatives | 12 | Gauche à droite |
- +
|
Opérations additives | 11 | Gauche à droite |
<< >> >>>
|
Décalage de bits, à gauche et à droite | 10 | Gauche à droite |
instanceof < <= > >=
|
Opérateurs relationnels | 9 | Gauche à droite |
== !=
|
Opérateurs d’égalité | 8 | Gauche à droite |
&
|
Et logique bit à bit | 7 | Gauche à droite |
^
|
Ou exclusif logique bit à bit | 6 | Gauche à droite |
|
|
Ou inclusif logique bit à bit | 5 | Gauche à droite |
&&
|
Et conditionnel | 4 | Gauche à droite |
||
|
Ou conditionnel | 3 | Gauche à droite |
? :
|
Opérateur conditionnel | 2 | Droite à gauche |
= *= /= %= += -= <<= >>= >>>= &= ^= |=
|
Opérateurs d’affectation | 1 | Droite à gauche |
2 + 3*4
vaudra bien 14 et non pas 20 si l’addition avait été calculée la première.
La règle d’associativité permet de lever les ambiguïtés lorsque l’on se trouve en présence d’opérateurs de même priorité. Par exemple :
5*3 % 5
. Si la multiplication est calculée la première, le résultat vaut 0, sinon il vaut 15. L’associativité pour ces opérations est ‘de gauche à droite’, on commence donc par calculer la multiplication, puis le modulo.
tab1 [++i] = 0 ; tab2 [j--] = 1 ;Dans le premier cas,
i
est incrémenté, puis il est utilisé comme indice de
tab1
. Dans le second cas,
j
est décrémenté après avoir été utilisé comme indice de
tab2
.
/
est utilisé pour les deux divisions : entière et flottante. Dans le cas de la division entière, le résultat est le quotient de la division. Ainsi, en entier, 7/3 vaut 2.
L’opérateur
%
(modulo) donne le reste de la division entière du numérateur par le dénominateur. Ainsi, 7%3 vaut 1.
On a toujours l’égalité suivante :
(x / y)*y + x % y = x
<<
et
>>
fonctionnent sur des
int
, et réalisent un décalage à gauche ou à droite du nombre de bits indiqué. Dans l’exemple suivant, la valeur finale de
i
est 2.
int i = 8 >> 2 ;Le décalage de bits à droite revient à diviser un entier par 2, et à gauche à le multiplier par deux. Ces opérations sont souvent utilisées en tant qu'optimisation. On prendra bien garde que ces opérateurs fonctionnent sur des
int
. Si l’on veut les faire opérer sur des entiers plus courts, par exemple des
byte
, il faut utiliser un masque logique suivi d’une conversion de type :
byte b = 1 ; b = (byte) ((b & 0xFF) << 4) ;Une conversion de type vers
int
a lieu au moment du
&
logique, elle aurait de toute façon lieu au moment du
<<
. Le masquage nous garantit que le résultat, bien que codé sur 32 bits, tient bien sur 8 bits, et que la conversion de type ne tronque donc rien.
Ces deux opérateurs sont signés, c’est à dire qu’ils propagent correctement le bit de signe.
L’opérateur
>>>
est un décalage à droite non signé, ce qui signfie que le bit de signe n’est pas propagé.
instanceof
est une originalité du langage Java. Il s’utilise dans un cas bien précis : quand on veut tester si un objet appartient à une classe donnée, quand il y a effectivement ambiguïté sur cette classe.
Supposons que l’objet soit déclaré de type
A
. L’ambiguïté n’existe que dans deux cas :
Class
offre une méthode
isAssignableFrom()
qui permet de faire un test analogue sur les classes, à la différence de
instanceof
qui fonctionne sur un objet, donc une instance de classe.
Action action = ilPleut() ? ouvertureParapluie() : fermetureParapluie() ;Dans notre exemple, si le retour de la méthode
ilPleut()
vaut
true
, alors l'objet
action
, de type
Action
prend la valeur de retour de la méthode
ouvertureParapluie()
. Dans le cas contraire, c'est l'autre méthode qui est utilisée.
Formellement, cette façon d'écrire est équivalente à :
Exemple 42. Équivalence opérateur ternaire et tests normaux
Action action ; if (ilPleut()) { action = ouvertureParapluie() ; } else { action = fermetureParapluie() ; }
String
StringBuffer
et
StringBuilder