IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Thinking in Java, 3rd ed. Revision 4.0


précédentsommairesuivant

III. Contrôle du Flux de Programme

Tout comme une créature sensible, un programme doit agir sur son environnement et faire des choix durant sa vie.

En Java les objets et les données sont manipulés au moyen d'opérateurs, et les choix sont faits au moyen des instructions de contrôle d'exécution. Java a hérité de C++, c'est pourquoi beaucoup d'instructions seront familières aux programmeurs C et C++. Java a également amené quelques améliorations et aussi quelques simplifications.

Si vous avez l'impression de patauger quelque peu dans ce chapitre, voyez le CD ROM multimédia fourni avec le livre : Foundations for Java. Il contient des cours audio, des diapositives, des exercices, et des solutions, le tout spécialement conçu pour vous familiariser rapidement avec les fondamentaux de Java.

III-A. Utilisation des opérateurs Java

Un opérateur agit sur un ou plusieurs arguments pour produire une nouvelle valeur. Les arguments se présentent sous une forme différente de celle d'un appel de méthode standard, mais le résultat est le même. L'addition (+), la soustraction et le moins unaire (-), la multiplication (*), la division (/), et l'affectation (=) fonctionnent de la même manière dans tous les langages de programmation.

Tous les opérateurs produisent une valeur à partir de leurs opérandes. En outre, un opérateur peut changer la valeur de l'un de ses opérandes. C'est ce qu'on appelle un effet de bord. L'utilisation la plus fréquente des opérateurs modifiant leurs opérandes est justement de générer un effet de bord, mais dans ce cas il faut garder à l'esprit que la valeur produite est disponible tout comme si on avait utilisé l'opérateur sans chercher à utiliser son effet de bord.

Presque tous les opérateurs travaillent uniquement avec les types primitifs. Les exceptions sont '=', '==' et '!=', qui fonctionnent avec tous les objets (ce qui est parfois déroutant lorsqu'on traite des objets). De plus, la classe String admet les opérateurs '+' et '+='.

III-A-1. Priorité

La priorité des opérateurs régit la manière d'évaluer une expression comportant plusieurs opérateurs. Java a des règles spécifiques qui déterminent l'ordre d'évaluation. La règle la plus simple est que la multiplication et la division passent avant l'addition et la soustraction. Souvent les programmeurs oublient les autres règles de priorité, aussi vaut-il mieux utiliser les parenthèses afin que l'ordre d'évaluation soit explicite. Par exemple :

 
Sélectionnez
a = x + y - 2/2 + z;

a une signification différente de la même instruction dans laquelle certains termes sont groupés entre parenthèses :

 
Sélectionnez
a = x + (y - 2)/(2 + z);

III-A-2. L'affectation

L'affectation est réalisée au moyen de l'opérateur =. Elle signifie « prendre la valeur se trouvant du côté droit (souvent appelée rvalue) et la copier du côté gauche (souvent appelée lvalue). » Une rvalue représente toute constante, variable ou expression capable de produire une valeur, mais une lvalue doit être une variable distincte et nommée (autrement dit, il existe un emplacement physique pour ranger le résultat). Par exemple, on peut affecter une valeur constante à une variable :

 
Sélectionnez
a = 4;

mais on ne peut pas affecter quoi que ce soit à une valeur constante - elle ne peut pas être une lvalue (on ne peut pas écrire 4 = a; ).

L'affectation des types primitifs est très simple. Puisque les données de type primitif contiennent une valeur réelle et non une référence à un objet, en affectant une valeur à une variable de type primitif on copie le contenu d'un endroit à un autre. Par exemple, si on écrit a = b pour des types primitifs, alors le contenu de b est copié dans a. Si alors on modifie a, bien entendu b n'est pas affecté par cette modification. C'est ce qu'on rencontre généralement en programmation.

Toutefois, les choses se passent différemment lorsqu'on affecte des objets. Quand on manipule un objet, on manipule en fait sa référence, ce qui fait que lorsqu'on effectue une affectation « depuis un objet vers un autre », en réalité on copie une référence d'un endroit à un autre. En d'autres termes, si on écrit c = d pour des objets, après l'exécution c et d pointeront tous deux vers l'objet qui, à l'origine, était pointé uniquement par d. L'exemple suivant démontre cela :

 
Sélectionnez
//: c03:Assignment.java
// l'affectation avec des objets n'est pas triviale.
import com.bruceeckel.simpletest.*;

class Number {
    int i;
}

public class Assignment {
    static Test monitor = new Test();
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        n1.i = 9;
        n2.i = 47;
        System.out.println("1: n1.i: " + n1.i +
            ", n2.i: " + n2.i);
        n1 = n2;
        System.out.println("2: n1.i: " + n1.i +
            ", n2.i: " + n2.i);
        n1.i = 27;
        System.out.println("3: n1.i: " + n1.i +
            ", n2.i: " + n2.i);
        monitor.expect(new String[] {
            "1: n1.i: 9, n2.i: 47",
            "2: n1.i: 47, n2.i: 47",
            "3: n1.i: 27, n2.i: 27"
        });
    }
} ///:~

Tout d'abord, il faut remarquer que quelque chose de nouveau a été ajouté. La ligne :

 
Sélectionnez
import com.bruceeckel.simpletest.*;

permet d'importer la librairie « simpletest » qui a été créée pour tester le code de ce livre, et est décrite dans le Chapitre 15. Au début de la classe Assignment, il apparaît la ligne :

 
Sélectionnez
static Test monitor = new Test();

Cela permet de créer une instance de la classe Test, appelée monitor. Enfin, à la fin de main( ), apparaît :

 
Sélectionnez
monitor.expect(new String[] {
    "1: n1.i: 9, n2.i: 47",
    "2: n1.i: 47, n2.i: 47",
    "3: n1.i: 27, n2.i: 27"
});

C'est le résultat attendu du programme, sous forme d'un tableau de String. Quand le programme est exécuté, il ne fait pas qu'afficher le résultat en sortie, mais il le compare à ce tableau pour vérifier que ce tableau est correct. Donc, quand apparaîtra un programme dans ce livre qui utilise simpletest, il apparaîtra aussi un appel à expect( ) qui permettra de visualiser la sortie du programme. Par ce moyen, vous pourrez voir la sortie validée du programme.

La classe Number est simple, main( ) en crée deux instances (n1 et n2). La valeur i de chaque Number est initialisée différemment, puis n2 est affecté à n1, et la valeur de n1 est changée. Dans beaucoup de langages de programmation on s'attendrait à ce que n1 et n2 restent toujours indépendants, mais, du fait que l'on a affecté une référence, vous verrez la sortie dans expect( ). Changer l'objet n1 semble changer simultanément l'objet n2 ! Cela est dû au fait que n1 et n2 contiennent la même référence, référence pointant sur le même objet. (La référence d'origine initialement dans n1, qui pointait sur l'objet contenant la valeur 9, a été écrasée durant l'assignation et est réellement perdue ; l'objet initialement référencé sera ensuite supprimé par le garbage collector.)

Ce phénomène est souvent appelé aliasing et correspond à une caractéristique fondamentale de la gestion des objets en Java. Mais qu'en est-il si vous ne voulez pas d'aliasing dans ce cas ? Vous pouvez alors écrire :

 
Sélectionnez
n1.i = n2.i;

Les deux objets restent indépendants plutôt que d'en perdre un et de faire pointer n1 et n2 vers le même objet, mais on s'aperçoit très vite que manipuler les champs des objets ne donne pas un code lisible et va à l'encontre des bons principes de la conception orientée objet. C'est un sujet non trivial, je le laisserai de côté et je le traiterai dans l'Annexe A, consacrée à l'aliasing. En attendant, gardons à l'esprit que l'affectation des objets peut entraîner des surprises.

III-A-2-a. L'aliasing pendant l'appel des méthodes

L'aliasing peut également se produire en passant un objet à une méthode :

 
Sélectionnez
//: c03:PassObject.java
// Le passage d'objets à une méthode peut avoir
// un effet différent de celui qu'on espère
import com.bruceeckel.simpletest.*;

class Letter {
    char c;
}

public class PassObject {
    static Test monitor = new Test();
    static void f(Letter y) {
        y.c = 'z';
    }
    public static void main(String[] args) {
        Letter x = new Letter();
        x.c = 'a';
        System.out.println("1: x.c: " + x.c);
        f(x);
        System.out.println("2: x.c: " + x.c);
        monitor.expect(new String[] {
            "1: x.c: a",
            "2: x.c: z"
        });
    }
} ///:~

Dans beaucoup de langages de programmation, la méthode f( ) est censée faire une copie de son argument Letter y dans la zone de visibilité de la méthode. Mais, encore une fois, c'est une référence qui est passée et donc la ligne :

 
Sélectionnez
y.c = 'z';

modifie en réalité l'objet se trouvant au-dehors de f( ). C'est la sortie générée par expect( ) qui fournit ceci.

L'aliasing et ses conséquences sont un sujet complexe, toutes les réponses à ces questions seront données dans l'Annexe A, mais il vous faut dès maintenant prendre conscience de son existence afin d'en éviter les pièges.

III-A-3. Opérateurs mathématiques

Les opérateurs mathématiques de base sont les mêmes que ceux qui sont disponibles dans la plupart des langages : addition (+), soustraction (-), division (/), multiplication (*) et modulo (%, qui fournit le reste de la division entière). La division entière tronque, plutôt que d'arrondir, le résultat.

Java utilise également une notation abrégée pour effectuer une opération et une affectation en même temps. Ceci est noté par un opérateur suivi d'un signe égal, et c'est conforme à tous les opérateurs dans le langage (quand cela a un sens). Par exemple, pour ajouter 4 à la variable x et affecter le résultat à x, Utilisez : x += 4.

Cet example montre l'utilisation des opérateurs mathématiques :

 
Sélectionnez
//: c03:MathOps.java
// Montre les opérateurs mathématiques.
import com.bruceeckel.simpletest.*;
import java.util.*;

public class MathOps {
    static Test monitor = new Test();
    // Raccourci pour afficher un String et un int :
    static void printInt(String s, int i) {
        System.out.println(s + " = " + i);
    }
    // Raccourci pour afficher un String et un float :
    static void printFloat(String s, float f) {
        System.out.println(s + " = " + f);
    }
    public static void main(String[] args) {
        // Crée un générateur de nombres aléatoires,
        // initialisé avec l'heure actuelle :
        Random rand = new Random();
        int i, j, k;
        // Choisit une valeur entre 1 et 100 :
        j = rand.nextInt(100) + 1;
        k = rand.nextInt(100) + 1;
        printInt("j", j);  printInt("k", k);
        i = j + k; printInt("j + k", i);
        i = j - k; printInt("j - k", i);
        i = k / j; printInt("k / j", i);
        i = k * j; printInt("k * j", i);
        i = k % j; printInt("k % j", i);
        j %= k; printInt("j %= k", j);
        // Test des nombres en virgule flottante :
        float u,v,w;  // s'applique également aux doubles
        v = rand.nextFloat();
        w = rand.nextFloat();
        printFloat("v", v); printFloat("w", w);
        u = v + w; printFloat("v + w", u);
        u = v - w; printFloat("v - w", u);
        u = v * w; printFloat("v * w", u);
        u = v / w; printFloat("v / w", u);
        // La suite fonctionne également avec
        // char, byte, short, int, long,
        // et double:
        u += v; printFloat("u += v", u);
        u -= v; printFloat("u -= v", u);
        u *= v; printFloat("u *= v", u);
        u /= v; printFloat("u /= v", u);
        monitor.expect(new String[] {
            "%% j = -?\\d+",
            "%% k = -?\\d+",
            "%% j \\+ k = -?\\d+",
            "%% j - k = -?\\d+",
            "%% k / j = -?\\d+",
            "%% k \\* j = -?\\d+",
            "%% k % j = -?\\d+",
            "%% j %= k = -?\\d+",
            "%% v = -?\\d+\\.\\d+(E-?\\d)?",
            "%% w = -?\\d+\\.\\d+(E-?\\d)?",
            "%% v \\+ w = -?\\d+\\.\\d+(E-?\\d)??",
            "%% v - w = -?\\d+\\.\\d+(E-?\\d)??",
            "%% v \\* w = -?\\d+\\.\\d+(E-?\\d)??",
            "%% v / w = -?\\d+\\.\\d+(E-?\\d)??",
            "%% u \\+= v = -?\\d+\\.\\d+(E-?\\d)??",
            "%% u -= v = -?\\d+\\.\\d+(E-?\\d)??",
            "%% u \\*= v = -?\\d+\\.\\d+(E-?\\d)??",
            "%% u /= v = -?\\d+\\.\\d+(E-?\\d)??"
        });
    }
} ///:~

