Penser en Java 2nde édition | - | Sommaire | Préface | Avant-propos | Chapitre : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Annexe : A B C D | Tables des matières | - | Thinking in Java |
Cet opérateur est inhabituel parce qu'il a trois opérandes. C'est un véritable opérateur dans la mesure où il produit une valeur, à l'inverse de l'instruction habituelle if-else que nous étudierons dans la prochaine section de ce chapitre. L'expression est de la forme :
Si le résultat de expression-booléenne est true, l'expression valeur0 est évaluée et son résultat devient le résultat de l'opérateur. Si expression-booléenne est false, c'est l'expression valeur1 qui est évaluée et son résultat devient le résultat de l'opérateur.
Bien entendu, il est possible d'utiliser à la place une instruction if-else (qui sera décrite plus loin), mais l'opérateur ternaire est plus concis. Bien que C (d'où est issu cet opérateur) s'enorgueillit d'être lui-même un langage concis, et que l'opérateur ternaire ait été introduit, entre autres choses, pour des raisons d'efficacité, il faut se garder de l'utiliser à tout bout de champ car on aboutit très facilement à un code illisible.
Cet opérateur conditionnel peut être utilisé, soit pour ses effets de bord, soit pour la valeur produite, mais en général on recherche la valeur puisque c'est elle qui rend cet opérateur distinct du if-else. En voici un exemple :
Ce code est plus compact que celui qu'on aurait écrit sans l'opérateur ternaire :
La deuxième forme est plus compréhensible, et ne nécessite pas de commentaire. Il est donc nécessaire de bien peser tous les arguments avant d'opter pour l'opérateur ternaire.
La virgule est utilisée en C et C++ non seulement comme séparateur dans la liste des argument des fonctions, mais aussi en tant qu'opérateur pour une évaluation séquentielle. L'opérateur virgule est utilisé en Java uniquement dans les boucles for, qui seront étudiées plus loin dans ce chapitre.
Un des opérateurs a une utilisation spéciale en Java : l'opérateur + peut être utilisé pour concaténer des chaînes de caractères, comme on l'a déjà vu. Il semble que ce soit une utilisation naturelle de l'opérateur +, même si cela ne correspond pas à son utilisation traditionnelle. Étendre cette possibilité semblait une bonne idée en C++, aussi la surcharge d'opérateurs fut ajoutée au C++ afin de permettre au programmeur d'ajouter des significations différentes à presque tous les opérateurs. En fait, la surcharge d'opérateurs, combinée à d'autres restrictions du C++, s'est trouvée être très compliquée à mettre en oeuvre par les programmeurs pour la conception de leurs classes. En Java, la surcharge d'opérateurs aurait été plus simple à implémenter qu'elle ne l'a été en C++ ; mais cette fonctionnalité a été jugée trop complexe, et les programmeurs Java, à la différence des programmeurs C++, ne peuvent implémenter leurs propres surcharges d'opérateurs.
L'utilisation de l'opérateur + pour les String présente quelques caractéristiques intéressantes. Si une expression commence par une String, alors tous les opérandes qui suivent doivent être des String (souvenez-vous que le compilateur remplace une séquence de caractères entre guillemets par une String) :
Ici, le compilateur Java convertit x, y, et z dans leurs représentations String au lieu d'ajouter d'abord leurs valeurs. Et si on écrit :
Java remplacera x par une String.
L'un des pièges dûs aux opérateurs est de vouloir se passer des parenthèses alors qu'on n'est pas tout à fait certain de la manière dont sera évaluée l'opération. Ceci reste vrai en Java.
Une erreur très classique en C et C++ ressemble à celle-ci :
Le programmeur voulait tester l'équivalence (==) et non effectuer une affectation. En C et C++ le résultat de cette affectation est toujours true si y est différent de zéro, et on a toutes les chances de partir dans une boucle infinie. En Java, le résultat de cette expression n'est pas un boolean ; le compilateur attendant un boolean, et ne transtypant pas l'int, générera une erreur de compilation avant même l'exécution du programme. Par suite cette erreur n'apparaîtra jamais en Java. Il n'y aura aucune erreur de compilation que dans le seul cas où x et y sont des boolean, pour lequel x = y est une expression légale ; mais il s'agirait probablement d'une erreur dans l'exemple ci-dessus.
Un problème similaire en C et C++ consiste à utiliser les AND et OR bit à bit au lieu de leurs versions logiques. Les AND et OR bit à bit utilisent un des caractères (& ou |) alors que les AND et OR logique en utilisent deux (&& et ||). Tout comme avec = et ==, il est facile de ne frapper qu'un caractère au lieu de deux. En Java, le compilateur interdit cela et ne vous laissera pas utiliser cavalièrement un type là où il n'a pas lieu d'être.
Le mot transtypage est utilisé dans le sens de « couler dans un moule ». Java transforme automatiquement un type de données dans un autre lorsqu'il le faut. Par exemple, si on affecte une valeur entière à une variable en virgule flottante, le compilateur convertira automatiquement l'int en float. Le transtypage permet d'effectuer cette conversion explicitement, ou bien de la forcer lorsqu'elle ne serait pas effectuée implicitement.
Pour effectuer un transtypage, il suffit de mettre le type de données voulu (ainsi que tous ses modificateurs) entre parenthèses à gauche de n'importe quelle valeur. Voici un exemple :
Comme on peut le voir, il est possible de transtyper une valeur numérique aussi bien qu'une variable. Toutefois, dans les deux exemples présentés ici, le transtypage est superflu puisque le compilateur promouvra une valeur int en long si nécessaire. Il est tout de même possible d'effectuer un tel transtypage, soit pour le souligner, soit pour rendre le code plus clair. Dans d'autres situations, un transtypage pourrait être utilisé afin d'obtenir un code compilable.
En C et C++, le transtypage est parfois la source de quelques migraines. En Java, le transtypage est sûr, avec l'exception suivante : lorsqu'on fait ce qu'on appelle une conversion rétrécissante (c'est à dire lorsqu'on transtype depuis un type de données vers un autre, le premier pouvant contenir plus d'information que le second) on court le risque de perdre de l'information. Dans ce cas le compilateur demande un transtypage explicite, en émettant un message à peu près formulé ainsi « ceci peut être dangereux - néanmoins, si c'est ce que vous voulez vraiment faire, je vous en laisse la responsabilité ». Avec une conversion élargissante, le transtypage explicite n'est pas obligatoire car il n'y a pas de risque de perte d'information, le nouveau type pouvant contenir plus d'information que l'ancien.
Java permet de transtyper n'importe quel type primitif vers n'importe quel autre type primitif, excepté le type boolean, pour lequel il n'existe aucun transtypage. Les types Class ne peuvent être transtypés. Pour convertir une classe en une autre il faut utiliser des méthodes spéciales. (String est un cas à part, et on verra plus loin dans ce livre que les objets peuvent être transtypés à l'intérieur d'une famille de types ; un Chêne peut être transtypé en Arbre et vice versa, mais non dans un type étranger tel que Roche).
Habituellement le compilateur sait exactement quel type affecter aux valeurs littérales insérées dans un programme. Quelquefois, cependant, le type est ambigu. Dans de tels cas il faut guider le compilateur en ajoutant une information supplémentaire sous la forme de caractères associés à la valeur littérale. Le code suivant montre l'utilisation de ces caractères :
L'hexadécimal (base 16), utilisable avec tous les types entier, est représenté par 0x ou 0X suivi de caractères 0-9 et/ou a-f en majuscules ou en minuscules. Si on tente d'initialiser une variable avec une valeur plus grande que celle qu'elle peut contenir (indépendamment de la forme numérique de la valeur), le compilateur émettra un message d'erreur. Le code ci-dessus montre entre autres la valeur hexadécimale maximale possible pour les types char, byte, et short. Si on dépasse leur valeur maximale, le compilateur crée automatiquement une valeur int et émet un message nous demandant d'utiliser un transtypage rétrécissant afin de réaliser l'affectation : Java nous avertit lorsqu'on franchit la ligne.
L'octal (base 8) est représenté par un nombre dont le premier digit est 0 (zéro) et les autres 0-7. Il n'existe pas de représentation littérale des nombres binaires en C, C++ ou Java.
Un caractère suivant immédiatement une valeur littérale établit son type : L, majuscule ou minuscule, signifie long ; F, majuscule ou minuscule, signifie float, et D, majuscule ou minuscule, double.
Les exposants utilisent une notation qui m'a toujours passablement consterné : 1.39 e-47f. En science et en ingénierie, « e » représente la base des logarithmes naturels, approximativement 2.718 (une valeur double plus précise existe en Java, c'est Math.E). e est utilisé dans les expressions d'exponentiation comme 1.39 x e-47, qui signifie 1.39 x 2.718-47. Toutefois, lorsque FORTRAN vit le jour il fut décidé que e signifierait naturellement « dix puissance », décision bizarre puisque FORTRAN a été conçu pour résoudre des problèmes scientifiques et d'ingénierie, et on aurait pu penser que ses concepteurs auraient fait preuve de plus de bons sens avant d'introduire une telle ambiguïté.[25] Quoi qu'il en soit, cette habitude a continué avec C, C++ et maintenant Java. Ceux d'entre vous qui ont utilisé e en tant que base des logarithmes naturels doivent effectuer une translation mentale en rencontrant une expression telle que 1.39 e-47f en Java ; elle signifie 1.39 x 10-47.
Noter que le caractère de fin n'est pas obligatoire lorsque le compilateur est capable de trouver le type approprié. Avec :
il n'y a pas d'ambiguïté, un L suivant le 200 serait superflu. Toutefois, avec :
le compilateur traite normalement les nombres en notation scientifique en tant que double, et en l'absence du f final générerait un message d'erreur disant qu'on doit effectuer un transtypage explicite afin de convertir un double en float.
Lorsqu'on effectue une opération mathématique ou bit à bit sur des types de données primitifs plus petits qu'un int (c'est à dire char, byte, ou short), on découvre que ces valeurs sont promues en int avant que les opérations ne soient effectuées, et que le résultat est du type int. Par suite, si on affecte ce résultat à une variable d'un type plus petit, il faut effectuer un transtypage explicite qui peut d'ailleurs entraîner une perte d'information. En général, dans une expression, la donnée du type le plus grand est celle qui détermine le type du résultat de cette expression ; en multipliant un float et un double, le résultat sera un double ; en ajoutant un int et un long, le résultat sera un long.
En C and C++, l'opérateur sizeof( ) satisfait un besoin spécifique : il renseigne sur le nombre d'octets alloués pour les données individuelles. En C et C++, la principale raison d'être de l'opérateur sizeof( ) est la portabilité. Plusieurs types de données peuvent avoir des tailles différentes sur des machines différentes, et le programmeur doit connaître la taille allouée pour ces types lorsqu'il effectue des opérations sensibles à la taille des données. Par exemple, un ordinateur peut traiter les entiers sur 32 bits, alors qu'un autre les traitera sur 16 bits : les programmes peuvent ranger de plus grandes valeurs sur la première machine. Comme on peut l'imaginer, la portabilité est un énorme casse-tête pour les programmeurs C et C++.
Java n'a pas besoin d'un opérateur sizeof( ) car tous les types de données ont la même taille sur toutes les machines. Il n'est absolument pas besoin de parler de portabilité à ce niveau - celle-ci est déjà intégrée au langage.
Lors d'un séminaire, entendant mes jérémiades au sujet de la difficulté de se souvenir de la priorité des opérateurs, un étudiant suggéra un procédé mnémotechnique qui est également un commentaire : « Ulcer Addicts Really Like C A lot ». (« Les Accros de l'Ulcère Adorent Réellement C »)
Mnémonique | Type d'opérateur | Opérateurs |
Ulcer | Unaire | + - ++-- |
Addicts | Arithmétique (et décalage) | * / % + - << >> |
Really | Relationnel | > < >= <= == != |
Like | Logique (et bit à bit) | && || & | ^ |
C | Conditionnel (ternaire) | A > B ? X : Y |
A Lot | Affectation | = (et affectation composée comme*=) |
Bien entendu, ce n'est pas un moyen mnémotechnique parfait puisque les opérateurs de décalage et les opérateurs bit à bit sont quelque peu éparpillés dans le tableau, mais il fonctionne pour les autres opérateurs.
L'exemple suivant montre les types de données primitifs qu'on peut associer avec certains opérateurs. Il s'agit du même exemple de base répété plusieurs fois, en utilisant des types de données primitifs différents. Le fichier ne doit pas générer d'erreur de compilation dans la mesure où les lignes pouvant générer de telles erreurs ont été mises en commentaires avec //! :