3. Opérateurs, ordre d'exécution

3.1. Ordre d'exécution

Les opérateurs et l’ordre d’exécution des calculs ont été spécifiés très précisément en Java, à la différence d’autres langages. Un exemple souvent donné est le suivant :
 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 :
  • L’opérande de gauche est calculé avant celui de droite dans le cas d’une opération binaire. C’est le cas même pour l’opérateur =.
  • S’il y a un élément de tableau, les éléments entre crochets ( []) sont calculés en premier.
  • Lors d’un appel de méthode du type : 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.
  • Lors de l’allocation d’un tableau à plusieurs dimensions, les expressions sont calculées une par une, de gauche à droite.

3.2. Les opérateurs

Voyons maintenant l’ensemble des opérateurs Java et leur ordre de priorité. Plus l’indice de priorité est élevé, plus l’opérateur est prioritaire.

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

La signification de l’ordre de priorité est classique. La multiplication a une priorité plus forte que l’addition, donc 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.

3.3. Les opérateurs ++ et --

Ces deux opérateurs sont appelés respectivement post et préincrémentation ou post et prédécrémentation. L’incrémentation ou la décrémentation se fait avant toute chose, ou après toute chose suivant le cas. Considérons l’exemple suivant :
 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.

3.4. Les opérateurs % et /

Le signe / 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

3.5. Les opérateurs <<, >> et >>>

Les opérateurs << 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é.

3.6. L'opérateur instanceof

L’opérateur 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 :
  • Le type déclaré est une interface, et l’on teste si l’objet appartient à une classe qui implémente cette interface.
  • Le type déclaré est une interface ou une classe, et l’on teste si l’objet appartient à une interface ou une classe qui étend cette interface ou classe.
Nous verrons en détails ce que sont les interfaces dans la suite. Lorsqu'il n'y a pas d'ambiguïté sur le type que l'on teste, le compilateur génère une erreur. Notons que la classe 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.

3.7. Les opérateurs &, | et ^

Ces opérateurs correspondent respectivement au AND, OR et XOR logiques. Ils peuvent opérer sur des types booléen, ou entier, auquel cas ils fonctionnent en bit à bit.

3.8. Les opérateurs && et ||

Ces deux opérateurs correspondent respectivement au AND et OR logiques. Ils ne peuvent opérer que sur des booléens.

3.9. L'opérateur ? ... :

Cet opérateur est aussi appelé "opérateur ternaire". Sa syntaxe d'utilisation est la suivante :
 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() ;  
}

Notons que les spécifications du langage nous précisent que si les deux possibilités de l'opérateur ternaire sont des expressions, seule celle qui est utilisée est évaluée.

3.10. Les opérateurs d'affectation

Les opérateurs d’affectation sont des notations qui permettent d’alléger le code. Les deux expressions suivantes sont strictement équivalentes :
 int a = 1 ; 
 a = a + 4 ; // ces deux lignes sont équivalentes
 a += 4 ;