La première chose que vous verrez c'est des méthodes abrégées pour afficher : printInt( ) affiche un String suivi d'un int et printFloat( ) affiche un String suivi d'un float.

Pour générer des nombres, le programme crée d'abord un objet Random. Parce qu'aucun argument n'est fourni à la création, Java utilise l'heure actuelle comme initialisation du générateur de nombres aléatoires. Le programme génère un certain nombre de nombres aléatoires de différents types avec l'objet Random en appelant simplement les méthodes : nextInt( ) et nextFloat( ) (vous pouvez aussi appeler nextLong( ) ou nextDouble( )).

L'opérateur modulo, quand il est utilisé avec le résultat du générateur de nombres aléatoires, limite le résultat à un seuil supérieur égal à l'opérande moins 1 (99 dans ce cas).

III-A-3-a. Expressions régulières

Étant donné que des nombres aléatoires sont utilisés pour générer la sortie de ce programme, l'instruction expect( ) ne peut pas simplement montrer la sortie littérale comme elle le faisait avant, puisque la sortie variera d'une exécution à l'autre. Pour résoudre ce problème, les expressions régulières, une nouvelle fonctionnalité introduite dans le JDK 1.4 (mais une ancienne fonctionnalité dans des langages tels que Perl et Python) seront utilisées dans l'instruction expect( ). Bien que la couverture de cet outil intensivement puissant n'interviendra pas avant le chapitre 12, pour comprendre ces instructions vous aurez besoin d'une introduction aux expressions régulières. Ici, vous en apprendrez juste assez pour lire l'instruction expect( ), mais si vous voulez une description complète, regardez java.util.regex.Pattern dans la documentation téléchargeable du JDK.

Une expression régulière est un moyen de décrire des chaînes dans des termes généraux, ainsi vous pouvez dire : « Si une chaîne a ces choses dedans, alors il correspond à ce que je cherche ». Par exemple, pour dire qu'un nombre pourrait ou non être précédé d'un signe moins, vous mettez un signe moins suivi d'un point d'interrogation, comme ceci :

 
Sélectionnez
-?

Pour décrire un entier, vous dites que c'est un ou plusieurs chiffres. Dans les expressions régulières, un chiffre c'est '\d', mais dans un String Java, vous devez « échapper » le backslash en mettant un second backslash : '\\d'. Pour indiquer « une ou plusieurs de l'expression précédente » dans les expressions régulières, vous utilisez le '+'. Ainsi, pour dire « éventuellement un signe moins, suivi de un ou plusieurs chiffres, » vous écrivez :

 
Sélectionnez
-?\\d+

Que vous pouvez voir dans les premières lignes de l'instruction expect( ) du code précédent.

Une chose qui ne fait pas partie des expressions régulières c'est le '%%' (notez l'espace inséré pour la lisibilité) au début des lignes dans l'instruction expect( ). C'est un indicateur utilisé par simpletest pour indiquer que le reste de la ligne est une expression régulière. Ainsi vous ne le verrez pas dans les expressions régulières normales, seulement dans les instructions simpletest expect( ).

Tous les autres caractères qui ne sont pas des caractères spéciaux des expressions régulières sont traités comme des correspondances exactes. Aussi dans la première ligne :

 
Sélectionnez
%% j = -?\\d+

Le 'j = ' est cherché à l'identique. Toutefois, dans la troisième ligne, le '+' dans 'j + k' doit être échappé parce que c'est un caractère spécial des expressions régulières, comme l'est '*'. Le reste des lignes devrait être compréhensible depuis cette introduction. Plus tard dans le livre, quand des fonctionnalités supplémentaires des expressions régulières seront utilisées dans les instructions expect( ), elles seront expliquées.

III-A-3-b. Les opérateurs unaires moins et plus

Le moins unaire (-) et le plus unaire (+) sont les mêmes opérateurs que le moins et le plus binaires. Le compilateur devine quelle utilisation est voulue par la façon dont vous écrivez l'expression. Par exemple, l'instruction

 
Sélectionnez
x = -a;

A une signification évidente. Le compilateur est capable de comprendre :

 
Sélectionnez
x = a * -b;

mais le lecteur peut être embrouillé, aussi il est plus clair de dire :

 
Sélectionnez
x = a * (-b);

Le moins unaire inverse le signe de la donnée. Le plus unaire fournit la symétrie du moins unaire, bien qu'il n'ait aucun effet.

III-A-4. Incrémentation et décrémentation automatique

Java, à l'image du langage C, est rempli de raccourcis. Ces raccourcis peuvent faciliter le développement, mais également rendre plus facile ou difficile la lecture.

Deux des meilleurs exemples de ces raccourcis sont les opérateurs d'incrémentation et de décrémentation (souvent référencés comme opérateur d'autoincrémentation et d'autodécrémentation). L'opérateur de décrémentation est -- et veut dire « diminution d'une unité ». L'opérateur d'incrémentation est ++ et signifie « augmentation d'une unité ». Si a est un entier (int), par exemple, l'expression ++a est équivalente à a = a + 1. Les opérateurs d'incrémentation et de décrémentation ne modifient pas seulement la variable, mais également produisent la valeur en résultat.

Il y a deux versions pour chaque type d'opérateurs souvent appelés versions préfixée et postfixée. La préincrémentation signifie que l'opérateur ++ apparaît avant la variable ou l'expression, tandis que la postincrémentation signifie que l'opérateur ++ vient après la variable ou l'expression. De manière similaire, prédecrémentation signifie que l'opérateur -- apparaît avant la variable ou l'expression et postdécrémentation que l'opérateur -- apparaît après la variable ou l'expression. Pour la préincrémentation et la prédécrémentation (i.e. ++a ou --a), l'opération est exécutée et la valeur résultat est produite. Pour la postincrémentation et la postdécrémentation (i.e. a++ ou a--), la valeur est produite et ensuite l'opération est exécutée. Par exemple :

 
Sélectionnez
//: c03:AutoInc.java
// Démonstration des opérateurs ++ et --.
import com.bruceeckel.simpletest.*;

public class AutoInc {
    static Test monitor = new Test();
    public static void main(String[] args) {
        int i = 1;
        System.out.println("i : " + i);
        System.out.println("++i : " + ++i); // préincrémentation
        System.out.println("i++ : " + i++); // Post-incrémentation
        System.out.println("i : " + i);
        System.out.println("--i : " + --i); // prédécrémentation
        System.out.println("i-- : " + i--); // Post-décrémentation
        System.out.println("i : " + i);
        monitor.expect(new String[] {
            "i : 1",
            "++i : 2",
            "i++ : 2",
            "i : 3",
            "--i : 2",
            "i-- : 2",
            "i : 1"
        });
    }
} ///:~

