IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 3 - Contrôle du flux du programme

pages : 1 2 3 4 5 6 

Itération

Les instructions de contrôle de boucle while, do-while et for sont souvent appelés 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 :

while(expression booléenne)
  instruction

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 :

//: c03:WhileTest.java
// Démonstration de la boucle while.

public class WhileTest {
  public static void main(String[] args) {
    double r = 0;
    while(r < 0.99d) {
      r = Math.random();
      System.out.println(r);
    }
  }
} ///:~

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, et la taille de cette liste est différente à chaque exécution du programme.

do-while

Voici la forme de la boucle do-while :

do
  instruction
while(expression booléenne);

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

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 :

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

Chacune des expressions instruction d'initialisation, expression booléenne et instruction d'itération peut être vide. 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, 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 :

//: c03:ListCharacters.java
// Démonstration de la boucle "for" listant
// tous les caractères ASCII.

public class ListCharacters {
  public static void main(String[] args) {
  for( char c = 0; c < 128; c++)
    if (c != 26 )  // Effacement de l'écran ANSI
      System.out.println(
        "value: " + (int)c +
        " character: " + c);
  }
} ///:~

Remarquons que la variable c 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 c est l'expression contrôlée par la boucle for.

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 :

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.

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) avait un seul usage 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 :

//: c03:CommaOperator.java
public class CommaOperator {
  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);
    }
  }
} ///:~

En voici le résultat :

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.

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 :

//: c03:BreakAndContinue.java
// Démonstration des mots clefs break et continue.

public class BreakAndContinue {
  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 boucle
      if(i % 10 != 0) continue; // Début de boucle
      System.out.println(i);
    }
  }
} ///:~

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 n'imprime que lorsque la valeur de i est divisible par 10. La sortie est :

0
9
18
27
36
45
54
63
72
10
20
30
40

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.

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. Toutefois, 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 :

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 :

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

Cas 1 : break interrompt l'itération intérieure, on se retrouve dans l'itération extérieure. Cas 2 : continue branche à l'itération intérieure. Mais, 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. 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 :

//: c03:LabeledFor.java
// la boucle for "étiquetée" en Java.

public class LabeledFor {
  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++) {
        prt("i = " + i);
        if(i == 2) {
          prt("continue");
          continue;
        }
        if(i == 3) {
          prt("break");
          i++; // Sinon i ne sera
               // jamais incrémenté.
          break;
        }
        if(i == 7) {
          prt("continue outer");
          i++; // Sinon i ne sera
               // jamais incrémenté.
          continue outer;
        }
        if(i == 8) {
          prt("break outer");
          break outer;
        }
        for(int k = 0; k < 5; k++) {
          if(k == 3) {
            prt("continue inner");
            continue inner;
          }
        }
      }
    }
    // On ne peut pas utiliser un break ou
    // un continue vers une étiquette ici
  }
  static void prt(String s) {
    System.out.println(s);
  }
} ///:~

Ce programme utilise la méthode prt( ) déjà définie dans d'autres exemples.

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.

Voici le résultat :

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

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 :

//: c03:LabeledWhile.java
// La boucle while "étiquetée" en Java.

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

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

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

Le résultat de cette méthode éclaircit cela :

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

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

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 :

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 correspondante (simple ou composée) est exécutée. Si aucune égalité n'est trouvée, l'instruction qui suit default est exécutée.

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Jean-Pierre Vidal ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 5 6 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java