Java langage & API
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
Introduction : un peu d'histoire
1. Java : genèse d'un nouveau langage
Programmer en Java
1. Un premier exemple
1.1. Aperçu général, cycle de vie
1.2. Un premier programme
1.3. Programmes, applets, servlets, etc...
2. Une première classe
2.1. Écriture d'une classe
2.2. Instanciation de cette classe
3. Types de base, classes et objets
3.1. Types de base
3.2. Classes et objets
Classes Object et String
1. Introduction
2. La classe Object
2.1. La méthode toString()
2.2. La méthode clone()
2.3. La méthode equals()
2.4. La méthode hashCode()
2.5. La méthode finalize()
2.6. La méthode getClass()
3. La classe String
3.1. Introduction
3.2. Construction d'un objet de type String
3.3. Concaténation, StringBuffer et StringBuilder
3.4. Concaténations de chaînes de caractères depuis Java 8
3.5. Concaténations de chaînes de caractères depuis Java 11
3.6. Extraction d'une sous-chaîne de caractères
3.7. Comparaison de deux chaînes de caractères
3.8. Méthodes de comparaisons lexicographiques
3.9. Méthode de recherche de caractères
3.10. Méthode de modification de chaîne
3.11. Méthode de duplication
3.12. Support de l'unicode, internationalisation
Structure d'une classe
1. Introduction
2. Classes
2.1. Classes publiques
2.2. Classes internes
2.3. Classe membre
2.4. Classes locales
2.5. Classes anonymes
2.6. Le mot-clé this
3. Éléments statiques
3.1. Champ statique
3.2. Cas des constantes
3.3. Bloc statique
3.4. Classe membre statique
4. Membres d'une classe, visibilité
4.1. Bloc non statique
4.2. Accès à un membre, visibilité
4.3. Les champs
4.4. Signature d'une méthode
4.5. Les méthodes
4.6. Getters et Setters
5. Constructeur, instanciation
5.1. Chargement d'une classe
5.2. Constructeurs d'une classe
5.3. Instanciation d'un objet
5.4. Destruction d'objets
5.5. Le mot-clé final
6. Énumérations
6.1. Déclaration d'une énumération
6.2. Classe énumération
6.3. Méthode toString()
6.4. Méthode valueOf()
6.5. Méthode values()
6.6. Méthode ordinal()
6.7. Méthode compareTo()
6.8. Constructeurs privés
6.9. Classe utilitaire : EnumSet
Noms, opérateurs, tableaux
1. Introduction
2. Identificateurs, noms et expressions
2.1. Identificateurs et noms
2.2. Expressions
3. Opérateurs, ordre d'exécution
3.1. Ordre d'exécution
3.2. Les opérateurs
3.3. Les opérateurs ++ et --
3.4. Les opérateurs % et /
3.5. Les opérateurs <<, >> et >>>
3.6. L'opérateur instanceof
3.7. Les opérateurs &, | et ^
3.8. Les opérateurs && et ||
3.9. L'opérateur ? ... :
3.10. Les opérateurs d'affectation
4. Tableaux
4.1. Création d'un tableau
4.2. Initialisation d'un tableau
4.3. Utilisation d'un tableau comme un Object
4.4. Tableaux de tableaux
4.5. Copie de tableaux
5. Blocs, boucles et contrôles
5.1. Blocs
5.2. Mots-clés réservés
5.3. Tests : if et switch
5.4. Boucles : for, while, do ... while
5.5. Commandes continue et break
5.6. Commandes return et goto
Nombres, précision, calculs
1. Introduction
2. Calculs
2.1. Précision
2.2. Codage des nombres flottants
2.3. Le mot-clé strictfp
2.4. Conversion de types
3. Dépassements de capacité
3.1. Cas des entiers
3.2. Cas des flottants
3.3. Bibliothèques BigInteger et BigDecimal
4. Fonctions mathématiques
4.1. Fonctions usuelles
4.2. Générateurs aléatoires
5. Classes enveloppe
5.1. Associer les types de base à des objets
5.2. Auto-boxing
Héritage, abstraction, interfaces
1. Introduction
2. Abstraction et encapsulation
2.1. Abstraction
2.2. Encapsulation
3. Héritage
3.1. Définition de l'héritage
3.2. Conséquences pour les membres
3.3. Polymorphisme
3.4. Empêcher l'héritage
4. Classes abstraites
5. Interfaces
5.1. Introduction
5.2. Définition
5.3. Java 8 et les interfaces
5.4. Utilisation des interfaces
5.5. Définition de constantes dans les interfaces
5.6. Utilité des interfaces
Packages
1. Introduction
2. Notion de paquet
2.1. Déclaration d’appartenance à un paquet
2.2. Chargement d’une classe
2.3. Choix de nom
3. Archives, chemin de recherche, classpath
3.1. Archives
3.2. Variable CLASSPATH
3.3. Notion de classloader
3.4. Bilan sur les classes chargées
3.5. Visibilité
3.6. Conseils d'écriture, bibliothèque standard
Exceptions
1. Introduction
2. Erreurs et Exceptions
2.1. Classe Throwable, notion de stack trace
2.2. Classe Error
2.3. Classe RuntimException
2.4. Classe Exception
3. Déclenchement d'une exception
3.1. Exceptions déclenchées par la JVM
3.2. Exceptions déclenchées par l'application
4. Capter une exception
4.1. Traiter une exception localement
4.2. Code de captage
5. Créer ses propres exceptions
Entrées / sorties
1. Introduction
2. Notion de fichier
2.1. Introduction
2.2. La classe File
2.3. Construction d'une instance de File
2.4. Méthodes exposées
3. Flux de sortie
3.1. Introduction, notion de flux
3.2. Écriture de caractères, classe Writer
3.3. Bufferisation, construction d'un flux sur un autre
3.4. Utilisation de PrintWriter
3.5. Écriture d'octets, OuputStream
3.6. Écriture de types primitifs : DataOutputStream
3.7. Écriture d'objets : ObjectOutputStream
4. Flux d'entrée
4.1. Introduction
4.2. Lecture de caractères, classe Reader
4.3. Bufferisation, lecture ligne par ligne
4.4. Lecture d'octets : InputStream
4.5. Lecture de types primitifs : DataInputStream
4.6. Lecture d'objets : ObjectInputStream
5. Lecture et écriture de flux croisés
5.1. Introduction
5.2. Lire des caractères sur un flux binaire : classe InputStreamReader
5.3. Écrire des caractères sur un flux binaire : classe OutputStreamWriter
5.4. Remarque sur les jeux de caractères
6. Serialization d'objets
6.1. Enjeu de la sérialization d'objets
6.2. Serialization d'un objet
6.3. Sérialization d'une grappe d'objets
6.4. Première surcharge : méthode writeObject() readObject()
6.5. Deuxième surcharge : utilisation d'un externalizer
6.6. Troisième surcharge : utilisation d'un objet proxy
7. Flux compressés
7.1. Introduction
7.2. Flux de type gzip
7.3. Flux de type zip