Vous pouvez voir que pour la forme préfixée, on obtient la valeur après que l'opération ait été exécutée tandis que pour la forme postfixée, on obtient la valeur avant que l'opération soit exécutée. Ce sont les seuls opérateurs (autres que l'assignation) qui ont des effets de bords. (C'est-à-dire qu'ils changent l'opérande au lieu d'utiliser simplement sa valeur.)

L'opérateur d'incrémentation est une explication pour le nom C++, indiquant « une étape au-delà du C ». Lors d'une des premières conférences sur Java, Bill Joy (un des créateurs de Java), a déclaré que « Java=C++-- » (C plus plus moins moins), suggérant que Java est égal à C++ auquel les parties non nécessaires auraient été supprimées, constituant donc un langage plus simple. Au fil de votre lecture, vous découvrirez que beaucoup de choses sont plus simples en Java bien qu'au fond, il ne soit pas tellement plus simple que C++.

III-A-5. Opérateurs relationnels

Les opérateurs relationnels génèrent un résultat de type boolean. Ils évaluent la relation entre les valeurs des opérandes. Une expression relationnelle renvoie true si la relation est vraie et false si ce n'est pas le cas. Les opérateurs relationnels sont plus petit que (<), plus grand que (>), plus petit ou égal à (<=), plus grand ou égal à (>=), équivalent (==) et non équivalent (!=). L'équivalence et la non-équivalence fonctionnent avec tous les types de données, mais les autres opérateurs de comparaisons ne fonctionnent pas avec le type boolean.

III-A-5-a. Tester l'équivalence entre objets

Les opérateurs relationnels == et != fonctionnent aussi avec tous les objets, mais leur signification peut souvent confondre le programmeur Java débutant. Voici un exemple:

 
Sélectionnez
//: c03:Equivalence.java
import com.bruceeckel.simpletest.*;

public class Equivalence {
    static Test monitor = new Test();
    public static void main(String[] args) {
        Integer n1 = new Integer(47);
        Integer n2 = new Integer(47);
        System.out.println(n1 == n2);
        System.out.println(n1 != n2);
        monitor.expect(new String[] {
            "false",
            "true"
        });
    }
} ///:~

L'expression System.out.println(n1 == n2) affichera à l'écran le résultat de la comparaison booléenne. La sortie devrait être true et ensuite false, puisque les deux objets Integer sont les mêmes. Mais tandis que le contenu des objets est le même, les références ne le sont pas et les opérateurs == et != comparent les références des objets. Donc la sortie sera false puis true. Naturellement, ceci surprend les gens au début.

Que faire si vous souhaitez comparer l'égalité entre le contenu d'objets ? Vous devez utiliser la méthode spéciale equals( ) qui existe pour tous les objets (pas les types primitifs pour lesquels == et != fonctionnent très bien). Voici comment l'utiliser:

 
Sélectionnez
/
/: c03:EqualsMethod.java
import com.bruceeckel.simpletest.*;

public class EqualsMethod {
    static Test monitor = new Test();
    public static void main(String[] args) {
        Integer n1 = new Integer(47);
        Integer n2 = new Integer(47);
        System.out.println(n1.equals(n2));
        monitor.expect(new String[] {
            "true"
        });
    }
} ///:~

Le résultat sera true comme vous pouvez vous en douter. Mais attention, ce n'est pas si simple que ça. Si vous créez votre propre classe, comme ceci:

 
Sélectionnez
//: c03:EqualsMethod2.java
import com.bruceeckel.simpletest.*;

class Value {
    int i;
}

public class EqualsMethod2 {
    static Test monitor = new Test();
    public static void main(String[] args) {
        Value v1 = new Value();
        Value v2 = new Value();
        v1.i = v2.i = 100;
        System.out.println(v1.equals(v2));
        monitor.expect(new String[] {
            "false"
        });
    }
} ///:~

vous voici revenu à la case départ: le résultat est false. Ceci est dû au comportement par défaut de la méthode equals( ) qui compare les références. Donc tant que vous ne redéfinissez pas (override) la méthode equals( ) dans votre nouvelle classe, vous n'aurez pas le comportement désiré. Malheureusement, nous ne verrons pas la redéfinition des méthodes (overriding) avant le Chapitre 7 et la bonne manière de définir la méthode equals( ) avant le Chapitre 11, mais d'ici là, il faudra que vous gardiez à l'esprit le comportement de cette méthode.

La plupart des classes des librairies Java implémentent equals( ) afin de comparer le contenu des objets au lieu de leurs références.

III-A-6. Opérateurs logiques

Chaque opérateur logique AND (&&), OR (||) and NOT (!) produit une valeur booléenne qui peut être true ou false basée sur la relation logique des arguments. Voici un exemple illustrant les opérateurs relationnels et logiques:

 
Sélectionnez
//: c03:Bool.java
// Opérateurs relationnels et logiques.
import com.bruceeckel.simpletest.*;
import java.util.*;

public class Bool {
    static Test monitor = new Test();
    public static void main(String[] args) {
        Random rand = new Random();
        int i = rand.nextInt(100);
        int j = rand.nextInt(100);
        System.out.println("i = " + i);
        System.out.println("j = " + j);
        System.out.println("i > j is " + (i > j));
        System.out.println("i < j is " + (i < j));
        System.out.println("i >= j is " + (i >= j));
        System.out.println("i <= j is " + (i <= j));
        System.out.println("i == j is " + (i == j));
        System.out.println("i != j is " + (i != j));
        // Treating an int as a boolean is not legal Java:
//! System.out.println("i && j is " + (i && j));
//! System.out.println("i || j is " + (i || j));
//! System.out.println("!i is " + !i);
        System.out.println("(i < 10) && (j < 10) is "
            + ((i < 10) && (j < 10)) );
        System.out.println("(i < 10) || (j < 10) is "
            + ((i < 10) || (j < 10)) );
        monitor.expect(new String[] {
            "%% i = -?\\d+",
            "%% j = -?\\d+",
            "%% i > j is (true|false)",
            "%% i < j is (true|false)",
            "%% i >= j is (true|false)",
            "%% i <= j is (true|false)",
            "%% i == j is (true|false)",
            "%% i != j is (true|false)",
            "%% \\(i < 10\\) && \\(j < 10\\) is (true|false)",
            "%% \\(i < 10\\) \\|\\| \\(j < 10\\) is (true|false)"
        });
    }
} ///:~

Dans la méthode expect( ), les parenthèses servent à grouper les expressions et la barre verticale '|' a la signification du OR. Donc:

 
Sélectionnez
(true|false)

Signifie que cette partie de la chaîne de caractère peut être 'true' (vraie) ou 'false' (fausse). Du fait que ces caractères sont spéciaux dans les expressions régulières, ils doivent être échappés avec un '\\' si vous voulez qu'ils apparaissent comme des caractères ordinaires dans l'expression.

Les opérateurs AND, OR, ou NOT peuvent seulement être appliqués à des valeurs booléennes. Vous ne pouvez pas utiliser un non-booléen comme si c'était un booléen dans une expression logique comme vous pouvez le faire en C ou C++. Vous pouvez le voir en décommentant les lignes avec le marqueur //!. Les expressions résultantes produisent, cependant, des valeurs booléennes en utilisant des comparaisons relationnelles, alors qu'elles utilisent des opérations logiques dans les résultats.

Notez qu'une valeur booléenne est automatiquement convertie sous forme de texte si elle est utilisée quand une String est attendue.

Vous pouvez remplacer la définition de l'entier int du programme précédent par n'importe quel autre type primitif sauf boolean. Attention, cependant, la comparaison des nombres à virgule flottante est très stricte. Un nombre qui a une toute petite fraction de différence par rapport à un autre n'est pas égal à celui-ci. Un nombre le plus proche possible de zéro n'est toujours pas zéro.

III-A-6-a. Court circuit

Quand on travaille avec les opérateurs logiques, on rencontre un phénomène appelé « court-circuit ». Ceci signifie que l'expression sera évaluée seulement jusqu'à ce qu'elle puisse être déterminée vraie ou fausse sans ambigüité. Comme résultat, la partie restante de l'expression logique ne sera pas évaluée. Voici un exemple qui démontre le court-circuitage:

 
Sélectionnez
//: c03:ShortCircuit.java
// Démontre le comportement du court-cicuitage.
// avec les opérateurs logiques.
import com.bruceeckel.simpletest.*;

public class ShortCircuit {
    static Test monitor = new Test();
    static boolean test1(int val) {
        System.out.println("test1(" + val + ")");
        System.out.println("result: " + (val < 1));
        return val < 1;
    }
    static boolean test2(int val) {
        System.out.println("test2(" + val + ")");
        System.out.println("result: " + (val < 2));
        return val < 2;
    }
    static boolean test3(int val) {
        System.out.println("test3(" + val + ")");
        System.out.println("result: " + (val < 3));
        return val < 3;
    }
    public static void main(String[] args) {
        if(test1(0) && test2(2) && test3(2))
            System.out.println("expression is true");
        else
            System.out.println("expression is false");
        monitor.expect(new String[] {
            "test1(0)",
            "result: true",
            "test2(2)",
            "result: false",
            "expression is false"
        });
    }
} ///:~

Chaque test fait une comparaison sur les arguments et retourne vrai ou faux. Il imprime aussi les informations pour montrer ce qui est appelé. Les tests sont utilisés dans l'expression:

 
Sélectionnez
if(test1(0) && test2(2) && test3(2))

Vous pourriez naturellement penser que ces trois tests seront exécutés, mais la sortie montre autre chose. Le premier test produit une valeur vraie, donc l'évaluation de l'expression continue. Cependant, le second test produit un résultat faux. Puisque cela veut dire que le résultat de l'expression sera false, pourquoi continuer à l'évaluer? Ce serait un gaspillage de temps. La raison du court-cicuitage, en fait, est que vous pouvez gagner en performance si toutes les parties d'une expression logique ne doivent pas être évaluées.

III-A-7. Les opérateurs bit à bit

Les opérateurs bit à bit permettent de manipuler les bits individuels d'une donnée de type primitif. Les opérateurs bit à bit effectuent des opérations d'algèbre booléenne sur les bits en correspondance dans les deux arguments afin de produire un résultat.

L'origine des opérateurs bit à bit est à rechercher dans l'orientation bas niveau du langage C, ou vous deviez souvent manipuler directement le hardware et positionner les bits des registres matériels. Java a été conçu à l'origine pour être embarqué dans les décodeurs TV, ce qui explique cette orientation bas niveau. Cepdendant, vous n'utiliserez vraisemblablement pas beaucoup ces opérateurs.

L'opérateur AND (&) bit à bit retourne la valeur un si les deux bits correspondants des opérandes d'entrée sont à un ; sinon il retourne la valeur zéro. L'opérateur OR (|) bit à bit retourne la valeur un si l'un des deux bits correspondants des opérandes d'entrée est à un et retourne la valeur zéro dans le cas où les deux bits sont à zéro. L'opérateur EXCLUSIVE OR, ou XOR (^), retourne la valeur un si l'un des deux bits correspondants des opérandes est à un, mais pas les deux. L'opérateur NOT bit à bit (~ , appelé également opérateur de complément à un) est un opérateur unaire, il a un seul argument (tous les autres opérateurs bit à bit sont des opérateurs binaires), il renvoie l'opposé de l'argument - un si le bit de l'argument est à zéro, zéro si le bit est à un.

Les opérateurs bit à bit et les opérateurs logiques étant représentés par les mêmes caractères, je vous propose un procédé mnémotechnique pour vous souvenir de leur signification : les bits étant « petits », les opérateurs bit à bit comportent un seul caractère.

Les opérateurs bit à bit peuvent être combinés avec le signe = pour réaliser en une seule fois opération et affectation : &=, |= et ^= sont tous légitimes. (~ étant un opérateur unaire, il ne peut être combiné avec le signe =).

Le type boolean est traité comme une valeur binaire et il est quelque peu différent. Il est possible de réaliser des opérations AND, OR et XOR « bit à bit », mais il est interdit d'effectuer un NOT « bit à bit » (vraisemblablement pour ne pas faire de confusion avec le NOT logique). Pour le type boolean les opérateurs bit à bit ont le même effet que les opérateurs logiques, sauf qu'il n'y a pas de « court-circuit ». De plus, parmi les opérations bit à bit effectuées sur des types boolean il existe un opérateur XOR logique qui ne fait pas partie de la liste des opérateurs « logiques ». Enfin, le type boolean ne doit pas être utilisé dans les expressions de décalage décrites ci-après.

III-A-8. Les opérateurs de décalage

Les opérateurs de décalage manipulent eux aussi des bits. On ne peut les utiliser qu'avec les types primitifs entiers. L'opérateur de décalage à gauche (<<) a pour résultat la valeur de l'opérande situé à gauche de l'opérateur, décalée vers la gauche du nombre de bits spécifié à droite de l'opérateur (en insérant des zéros dans les bits de poids faible). L'opérateur signé de décalage à droite (>>) a pour résultat la valeur de l'opérande situé à gauche de l'opérateur, décalée vers la droite du nombre de bits spécifié à droite de l'opérateur. L'opérateur signé de décalage à droite >> étend le signe : si la valeur est positive, des zéros sont insérés dans les bits de poids fort ; si la valeur est négative, des uns sont insérés dans les bits de poids fort. Java comprend également un opérateur de décalage à droite non signé >>>, qui étend les zéros : quel que soit le signe, des zéros sont insérés dans les bits de poids fort. Cet opérateur n'existe pas en C ou C++.

Si on décale un char, byte, ou short, il sera promu en int avant le décalage, et le résultat sera un int. Seuls seront utilisés les cinq bits de poids faible de la valeur de décalage, afin de ne pas décaler plus que le nombre de bits dans un int. Si on opère avec un long, le résultat sera un long. Seuls les six bits de poids faible de la valeur de décalage seront utilisés, on ne peut donc décaler un long d'un nombre de bits supérieur à celui qu'il contient.

Les décalages peuvent être combinés avec le signe égal (<<=, >>= ou >>>=). La lvalue est remplacée par la lvalue décalée de la valeur rvalue. Il y a un problème, toutefois, avec le décalage à droite non signé combiné à une affectation. Son utilisation avec un byte ou un short ne donne pas un résultat correct. En réalité, l'opérande est promu en int, décalé à droite, puis tronqué comme s'il devait être affecté dans sa propre variable, et dans ce cas on obtient -1. L'exemple suivant démontre cela :

 
Sélectionnez
//: c03:URShift.java
// Test du décalage à droite non signé.
import com.bruceeckel.simpletest.*;

public class URShift {
    static Test monitor = new Test();
    public static void main(String[] args) {
        int i = -1;
        System.out.println(i >>>= 10);
        long l = -1;
        System.out.println(l >>>= 10);
        short s = -1;
        System.out.println(s >>>= 10);
        byte b = -1;
        System.out.println(b >>>= 10);
        b = -1;
        System.out.println(b>>>10);
        monitor.expect(new String[] {
            "4194303",
            "18014398509481983",
            "-1",
            "-1",
            "4194303"
        });
    }
} ///:~

Dans le dernier décalage, la valeur résultante n'est pas réaffectée à b, mais directement imprimée et dans ce cas le comportement est correct.

Voici un exemple montrant l'utilisation de tous les opérateurs travaillant sur des bits :

 
Sélectionnez
//: c03:BitManipulation.java
// Utiliser les opérateurs bit à bit.
import com.bruceeckel.simpletest.*;
import java.util.*;

public class BitManipulation {
    static Test monitor = new Test();
    public static void main(String[] args) {
        Random rand = new Random();
        int i = rand.nextInt();
        int j = rand.nextInt();
        printBinaryInt("-1", -1);
        printBinaryInt("+1", +1);
        int maxpos = 2147483647;
        printBinaryInt("maxpos", maxpos);
        int maxneg = -2147483648;
        printBinaryInt("maxneg", maxneg);
        printBinaryInt("i", i);
        printBinaryInt("~i", ~i);
        printBinaryInt("-i", -i);
        printBinaryInt("j", j);
        printBinaryInt("i & j", i & j);
        printBinaryInt("i | j", i | j);
        printBinaryInt("i ^ j", i ^ j);
        printBinaryInt("i << 5", i << 5);
        printBinaryInt("i >> 5", i >> 5);
        printBinaryInt("(~i) >> 5", (~i) >> 5);
        printBinaryInt("i >>> 5", i >>> 5);
        printBinaryInt("(~i) >>> 5", (~i) >>> 5);
        
        long l = rand.nextLong();
        long m = rand.nextLong();
        printBinaryLong("-1L", -1L);
        printBinaryLong("+1L", +1L);
        long ll = 9223372036854775807L;
        printBinaryLong("maxpos", ll);
        long lln = -9223372036854775808L;
        printBinaryLong("maxneg", lln);
        printBinaryLong("l", l);
        printBinaryLong("~l", ~l);
        printBinaryLong("-l", -l);
        printBinaryLong("m", m);
        printBinaryLong("l & m", l & m);
        printBinaryLong("l | m", l | m);
        printBinaryLong("l ^ m", l ^ m);
        printBinaryLong("l << 5", l << 5);
        printBinaryLong("l >> 5", l >> 5);
        printBinaryLong("(~l) >> 5", (~l) >> 5);
        printBinaryLong("l >>> 5", l >>> 5);
        printBinaryLong("(~l) >>> 5", (~l) >>> 5);
        monitor.expect("BitManipulation.out");
    }
    static void printBinaryInt(String s, int i) {
        System.out.println(
            s + ", int: " + i + ", binary: ");
        System.out.print("   ");
        for(int j = 31; j >= 0; j--)
            if(((1 << j) &  i) != 0)
                System.out.print("1");
            else
                System.out.print("0");
        System.out.println();
    }
    static void printBinaryLong(String s, long l) {
        System.out.println(
            s + ", long: " + l + ", binary: ");
        System.out.print("   ");
        for(int i = 63; i >= 0; i--)
            if(((1L << i) & l) != 0)
                System.out.print("1");
            else
                System.out.print("0");
        System.out.println();
    }
} ///:~

Les deux dernières méthodes, printBinaryInt( ) et printBinaryLong( ), sont appelées respectivement avec un int et un long, et l'impriment en format binaire avec une description. Nous ne parlerons pas de cette implémentation pour le moment.

Remarquez l'utilisation de System.out.print( ) au lieu de System.out.println( ). La méthode print( ) n'émet pas de retour-chariot, et permet ainsi d'imprimer une ligne en plusieurs fois.

Dans ce cas, l'instruction expect( ) prend un nom de fichier, duquel elle lit les lignes attendues (qui peuvent contenir ou non des expressions régulières). C'est utile dans des situations où la sortie est trop longue ou inappropriée pour inclure dans un livre. Les fichiers se terminant par « .out » sont inclus dans la distribution de code, téléchargeables à www.BruceEckel.com, ainsi vous pouvez ouvrir le fichier et regarder à ce que devrait être la sortie (ou simplement lancer le programme vous-même).

Cet exemple montre l'effet de tous les opérateurs bit à bit pour les int et les long, mais aussi ce qui se passe avec les valeurs minimale, maximale, +1 et -1, pour un int et pour un long. Noter que le bit de poids le plus fort représente le signe : 0 signifie positif, 1 négatif. Voici la sortie pour la partie int :

 
Sélectionnez
-1, int: -1, binary: 
    11111111111111111111111111111111
+1, int: 1, binary: 
    00000000000000000000000000000001
maxpos, int: 2147483647, binary: 
    01111111111111111111111111111111
maxneg, int: -2147483648, binary: 
    10000000000000000000000000000000
i, int: 59081716, binary: 
    00000011100001011000001111110100
~i, int: -59081717, binary: 
    11111100011110100111110000001011
-i, int: -59081716, binary: 
    11111100011110100111110000001100
j, int: 198850956, binary: 
    00001011110110100011100110001100
i & j, int: 58720644, binary: 
    00000011100000000000000110000100
i | j, int: 199212028, binary: 
    00001011110111111011101111111100
i ^ j, int: 140491384, binary: 
    00001000010111111011101001111000
i << 5, int: 1890614912, binary: 
    01110000101100000111111010000000
i >> 5, int: 1846303, binary: 
    00000000000111000010110000011111
(~i) >> 5, int: -1846304, binary: 
    11111111111000111101001111100000
i >>> 5, int: 1846303, binary: 
    00000000000111000010110000011111
(~i) >>> 5, int: 132371424, binary: 
    00000111111000111101001111100000

La représentation binaire des nombres est dite en complément à deux signé.

III-A-9. Opérateur ternaire if-else

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 :

 
Sélectionnez
expression-booléenne ? valeur0 : valeur1

Si le résultat de expression booléenne est vrai, l'expression valeur0 est évaluée et son résultat devient le résultat de l'opérateur. Si expression booléenne est fausse, 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 ordinaire if-else (qui sera décrite plus loin), mais l'opérateur ternaire est beaucoup plus concis. Bien que C (d'où est issu cet opérateur) s'enorgueillisse 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 :

 
Sélectionnez
static int ternary(int i) {
    return i < 10 ? i * 100 : i * 10;
}

On peut voir que ce code est plus compact que celui qu'on aurait écrit sans l'opérateur ternaire :

 
Sélectionnez
static int alternative(int i) {
    if (i < 10)
        return i * 100;
    else
        return i * 10;
}

La deuxième forme est plus compréhensible et n'est pas beaucoup plus longue à saisir. Il est donc nécessaire de bien peser tous les arguments avant d'opter pour l'opérateur ternaire - il est généralement justifié lorsqu'une variable peut prendre deux valeurs.

III-A-10. L'opérateur virgule

La virgule est utilisée en C et C++ non seulement comme séparateur dans la liste des arguments 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.

III-A-11. String opérateur +

Il existe en Java une utilisation spécifique d'un opéateur: l'opérateur + qui permet, comme vous avez déjà pu le constater de concaténer des chaînes de caractères. Il semble naturel d'utiliser l'opérateur+ ainsi, bien que cela ne corresponde pas à l'usage qu'on en fait traditionnellement. Cette fonctionnalité semblait être une bonne idée en C++, pour cette raison, la surcharge d'opérateur a été implémentée en C++ afin de permettre au programmeur C++ d'ajouter des comportements à presque tous les opérateurs. Malheureusement la surcharge d'opérateur combinée à certaines restrictions du C++ se revèle être une fonctionnalité compliquée pour les programmeurs lors de la modélisation de leurs classes. Bien que la surcharge d'opérateur aurait été plus facile à implémenter en Java qu'en C++, celle-ci a néanmoins été jugée trop complexe, et les programmeurs java ne peuvent donc pas créer leurs propres opérateurs surchargés comme le faisaient les programmeurs C++.

L'utilisation de String + a un comportement qui mérite qu'on s'y intéresse. Si une expression commence par une chaîne de caractères String, alors tous opérandes qui suivent doivent être de type String (souvenez-vous que le compilateur va transformer une suite de caractères entre guillemets en String) :

 
Sélectionnez
int x = 0, y = 1, z = 2;
    String sString = "x, y, z ";
    System.out.println(sString + x + y + z);

Ici, le compilateur Java convertira x, y et z en représentations String au lieu d'en faire l'addition en premier lieu et de la concaténer ensuite. Et si vous écrivez:

 
Sélectionnez
System.out.println(x + sString);

Java convertira x en String.

III-A-12. Pièges courants lors de l'utilisation d'opérateurs

Un des pièges lors de l'utilisation d'opérateurs est d'omettre les parenthèses quand il y' a un doute sur la manière d'évaluer une expression. C 'est encore le cas en Java.

Une erreur très courante en C et en C++ ressemble à cela:

 
Sélectionnez
while(x = y) {
        // ....
    }

Le programmeur souhaitait tester l'équivalence avec (==) plutôt que de réaliser une affectation. En C et en C++, le résultat de cette affectation sera toujours true si y est différent de 0, et cela va probablement générer une boucle infinie. En Java, le résultat de cette expression n'est pas un booléen, mais le compilateur attend un booléen et ne le convertira pas depuis un int, ainsi il renverra une erreur d'évaluation à la compilation et va gérer le problème avant que vous ne tentiez d'exécuter le programme. En outre, ce piège ne se produit jamais en Java. (Le seul cas où vous n'obtiendrez pas d'erreur d'évaluation à la compilation se produit quand x et y sont des booléens, dans ce cas x = y est une expression valide, mais dans l'exemple précédent c'est certainement une erreur.)

Un problème similaire en C et en C++ est l'emploi des expressions bit à bit avec AND et OR au lieu de leur emploi logique. Les expressions bit à bit AND et OR s'écrivent avec un caractère & ou | alors que dans une expression logique, ils s'écrivent avec deux caractères && ou ||. Comme pour = et ==, il est courant de taper seulement un caractère au lieu de deux. En Java, le compilateur de nouveau empêchera cela, car il ne permettra pas une utilisation erronée d'un type primitif.

III-A-13. Les opérateurs de transtypage

Le mot cast est utilisé dans le sens de « couler dans un moule ». Java transforme automatiquement un type de données en 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 faite automatiquement.

Pour effectuer un transtypage, il suffit de mettre le type de données voulu (avec tous ses modificateurs) entre parenthèses à gauche de n'importe quelle valeur. Voici un exemple :

 
Sélectionnez
void casts() {
    int i = 200;
    long l = (long)i;
    long l2 = (long)200;
}

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 automatiquement 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 essentiel afin d'obtenir un code compilable.

En C et C++, le transtypage est parfois la source de migraines. En Java, le transtypage est sûr, à l'exception près que lorsque l'on fait ce que l'on appelle une conversion rétrécissante (c'est-à-dire quand le type de données de départ contient plus d'informations que celui vers lequel il est transtypé), on court le risque de perdre de l'information. Dans ce cas le compilateur demande un transtypage explicite, en émettant le message : « ceci peut être dangereux à faire, si vous voulez le faire vous devez faire un transtypage explicite ». Avec une conversion élargissante, un transtypage explicite n'est pas nécessaire, car il n'y a pas de risque de perte d'information, le nouveau type pouvant contenir plus d'informations 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 pas être transtypés. Pour convertir une classe en une autre, il faut utiliser des méthodes spéciales. (String est un cas spécial, 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 pas en un type étranger tel que Rocher).

III-A-13-a. Les littéraux

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 :

 
Sélectionnez
//: c03:Literals.java

public class Literals {
    char c = 0xffff; // plus grande valeur du type char en hexadécimal
    byte b = 0x7f; // plus grande valeur du type byte en hexadécimal
    short s = 0x7fff; // plus grande valeur du type short en hexadécimal
    int i1 = 0x2f; // Hexadécimal (minuscules)
    int i2 = 0X2F; // Hexadécimal (majuscules)
    int i3 = 0177; // Octal (avec zéro en tête)
    // Hexadécimal et Octal peuvent aussi être utilisés pour des objets du type long.
    long n1 = 200L; // suffixe long
    long n2 = 200l; // suffixe long (mais peut être source de confusion)
    long n3 = 200;
    //! long l6(200); // non autorisé
    float f1 = 1;
    float f2 = 1F; // suffixe float
    float f3 = 1f; // suffixe float
    float f4 = 1e-45f; // 10 puissance
    float f5 = 1e+9f; // suffixe float
    double d1 = 1d; // suffixe double
    double d2 = 1D; // suffixe double
    double d3 = 47e47d; // 10 puissance
} ///:~

L'hexadécimal (base 16), utilisable avec tous les types entier, est représenté par un préfixe 0x ou 0X suivi des caractères 0-9 ou a-f en majuscules ou en minuscules. Si l'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 les valeurs hexadécimales maximales possibles pour les types char, byte et short. Si l'on dépasse ces valeurs, 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. Vous savez que vous avez franchi la ligne.

L'octal (base 8) est représenté par un nombre dont le premier chiffre est zéro et les autres entre 0 et 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 signifie 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 est disponible en Java, Math.E). Ceci est utilisé dans les expressions d'exponentiation comme 1.39 x e-47, qui signifie 1.39 x 2.718-47. Cependant, quand FORTRAN fut inventé, il fut décidé que e signifierait naturellement « dix puissance », ce qui est une décision bizarre puisque FORTRAN a été conçu pour résoudre des problèmes scientifiques et d'ingénierie. On aurait donc pu penser que ses concepteurs auraient fait preuve de plus de bon sens avant d'introduire une telle ambiguïté. (17) Quoi qu'il en soit, cette habitude a continué avec C, C++ et maintenant Java. Ainsi si vous êtes habitué à utiliser e en tant que base des logarithmes naturels, vous devez effectuer une translation mentale en rencontrant une expression telle que 1.39 e-47f en Java ; cela signifie 1.39 x 10-47.

Notez que le caractère de fin n'est pas obligatoire lorsque le compilateur est capable de trouver le type approprié. Avec :

 
Sélectionnez
long n3 = 200;

il n'y a pas d'ambiguïté, unL suivant le 200 serait donc superflu. Toutefois, avec :

 
Sélectionnez
float f4 = 1e-47f; // 10 puissance

le compilateur traite normalement les nombres en notation scientifique en tant que doubles, et en l'absence du f final, générerait un message d'erreur disant que l'on doit effectuer un transtypage explicite afin de convertir un double en float.

III-A-13-b. La promotion

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), ces valeurs sont promues en int avant que les opérations ne soient effectuées, et le résultat est du type int. Par la suite, si on affecte ce résultat à une variable d'un type plus petit, il faut effectuer un transtypage explicite. (Et, puisque la variable est assignée dans un type plus petit, il peut y avoir une perte d'informations). 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.

III-A-14. Java n'a pas d'opérateur sizeof()

En C et en C++, l'opérateur sizeof( ) (« taille de ») répond à un besoin spécifique : il indique le nombre d'octets alloués pour les données. La principale raison d'être de l'opérateur sizeof( ) en C et C++ est la portabilité. Chaque type de données peut avoir une taille différente selon la machine, et le développeur doit donc connaître la taille allouée pour les types avant d'effectuer des opérations sensibles à la taille des données. Par exemple, un ordinateur pourrait stocker les entiers sur 32bits, alors qu'un autre le ferait sur 16bits. Les programmes pourraient ainsi stocker des valeurs plus grandes dans les entiers de cette première machine. Comme vous pouvez l'imaginer, la portabilité est donc un vrai casse-tête pour les développeurs C et C++.

Java n'a pas besoin d'opérateur sizeof( ) parce que chaque type de données a la même taille sur toutes les machines. Vous n'avez ainsi pas besoin de vous soucier de la portabilité à ce niveau parce que celle-ci est déjà prévue dans le langage.

III-A-15. Retour sur la priorité des opérateurs

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

Operateurs

Ulcer

Unaire

+ - ++--

Addicts

Arithmétique (et décalage)

* / % + - << >>

Really

Relationnel

> < >= <= == !=

Like

Logique (et bit à bit)

&& || & | ^

C

Conditionnel (ternaire)

À > B ? X : Y

À 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.

III-A-16. Résumé sur les opérateurs

L'exemple suivant montre les types de données primitifs que l'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 commentaire avec //!.

 
Sélectionnez
//: c03:AllOps.java
// Test de tous les opérateurs en liaison avec tous les types de données primitifs
// afin de montrer les associations acceptées par le compilateur Java.

public class AllOps {
    // Accepter les résultats d'un test booléen :
    void f(boolean b) {}
    void boolTest(boolean x, boolean y) {
        // Opérateurs arithmétiques :
        //! x = x * y;
        //! x = x / y;
        //! x = x % y;
        //! x = x + y;
        //! x = x - y;
        //! x++;
        //! x--;
        //! x = +y;
        //! x = -y;
        // Opérateurs relationnels et logiques :
        //! f(x > y);
        //! f(x >= y);
        //! f(x < y);
        //! f(x <= y);
        f(x == y);
        f(x != y);
        f(!y);
        x = x && y;
        x = x || y;
        // Opérateurs bit à bit :
        //! x = ~y;
        x = x & y;
        x = x | y;
        x = x ^ y;
        //! x = x << 1;
        //! x = x >> 1;
        //! x = x >>> 1;
        // Affectation composée :
        //! x += y;
        //! x -= y;
        //! x *= y;
        //! x /= y;
        //! x %= y;
        //! x <<= 1;
        //! x >>= 1;
        //! x >>>= 1;
        x &= y;
        x ^= y;
        x |= y;
        // Changement de type :
        //! char c = (char)x;
        //! byte B = (byte)x;
        //! short s = (short)x;
        //! int i = (int)x;
        //! long l = (long)x;
        //! float f = (float)x;
        //! double d = (double)x;
    }
    void charTest(char x, char y) {
        // Opérateurs arithmétiques :
        x = (char)(x * y);
        x = (char)(x / y);
        x = (char)(x % y);
        x = (char)(x + y);
        x = (char)(x - y);
        x++;
        x--;
        x = (char)+y;
        x = (char)-y;
        // Opérateurs relationnels et logiques :
        f(x > y);
        f(x >= y);
        f(x < y);
        f(x <= y);
        f(x == y);
        f(x != y);
        //! f(!x);
        //! f(x && y);
        //! f(x || y);
        // Opérateurs bit à bit :
        x= (char)~y;
        x = (char)(x & y);
        x  = (char)(x | y);
        x = (char)(x ^ y);
        x = (char)(x << 1);
        x = (char)(x >> 1);
        x = (char)(x >>> 1);
        // Affectation composée :
        x += y;
        x -= y;
        x *= y;
        x /= y;
        x %= y;
        x <<= 1;
        x >>= 1;
        x >>>= 1;
        x &= y;
        x ^= y;
        x |= y;
        // Changement de type :
        //! boolean b = (boolean)x;
        byte B = (byte)x;
        short s = (short)x;
        int i = (int)x;
        long l = (long)x;
        float f = (float)x;
        double d = (double)x;
    }
    void byteTest(byte x, byte y) {
        // Opérateurs arithmétiques :
        x = (byte)(x* y);
        x = (byte)(x / y);
        x = (byte)(x % y);
        x = (byte)(x + y);
        x = (byte)(x - y);
        x++;
        x--;
        x = (byte)+ y;
        x = (byte)- y;
        // Opérateurs relationnels et logiques :
        f(x > y);
        f(x >= y);
        f(x < y);
        f(x <= y);
        f(x == y);
        f(x != y);
        //! f(!x);
        //! f(x && y);
        //! f(x || y);
        // Opérateurs bit à bit :
        x = (byte)~y;
        x = (byte)(x & y);
        x = (byte)(x | y);
        x = (byte)(x ^ y);
        x = (byte)(x << 1);
        x = (byte)(x >> 1);
        x = (byte)(x >>> 1);
        // Affectation composée :
        x += y;
        x -= y;
        x *= y;
        x /= y;
        x %= y;
        x <<= 1;
        x >>= 1;
        x >>>= 1;
        x &= y;
        x ^= y;
        x |= y;
        // Changement de type :
        //! boolean b = (boolean)x;
        char c = (char)x;
        short s = (short)x;
        int i = (int)x;
        long l = (long)x;
        float f = (float)x;
        double d = (double)x;
    }
    void shortTest(short x, short y) {
        // Opérateurs arithmétiques :
        x = (short)(x * y);
        x = (short)(x / y);
        x = (short)(x % y);
        x = (short)(x + y);
        x = (short)(x - y);
        x++;
        x--;
        x = (short)+y;
        x = (short)-y;
        // Opérateurs relationnels et logiques :
        f(x > y);
        f(x >= y);
        f(x < y);
        f(x <= y);
        f(x == y);
        f(x != y);
        //! f(!x);
        //! f(x && y);
        //! f(x || y);
        // Opérateurs bit à bit :
        x = (short)~y;
        x = (short)(x & y);
        x = (short)(x | y);
        x = (short)(x ^ y);
        x = (short)(x << 1);
        x = (short)(x >> 1);
        x = (short)(x >>> 1);
        // Affectation composée :
        x += y;
        x -= y;
        x *= y;
        x /= y;
        x %= y;
        x <<= 1;
        x >>= 1;
        x >>>= 1;
        x &= y;
        x ^= y;
        x |= y;
        // Changement de type :
        //! boolean b = (boolean)x;
        char c = (char)x;
        byte B = (byte)x;
        int i = (int)x;
        long l = (long)x;
        float f = (float)x;
        double d = (double)x;
    }
    void intTest(int x, int y) {
         // Opérateurs arithmétiques :
        x = x * y;
        x = x / y;
        x = x % y;
        x = x + y;
        x = x - y;
        x++;
        x--;
        x = +y;
        x = -y;
        // Opérateurs relationnels et logiques :
        f(x > y);
        f(x >= y);
        f(x < y);
        f(x <= y);
        f(x == y);
        f(x != y);
        //! f(!x);
        //! f(x && y);
        //! f(x || y);
        // Opérateurs bit à bit :
        x = ~y;
        x = x & y;
        x = x | y;
        x = x ^ y;
        x = x << 1;
        x = x >> 1;
        x = x >>> 1;
        // Affectation composée :
        x += y;
        x -= y;
        x *= y;
        x /= y;
        x %= y;
        x <<= 1;
        x >>= 1;
        x >>>= 1;
        x &= y;
        x ^= y;
        x |= y;
        // Changement de type :
        //! boolean b = (boolean)x;
        char c = (char)x;
        byte B = (byte)x;
        short s = (short)x;
        long l = (long)x;
        float f = (float)x;
        double d = (double)x;
    }
    void longTest(long x, long y) {
        // Opérateurs arithmétiques :
        x = x * y;
        x = x / y;
        x = x % y;
        x = x + y;
        x = x - y;
        x++;
        x--;
        x = +y;
        x = -y;
        // Opérateurs relationnels et logiques :
        f(x > y);
        f(x >= y);
        f(x < y);
        f(x <= y);
        f(x == y);
        f(x != y);
        //! f(!x);
        //! f(x && y);
        //! f(x || y);
        // Opérateurs bit à bit :
        x = ~y;
        x = x & y;
        x = x | y;
        x = x ^ y;
        x = x << 1;
        x = x >> 1;
        x = x >>> 1;
        // Affectation composée :
        x += y;
        x -= y;
        x *= y;
        x /= y;
        x %= y;
        x <<= 1;
        x >>= 1;
        x >>>= 1;
        x &= y;
        x ^= y;
        x |= y;
        // Changement de type :
        //! boolean b = (boolean)x;
        char c = (char)x;
        byte B = (byte)x;
        short s = (short)x;
        int i = (int)x;
        float f = (float)x;
        double d = (double)x;
    }
    void floatTest(float x, float y) {
        // Opérateurs arithmétiques :
        x = x * y;
        x = x / y;
        x = x % y;
        x = x + y;
        x = x - y;
        x++;
        x--;
        x = +y;
        x = -y;
        // Opérateurs relationnels et logiques :
        f(x > y);
        f(x >= y);
        f(x < y);
        f(x <= y);
        f(x == y);
        f(x != y);
        //! f(!x);
        //! f(x && y);
        //! f(x || y);
        // Opérateurs bit à bit :
        //! x = ~y;
        //! x = x & y;
        //! x = x | y;
        //! x = x ^ y;
        //! x = x << 1;
        //! x = x >> 1;
        //! x = x >>> 1;
        // Affectation composée :
        x += y;
        x -= y;
        x *= y;
        x /= y;
        x %= y;
        //! x <<= 1;
        //! x >>= 1;
        //! x >>>= 1;
        //! x &= y;
        //! x ^= y;
        //! x |= y;
        // Changement de type :
        //! boolean b = (boolean)x;
        char c = (char)x;
        byte B = (byte)x;
        short s = (short)x;
        int i = (int)x;
        long l = (long)x;
        double d = (double)x;
    }
    void doubleTest(double x, double y) {
        // Opérateurs arithmétiques :
        x = x * y;
        x = x / y;
        x = x % y;
        x = x + y;
        x = x - y;
        x++;
        x--;
        x = +y;
        x = -y;
        // Opérateurs relationnels et logiques :
        f(x > y);
        f(x >= y);
        f(x < y);
        f(x <= y);
        f(x == y);
        f(x != y);
        //! f(!x);
        //! f(x && y);
        //! f(x || y);
        // Opérateurs bit à bit :
        //! x = ~y;
        //! x = x & y;
        //! x = x | y;
        //! x = x ^ y;
        //! x = x << 1;
        //! x = x >> 1;
        //! x = x >>> 1;
        // Affectation composée :
        x += y;
        x -= y;
        x *= y;
        x /= y;
        x %= y;
        //! x <<= 1;
        //! x >>= 1;
        //! x >>>= 1;
        //! x &= y;
        //! x ^= y;
        //! x |= y;
        // Changement de type :
        //! boolean b = (boolean)x;
        char c = (char)x;
        byte B = (byte)x;
        short s = (short)x;
        int i = (int)x;
        long l = (long)x;
        float f = (float)x;
    }
} ///:~

Noter que le type boolean est assez limité. Vous pouvez lui assigner les valeurs true et false, et vous pouvez tester sa vérité ou sa fausseté, mais vous ne pouvez ni additionner des booléens ni effectuer un autre type d'opération avec eux.

Vous pouvez observer l'effet de la promotion des types char, byte, et short avec l'application d'un opérateur arithmétique sur l'un d'entre eux. Chaque opération arithmétique pour chacun de ces types produit un résultat sous forme d'int, qui doit être explicitement transtypé vers le type original (c'est-à-dire une conversion rétrécissante qui peut entraîner une perte d'information) afin de l'affecter à une variable de ce type. Avec les valeurs int, toutefois, vous n'avez pas besoin de transtyper, puisque tout ce qu'on obtient est toujours un int. N'allez cependant pas croire qu'on peut faire n'importe quoi en toute impunité. Si vous multipliez deux ints qui sont assez grands, vous provoquez un débordement du résultat. L'exemple suivant en fait la démonstration :

 
Sélectionnez
//: c03:Overflow.java
// Surprise! Java ne contrôle pas vos débordements.
import com.bruceeckel.simpletest.*;

public class Overflow {
    static Test monitor = new Test();
    public static void main(String[] args) {
        int big = 0x7fffffff; // // plus grande valeur int
        System.out.println("big = " + big);
        int bigger = big * 4;
        System.out.println("bigger = " + bigger);
        monitor.expect(new String[] {
            "big = 2147483647",
            "bigger = -4"
        });
    }
} ///:~

La compilation se déroule sans erreur ni avertissement ; il n'y a pas d'exception lors de l'exécution. Java est puissant, mais tout de même pas à ce point-là.

Les affectations composées ne nécessitent pas de transtypage pour les types char, byte, ou short, bien qu'elles entraînent des promotions qui ont le même résultat que les opérations arithmétiques directes. Cette absence de transtypage simplifie certainement le code.

Vous pouvez remarquer qu'à l'exception du type boolean, tous les types primitifs peuvent être transtypés entre eux. Il faut rester attentif à l'effet des conversions rétrécissantes en transtypant vers un type plus petit, sous peine de perdre de l'information sans en être averti.

III-B. Le contrôle d'exécution

Java utilisant toutes les instructions de contrôle de C, bien des choses seront familières au programmeur C ou C++. La plupart des langages de programmation procéduraux possèdent le même type d'instructions de contrôle, et on retrouve beaucoup de choses d'un langage à un autre. En Java, les mots clefs sont if-else, while, do-while, for, et une instruction de sélection appelée switch. Toutefois Java ne possède pas le très dénigré goto (lequel reste le moyen le plus expéditif pour traiter certains types de problèmes). Il est possible d'effectuer un saut ressemblant au goto, mais bien plus contraignant qu'un goto classique.

III-B-1. true et false

Toutes les instructions conditionnelles utilisent la vérité ou la fausseté d'une expression conditionnelle pour déterminer le chemin d'exécution. Exemple d'une expression conditionnelle : À == B. Elle utilise l'opérateur conditionnel == pour déterminer si la valeur A est équivalente à la valeur B. L'expression renvoie la valeur true ou false. Tous les opérateurs relationnels vus plus haut dans ce chapitre peuvent être utilisés pour créer une instruction conditionnelle. Il faut garder à l'esprit que Java ne permet pas d'utiliser un nombre à la place d'un boolean, même si cela est autorisé en C et C++ (pour lesquels la vérité est « différent de zéro » et la fausseté « zéro »). Pour utiliser un non-boolean dans un test boolean, tel que if(a), il faut d'abord le convertir en une valeur boolean en utilisant une expression booléenne, par exemple if(a != 0).

III-B-2. if-else

L'instruction if-else est sans doute le moyen le plus simple de contrôler le déroulement du programme. L'instruction else est optionnelle, et par suite l'instruction if possède deux formes :

 
Sélectionnez
if(expression booléenne)
    instruction

ou bien :

 
Sélectionnez
if(expression booléenne)
    instruction
else
    instruction

L'expression conditionnelle, expression booléenne, doit fournir un résultat de type boolean. Instruction désigne soit une instruction simple terminée par un point-virgule, soit une instruction composée, c'est-à-dire un groupe d'instructions simples placées entre deux accolades. Par la suite, chaque utilisation du mot « instruction » sous-entendra que l'instruction peut être simple ou composée.

Voici un exemple d'instruction if-else : la méthode test( ) détermine si une estimation est supérieure, inférieure ou équivalente à une valeur donnée :

 
Sélectionnez
//: c03:IfElse.java
import com.bruceeckel.simpletest.*;

public class IfElse {
    static Test monitor = new Test();
    static int test(int testval, int target) {
        int result = 0;
        if(testval > target)
            result = +1;
        else if(testval < target)
            result = -1;
        else
            result = 0; // Identique
        return result;
    }
    public static void main(String[] args) {
        System.out.println(test(10, 5));
        System.out.println(test(5, 10));
        System.out.println(test(5, 5));
        monitor.expect(new String[] {
            "1",
            "-1",
            "0"
        });
    }
} ///:~

Par convention, on indente le corps d'une instruction de contrôle de flux, afin que le lecteur détermine plus facilement son début et sa fin.

III-B-3. return

Le mot clef return a deux buts : il spécifie la valeur que la méthode doit retourner (si elle n'a pas un type de retour void) et provoque le renvoi immédiat de cette valeur. Voici la méthode test() ci-dessus, réécrite en utilisant cette propriété :

 
Sélectionnez
//: c03:IfElse2.java
import com.bruceeckel.simpletest.*;

public class IfElse2 {
    static Test monitor = new Test();
    static int test(int testval, int target) {
        if(testval > target)
            return +1;
        else if(testval < target)
            return -1;
        else
            return 0; // Identique
    }
    public static void main(String[] args) {
        System.out.println(test(10, 5));
        System.out.println(test(5, 10));
        System.out.println(test(5, 5));
        monitor.expect(new String[] {
            "1",
            "-1",
            "0"
        });
    }
} ///:~

Il n'y a aucun besoin de l'instruction else car cette méthode ne se poursuivra pas après l'exécution de l'instruction return.

III-B-4. Itération

Les instructions de contrôle de boucle telles que while, do-while et for sont souvent appelées instructions d'itération. Une instruction est répétée jusqu'à ce que l'expression booléenne de contrôle devienne fausse. Voici la forme d'une boucle while :

 
Sélectionnez
while(expression booléenne)
    instruction

L'expression booléenne est évaluée à l'entrée de la boucle, puis après chaque itération ultérieure d'instruction.

Voici un exemple simple qui génère des nombres aléatoires jusqu'à l'arrivée d'une condition particulière :

 
Sélectionnez
//: c03:WhileTest.java
// Démonstration de la boucle while.
import com.bruceeckel.simpletest.*;

public class WhileTest {
    static Test monitor = new Test();
    public static void main(String[] args) {
        double r = 0;
        while(r < 0.99d) {
            r = Math.random();
            System.out.println(r);
            monitor.expect(new String[] {
                "%% \\d\\.\\d+E?-?\\d*"
            }, Test.AT_LEAST);
        }
    }
} ///:~

Ce test utilise la méthode static random( ) de la bibliothèque Math, qui génère une valeur double comprise entre 0 et 1. (0 inclus, 1 exclu). L'expression conditionnelle de la boucle while signifie « continuer l'exécution de cette boucle jusqu'à ce que le nombre généré soit égal ou supérieur à 0.99 ». Le résultat est une liste de nombre dont la taille est différente à chaque exécution du programme.

Dans l'instruction expect( ) apparait le drapeau Test.AT_LEAST à la suite de la liste de chaînes attendue. L'instruction expect( ) peut contenir différents drapeaux permettant de modifier son comportement ; celui-ci exprime le fait que l'instruction expect( ) devrait au moins traiter les lignes listées, mais d'autres lignes peuvent aussi apparaître (qui seront ignorées). Ici, la signification est la suivante : « vous devriez voir au moins une valeur double ».

III-B-5. do-while

La forme de la boucle do-while est

 
Sélectionnez
do
            instruction
        while(expression booléenne);

La seule différence entre while et do-while est que l'instruction d'un do-while est toujours exécutée au moins une fois, même si l'expression évaluée est fausse la première fois. Dans un while, si la condition est fausse la première fois l'instruction n'est jamais exécutée. En pratique, do-while est moins utilisé que while.

III-B-6. for

La boucle for effectue une initialisation avant la première itération. Puis elle effectue un test conditionnel et, à la fin de chaque itération, une « instruction d'itération ». Voici la forme d'une boucle for:

 
Sélectionnez
for(instruction d'initialisation; expression booléenne; instruction d'itération)
    instruction

Chacune des expressions instruction d'initialisation, expression booléenne ou instruction d'itération peut être vide. L'expression booléenne est testée avant chaque itération, et dès qu'elle est évaluée à false, l'exécution continue à la ligne suivant l'instruction for. À la fin de chaque boucle, l'instruction d'itération est exécutée.

Les boucles for sont généralement utilisées pour les tâches impliquant un décompte :

 
Sélectionnez
//: c03:ListCharacters.java
// Demonstration de la boucle "for" listant
// tous les caractères ASCII  minuscules.
import com.bruceeckel.simpletest.*;

public class ListCharacters {
    static Test monitor = new Test();
    public static void main(String[] args) {
        for(int i = 0; i < 128; i++)
            if(Character.isLowerCase((char)i))
                System.out.println("value: " + i +
                    " character: " + (char)i);
        monitor.expect(new String[] {
            "value: 97 character: a",
            "value: 98 character: b",
            "value: 99 character: c",
            "value: 100 character: d",
            "value: 101 character: e",
            "value: 102 character: f",
            "value: 103 character: g",
            "value: 104 character: h",
            "value: 105 character: i",
            "value: 106 character: j",
            "value: 107 character: k",
            "value: 108 character: l",
            "value: 109 character: m",
            "value: 110 character: n",
            "value: 111 character: o",
            "value: 112 character: p",
            "value: 113 character: q",
            "value: 114 character: r",
            "value: 115 character: s",
            "value: 116 character: t",
            "value: 117 character: u",
            "value: 118 character: v",
            "value: 119 character: w",
            "value: 120 character: x",
            "value: 121 character: y",
            "value: 122 character: z"
        });
    }
} ///:~

Remarquons que la variable i est définie à l'endroit où elle est utilisée, à l'intérieur de l'expression de contrôle de la boucle for, plutôt qu'au début du bloc commençant à l'accolade ouvrante. La portée de i est l'expression contrôlée par la boucle for.

Ce programme utilise aussi la classe « englobante » java.lang.Character, qui n'englobe pas seulement le type primitif char en un objet, mais possède également d'autres utilités. Ici, la méthode static isLowerCase( ) est utilisée pour détecter si le caractère en question est une minuscule.

Les langages procéduraux traditionnels tels que C exigent que toutes les variables soient définies au début d'un bloc afin que le compilateur leur alloue de l'espace mémoire lorsqu'il crée un bloc. En Java et C++ les déclarations de variables peuvent apparaître n'importe où dans le bloc, et être ainsi définies au moment où on en a besoin. Ceci permet un style de code plus naturel et rend le code plus facile à comprendre.

Il est possible de définir plusieurs variables dans une instruction for, à condition qu'elles aient le même type :

 
Sélectionnez
for(int i = 0, j = 1; i < 10 && j != 11; i++, j++)
    // corps de la boucle for

La définition int de l'instruction for s'applique à la fois à i et à j. La possibilité de définir des variables dans une expression de contrôle est limitée à la boucle for. On ne peut l'utiliser dans aucune autre instruction de sélection ou d'itération.

III-B-6-a. L'opérateur virgule

Au début de ce chapitre j'ai déclaré que l'opérateur virgule (à distinguer du séparateur virgule, utilisé pour séparer les définitions et les arguments de fonctions) a seulement une utilisation en Java : à savoir dans l'expression de contrôle d'une boucle for. Aussi bien la partie initialisation que la partie itération de l'expression de contrôle peuvent être formées de plusieurs instructions séparées par des virgules, et ces instructions seront évaluées séquentiellement. L'exemple précédent utilisait cette possibilité. Voici un autre exemple :

 
Sélectionnez
//: c03:CommaOperator.java
import com.bruceeckel.simpletest.*;

public class CommaOperator {
    static Test monitor = new Test();
    public static void main(String[] args) {
        for(int i = 1, j = i + 10; i < 5;
            i++, j = i * 2) {
            System.out.println("i= " + i + " j= " + j);
        }
        monitor.expect(new String[] {
            "i= 1 j= 11",
            "i= 2 j= 4",
            "i= 3 j= 6",
            "i= 4 j= 8"
        });
    }
} ///:~

Remarquez que la partie initialisation ainsi que la partie itération sont évaluées en ordre séquentiel. La partie initialisation peut comporter n'importe quel nombre de définitions du même type.

III-B-7. break et continue

Le déroulement de toutes les instructions d'itération peut être contrôlé de l'intérieur du corps de la boucle au moyen des instructions break et continue. L'instruction break sort de la boucle sans exécuter la suite des instructions. L'instruction continue arrête l'exécution de l'itération courante, et l'exécution reprend en début de boucle avec l'itération suivante.

Ce programme montre des exemples d'utilisation des instructions break et continue dans des boucles for et while :

 
Sélectionnez
//: c03:BreakAndContinue.java
// Démonstration des mots clefs break et continue.
import com.bruceeckel.simpletest.*;

public class BreakAndContinue {
    static Test monitor = new Test();
    public static void main(String[] args) {
        for(int i = 0; i < 100; i++) {
            if(i == 74) break; // Sortie définitive de la boucle for
            if(i % 9 != 0) continue; // Continue avec l'itération suivante
            System.out.println(i);
        }
        int i = 0;
        // une "boucle infinie" :
        while(true) {
            i++;
            int j = i * 27;
            if(j == 1269) break; // Sortie de la boucle
            if(i % 10 != 0) continue; // Début de la boucle
            System.out.println(i);
        }
        monitor.expect(new String[] {
            "0",
            "9",
            "18",
            "27",
            "36",
            "45",
            "54",
            "63",
            "72",
            "10",
            "20",
            "30",
            "40"
        });
    }
} ///:~

Dans la boucle for, la valeur de i n'atteint jamais 100, car l'instruction break termine la boucle lorsque i prend la valeur 74. En principe, il ne faudrait pas utiliser break de cette manière, à moins que l'on ne connaisse pas le moment où la condition de fin arrivera. L'instruction continue provoque un branchement au début de la boucle d'itération (donc en incrémentant i) chaque fois que i n'est pas divisible par 9. Lorsqu'il l'est, la valeur est imprimée.

La seconde partie montre une « boucle infinie » qui, théoriquement, ne devrait jamais s'arrêter. Toutefois, elle contient une instruction break lui permettant de le faire. De plus, l'instruction continue retourne au début de la boucle au lieu d'exécuter la fin (ainsi la seconde boucle s'imprime que lorsque la valeur de i est divisible par 10). Dans la sortie, la valeur 0 est imprimée, car 0 % 9 a pour résultat 0.

Il existe une seconde forme de boucle infinie, c'est for(;;). Le compilateur traite while(true) et for(;;) de la même manière, le choix entre l'une et l'autre est donc affaire de goût.

III-B-7-a. L'infâme « goto »

Le mot clef goto est aussi ancien que les langages de programmation. En effet, goto a été le premier moyen de contrôle des programmes dans les langages assembleur : « si la condition A est satisfaite, alors sauter ici, sinon sauter là ». Lorsqu'on lit le code assembleur finalement généré par n'importe quel compilateur, on voit qu'il comporte beaucoup de sauts (le compilateur Java produit son propre « code assembleur », mais ce code est plutôt exécuté par la machine virtuelle Java que directement sur un CPU).

Un goto au niveau du code source est un saut, et c'est ce qui lui a donné mauvaise réputation. Un programme n'arrêtant pas de sauter d'un point à un autre ne peut-il être réorganisé afin que le flux du programme soit plus séquentiel ? goto tomba en disgrâce après la publication du fameux article « Le goto considéré comme nuisible » écrit par Edsger Dijkstra, et depuis lors les guerres de religion à propos de son utilisation sont devenues monnaie courante, les partisans du mot clef honni recherchant une nouvelle audience.

Comme il est habituel en de semblables situations, la voie du milieu est la plus indiquée. Le problème n'est pas d'utiliser le goto, mais de trop l'utiliser ; dans quelques rares situations, le goto est réellement la meilleure façon de structurer le flux de programme.

Bien que goto soit un mot réservé de Java, on ne le trouve pas dans le langage ; Java n'a pas de goto. Cependant, il existe quelque chose qui ressemble à un saut, lié aux mots clefs break et continue. Ce n'est pas vraiment un saut, mais plutôt une manière de sortir d'une instruction d'itération. On le présente dans les discussions sur le goto parce qu'il utilise le même mécanisme : une étiquette.

Une étiquette est un identificateur suivi du caractère deux points, comme ceci :

 
Sélectionnez
label1:

En Java, une étiquette ne peut se trouver qu'en un unique endroit : juste avant une instruction d'itération. Et, j'insiste, juste avant - il n'est pas bon de mettre une autre instruction entre l'étiquette et la boucle d'itération. De plus, il n'y a qu'une seule bonne raison de mettre une étiquette avant une itération, c'est lorsqu'on a l'intention d'y nicher une autre itération ou un switch. Ceci parce que les mots clefs break et continue ont pour fonction naturelle d'interrompre la boucle en cours, alors qu'utilisés avec une étiquette ils interrompent la boucle pour se brancher à l'étiquette :

 
Sélectionnez
label1: 
outer-iteration {
    inner-iteration {
        //...
        break; // 1
        //...
        continue;  // 2
        //...
        continue label1; // 3
        //...
        break label1;  // 4
    }
}

Dans le cas 1, break interrompt l'itération intérieure, on se retrouve dans l'itération extérieure. Dans le cas 2, continue branche à l'itération intérieure. Mais dans le cas 3, continue label1 interrompt l'itération intérieure et l'itération extérieure, et branche dans tous les cas à label1. En fait, l'itération continue, mais en redémarrant à partir de l'itération extérieure. Dans le cas 4, break label1 interrompt lui aussi dans tous les cas et branche à label1, mais ne rentre pas à nouveau dans l'itération. En fait il sort des deux itérations.

Voici un exemple utilisant les boucles for:

 
Sélectionnez
//: c03:LabeledFor.java
// la boucle for "étiquetée" en Java.
import com.bruceeckel.simpletest.*;

public class LabeledFor {
    static Test monitor = new Test();
    public static void main(String[] args) {
        int i = 0;
        outer: // Il ne peut y avoir d'instruction ici
        for(; true ;) { // boucle infinie
            inner: // Il ne peut y avoir d'instruction ici
            for(; i < 10; i++) {
                System.out.println("i = " + i);
                if(i == 2) {
                    System.out.println("continue");
                    continue;
                }
                if(i == 3) {
                    System.out.println("break");
                    i++; // Sinon i ne sera
                        // jamais incrémenté.
                    break;
                }
                if(i == 7) {
                    System.out.println("continue outer");
                    i++; // Sinon i ne sera
                       // jamais incrémenté.
                    continue outer;
                }
                if(i == 8) {
                    System.out.println("break outer");
                    break outer;
                }
                for(int k = 0; k < 5; k++) {
                    if(k == 3) {
                        System.out.println("continue inner");
                        continue inner;
                    }
                }
            }
        }
        // On ne peut pas utiliser un break ou un continue vers une étiquette ici
        monitor.expect(new String[] {
            "i = 0",
            "continue inner",
            "i = 1",
            "continue inner",
            "i = 2",
            "continue",
            "i = 3",
            "break",
            "i = 4",
            "continue inner",
            "i = 5",
            "continue inner",
            "i = 6",
            "continue inner",
            "i = 7",
            "continue outer",
            "i = 8",
            "break outer"
        });
    }
} ///:~

Noter que break sort de la boucle for, et l'expression d'incrémentation ne sera jamais exécutée avant le passage en fin de boucle for. Puisque break saute l'instruction d'incrémentation, l'incrémentation est réalisée directement dans le cas où i == 3. Dans le cas i == 7 , l'instruction continue outer saute elle aussi en tête de la boucle, donc saute l'incrémentation, qui est, ici aussi, réalisée directement.

Si l'instruction break outer n'était pas là, il n'y aurait aucun moyen de se brancher à l'extérieur de la boucle extérieure depuis la boucle intérieure, puisque break utilisé seul ne permet de sortir que de la boucle la plus interne (il en est de même pour continue.)

Évidemment, lorsque break a pour effet de sortir de la boucle et de la méthode, il est plus simple d'utiliser return.

Voici une démonstration des instructions étiquetées break et continue avec des boucles while :

 
Sélectionnez
//: c03:LabeledWhile.java
// La boucle while "étiquetée" en Java.
import com.bruceeckel.simpletest.*;

public class LabeledWhile {
    static Test monitor = new Test();
    public static void main(String[] args) {
        int i = 0;
        outer:
        while(true) {
            System.out.println("Outer while loop");
            while(true) {
                i++;
                System.out.println("i = " + i);
                if(i == 1) {
                    System.out.println("continue");
                    continue;
                }
                if(i == 3) {
                    System.out.println("continue outer");
                    continue outer;
                }
                if(i == 5) {
                    System.out.println("break");
                    break;
                }
                if(i == 7) {
                    System.out.println("break outer");
                    break outer;
                }
            }
        }
        monitor.expect(new String[] {
            "Outer while loop",
            "i = 1",
            "continue",
            "i = 2",
            "i = 3",
            "continue outer",
            "Outer while loop",
            "i = 4",
            "i = 5",
            "break",
            "Outer while loop",
            "i = 6",
            "i = 7",
            "break outer"
        });
    }
} ///:~

Les mêmes règles s'appliquent au while:

  • Un continue génère un saut en tête de la boucle courante et poursuit l'exécution.
  • Un continue étiqueté génère un saut à l'étiquette puis entre à nouveau dans la boucle juste après cette étiquette.
  • Un break « sort de la boucle par le bas ».
  • Un break étiqueté sort de la boucle par le bas, à la fin de la boucle repérée par l'étiquette.

Il est important de se souvenir qu'il n'y a qu'une seule raison d'utiliser une étiquette en Java, c'est lorsqu'il existe plusieurs boucles imbriquées et que l'on veut utiliser break or continue pour traverser plus d'un niveau d'itération.

Dijkstra, dans son article « Le goto considéré comme nuisible », critiquait les étiquettes, mais pas le goto lui-même. Il avait observé que le nombre de bugs semblait augmenter avec le nombre d'étiquettes d'un programme. Les étiquettes et les goto rendent difficile l'analyse statique d'un programme, car ils introduisent des cycles dans le graphe d'exécution de celui-ci. Remarquons que les étiquettes Java ne sont pas concernées par ce problème, dans la mesure où leur emplacement est contraint, et qu'elles ne peuvent pas être utilisées pour réaliser un transfert de contrôle à la demande. Il est intéressant de noter que nous nous trouvons dans un cas où une fonctionnalité du langage est rendue plus utile en en diminuant la puissance.

III-B-8. switch

On parle parfois de switch comme d'une instruction de sélection. L'instruction switch sélectionne un morceau de code parmi d'autres en se basant sur la valeur d'une expression entière. Voici sa forme :

 
Sélectionnez
switch(sélecteur-entier) {
    case valeur-entière1 : instruction; break; 
    case valeur-entière2 : instruction; break;
    case valeur-entière3 : instruction; break;
    case valeur-entière4 : instruction; break;
    case valeur-entière5 : instruction; break;
    // ...
    default: instruction;
}

sélecteur-entier est une expression qui produit une valeur entière. switch compare le résultat de sélecteur-entier avec chaque valeur entière. S'il trouve une égalité, l'instruction (simple ou composée) est exécutée. Si aucune égalité n'est trouvée, l'instruction qui suit default est exécutée.

Notons dans la définition précédente que chaque instruction case se termine par une instruction break, qui provoque un saut à la fin du corps de l'instruction switch. Ceci est la manière habituelle de construire une instruction switch, mais le break est optionnel. S'il n'est pas là, le code associé à l'instruction case suivante est exécuté, et ainsi de suite jusqu'à la rencontre d'une instruction break. Bien qu'on n'ait généralement pas besoin d'utiliser cette possibilité, elle peut se montrer très utile pour un programmeur expérimenté. Remarquons que la dernière instruction, qui suit default, n'a pas besoin de break, car en fait l'exécution continue jusqu'à l'endroit où une instruction break l'aurait amenée de toute façon. Il n'y a cependant aucun problème à terminer l'instruction default par une instruction break si on pense que le style de programmation est une question importante.

L'instruction switch est une manière propre d'implémenter une sélection multiple (c'est-à-dire sélectionner un certain nombre de possibilités d'exécution), mais elle requiert une expression de sélection dont le résultat soit entier, comme int ou char. Par exemple on ne peut pas utiliser une chaîne de caractères ou un flottant comme sélecteur dans une instruction switch. Pour les types non entiers, il faut mettre en œuvre une série d'instructions if.

Voici un exemple qui crée des lettres aléatoirement, et détermine s'il s'agit d'une voyelle ou d'une consonne :

 
Sélectionnez
//: c03:VoyellesEtConsonnes.java
// Démonstration de l'instruction switch.
import com.bruceeckel.simpletest.*;

public class VoyellesEtConsonnes {
    static Test monitor = new Test();
    public static void main(String[] args) {
        for(int i = 0; i < 100; i++) {
            char c = (char)(Math.random() * 26 + 'a');
            System.out.print(c + ": ");
            switch(c) {
                case 'a':
                case 'e':
                case 'i':
                case 'o':
                case 'u': System.out.println("voyelle");
                          break;
                case 'y':
                case 'w': System.out.println("Quelquefois une voyelle");
                          break;
                default:  System.out.println("consonne");
            }
            monitor.expect(new String[] {
                "%% [aeiou]: voyelle|[yw]: Quelquefois une voyelle|" +
                  "[^aeiouyw]: consonne"
            }, Test.AT_LEAST);
        }
    }
} ///:~

Math.random( ) générant une valeur entre 0 et 1, il faut la multiplier par la borne supérieure de l'intervalle des nombres qu'on veut produire (26 pour les lettres de l'alphabet) puis ajouter un décalage correspondant à la borne inférieure.

Bien qu'il semble que la sélection s'opère ici sur un type caractère, l'instruction switch utilise en réalité la valeur entière du caractère. Les caractères entre guillemets simples des instructions case sont également interprétés comme des entiers avant la comparaison.

Remarquons la manière d'empiler les instructions case pour affecter un même code d'exécution à plusieurs cas d'égalité. Il faut également faire attention à terminer chaque instruction par une instruction break, autrement le contrôle serait transféré à l'instruction correspondant au case suivant.

Dans l'expression régulière de l'instruction expect( ), le '|' est utilisé pour indiquer trois possibilités différentes. Les '[]' délimitent un « ensemble » de caractères dans une expression régulière, donc la première partie dit « une lettre a, e, i, o, ou u, suivie par deux-points et le mot 'voyelle'. » La seconde possibilité indique y ou w et : « Quelquefois une voyelle. » L'ensemble dans la troisième possibilité commence par un '^', qui signifie « négation d' un des caractères dans cet ensemble, » donc n'importe quel caractère autre qu'une voyelle correspondra.

III-B-8-a. Détails de calcul

L'instruction :

 
Sélectionnez
char c = (char)(Math.random() * 26 + 'a');

mérite une attention particulière. Math.random( ) a pour résultat un double, par suite la valeur 26 est convertie en double avant que la multiplication ne soit effectuée ; cette dernière a aussi pour résultat un double. Ce qui signifie que 'a' doit lui aussi être converti en double avant d'exécuter l'addition. Le résultat double est finalement transtypé en char.

Que fait exactement ce transtypage ? En d'autres termes, si on obtient une valeur de 29.7 et qu'on la transtype en char, le résultat est-il 30 ou 29 ? La réponse est dans l'exemple suivant :

 
Sélectionnez
//: c03:CastingNumbers.java
// Qu'arrive-t-il lorsqu'on transtype un flottant
// ou un double vers une valeur entière ?
import com.bruceeckel.simpletest.*;

public class CastingNumbers {
    static Test monitor = new Test();
    public static void main(String[] args) {
        double
            above = 0.7,
            below = 0.4;
        System.out.println("above: " + above);
        System.out.println("below: " + below);
        System.out.println("(int)above: " + (int)above);
        System.out.println("(int)below: " + (int)below);
        System.out.println("(char)('a' + above): " +
            (char)('a' + above));
        System.out.println("(char)('a' + below): " +
            (char)('a' + below));
        monitor.expect(new String[] {
            "above: 0.7",
            "below: 0.4",
            "(int)above: 0",
            "(int)below: 0",
            "(char)('a' + above): a",
            "(char)('a' + below): a"
        });
    }
} ///:~

La réponse est donc : le transtypage d'un type float ou double vers une valeur entière entraîne une troncature dans tous les cas.

Une seconde question à propos de Math.random( ) : cette méthode produit des nombres entre zéro et un, mais : les valeurs 0 et 1 sont-elles incluses ou exclues ? En jargon mathématique, obtient-on ]0,1[, [0,1], ]0,1] ou [0,1[ ? (les crochets « tournés vers le nombre » signifient « ce nombre est inclus », et ceux tournés vers l'extérieur « ce nombre est exclu »). À nouveau, un programme de test devrait nous donner la réponse :

 
Sélectionnez
//: c03:RandomBounds.java
// Math.random() produit-il les valeurs 0.0 et 1.0 ?
// {RunByHand}

public class RandomBounds {
    static void usage() {
        System.out.println("Usage: \n\t" +
            "RandomBounds lower\n\tRandomBounds upper");
        System.exit(1);
    }
    public static void main(String[] args) {
        if(args.length != 1) usage();
        if(args[0].equals("lower")) {
            while(Math.random() != 0.0)
                ; // Essayer encore
            System.out.println("Produced 0.0!");
        }
        else if(args[0].equals("upper")) {
            while(Math.random() != 1.0)
                ; // Essayer encore
            System.out.println("Produced 1.0!");
        }
        else
            usage();
    }
} ///:~

Pour lancer le programme, frapper en ligne de commande :

 
Sélectionnez
java RandomBounds lower

ou bien :

 
Sélectionnez
java RandomBounds upper

Dans les deux cas, nous sommes obligés d'arrêter le programme manuellement, et il apparaît donc que Math.random( ) ne produit jamais les valeurs 0.0 ou 1.0. Une telle expérience est décevante. Si vous remarquez (18) qu'il existe environ 262 fractions différentes en double précision entre 0 et 1, alors la probabilité d'atteindre expérimentalement l'une ou l'autre des valeurs pourrait dépasser la vie d'un ordinateur, voire celle de l'expérimentateur. Cela ne permet pas de montrer que 0.0 est inclus dans les résultats de Math.random( ). En réalité, en jargon mathématique, le résultat est [0,1[.

III-C. Résumé

Ce chapitre termine l'étude des fonctionnalités fondamentales que l'on retrouve dans la plupart des langages de programmation : calcul, priorité des opérateurs, transtypage, sélection et itération. Vous êtes désormais prêts à aborder le monde de la programmation orientée objet. Le prochain chapitre traite de la question importante de l'initialisation et du nettoyage des objets, et le suivant du concept essentiel consistant à cacher l'implémentation.

III-D. Exercices

Les solutions des exercices sélectionnés peuvent être trouvées dans le document électronique The Thinking in Java Annotated Solution Guide, disponible pour un faible coût à www.BruceEckel.com.

  1. Dans la section « priorité » au début de ce chapitre, il y a deux expressions. Utiliser ces expressions dans un programme qui montre qu'elles produisent des résultats différents.
  2. Utiliser les méthodes ternary( ) et alternative( ) dans un programme qui fonctionne.
  3. À partir des sections « if-else » et « return », modifier les deux méthodes test( ) de sorte que testval soit testé pour savoir s'il est dans la fourchette entre les arguments begin et end inclus.
  4. Écrire un programme qui imprime les valeurs de 1 à 100.
  5. Modifier l'exercice 4 de manière que le programme termine avec la valeur 47, en utilisant le mot clef break. Même exercice en utilisant return.
  6. Écrire une fonction prenant pour arguments deux String, utiliser les comparaisons booléennes pour comparer les deux Strings et imprimer le résultat. En plus de == et !=, tester aussi equals( ). Dans la méthode main( ), appeler la fonction avec différents objets de type String.
  7. Écrire un programme qui génère aléatoirement 25 valeurs entières (int). Pour chaque valeur, utiliser une instruction if-else pour la classer (plus grande, plus petite, ou égale) par rapport à une deuxième valeur générée aléatoirement.
  8. Modifier l'exercice 7 en englobant le code dans une boucle while « infinie ». Il devrait alors fonctionner tant qu'on ne l'interrompt pas au moyen du clavier (classiquement avec Ctrl-C).
  9. Écrire un programme utilisant deux boucles for imbriquées ainsi que l'opérateur modulo (%) pour détecter et imprimer les nombres premiers (les nombres entiers qui ne sont divisibles que par eux-mêmes et l'unité).
  10. Écrire une instruction switch qui imprime un message pour chaque case, la mettre dans une boucle for qui teste chaque case. Mettre un break après chaque case, tester, puis enlever les breaks et voir ce qui se passe.

précédentsommairesuivant
John Kirkham a écrit, « J'ai fait mes débuts en informatique en 1962 avec FORTRAN II sur un IBM 1620. À cette époque, pendant les années 60 et jusque dans les années 70, FORTRAN était un langage entièrement écrit en majuscules. Sans doute parce que beaucoup de périphériques d'entrée des débuts de l'informatique étaient de vieux téléscripteurs utilisant le code Baudot à cinq moments (à cinq bits), sans possibilité de minuscules. La lettre 'E' dans la notation scientifique était en majuscule et ne pouvait jamais être confondue avec la base 'e' des logarithmes naturels, toujours écrite en minuscule. 'E' signifiait simplement puissance, et, naturellement, puissance de la base de numération habituellement utilisée, c'est-à-dire puissance de 10. À cette époque également, l'octal était largement utilisé par les programmeurs. Bien que je ne l'aie jamais vu utilisé, si j'avais vu un nombre octal en notation scientifique j'aurais pensé qu'il était en base 8. Mon plus ancien souvenir d'une notation scientifique utilisant un 'e' minuscule remonte à la fin des années 70 et je trouvais immédiatement cela déroutant. Le problème apparut lorsque l'on introduisit les minuscules en FORTRAN, et non à ses débuts. En réalité il existait des fonctions qui manipulaient la base des logarithmes naturels, et elles étaient toutes en majuscules ».
Chuck Allison écrivait : Le nombre total de nombres dans un système à virgule flottante est
2(M-m+1)b^(p-1) + 1
b est la base (généralement 2), p la précision (le nombre de chiffres dans la mantisse), M le plus grand exposant, et m le plus petit. Dans la norme IEEE 754, on a :
M = 1023, m = -1022, p = 53, b = 2
d'où le nombre total de nombres est
2(1023+1022+1)2^52
= 2((2^10-1) + (2^10-1))2^52
= (2^10-1)2^54
= 2^64 - 2^54
La moitié de ces nombres (correspondant à un exposant dans l'intervalle[-1022, 0]) sont inférieurs à un en valeur absolue, et donc 1/4 de cette expression, soit 2^62 - 2^52 + 1 (approximativement 2^62) est dans l'intervalle [0,1[. Voir mon article à http://www.freshsources.com/1995006a.htm (dernier du texte).

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.