7. Flux de contrôle
Table des matières
- 7.1 L’instruction if
- 7.2 L’instruction & l’expression switch
- 7.3 Deux formes de switch : switch Statement vs switch Expression
- 7.4 Gestion de null
Le flux de contrôle en Java fait référence à l’ordre dans lequel les instructions individuelles, les commandes ou les appels de méthode sont exécutés pendant l’exécution du programme.
Par défaut, les instructions s’exécutent séquentiellement de haut en bas, mais les instructions de contrôle du flux permettent au programme de prendre des décisions, répéter des actions ou dériver les chemins d’exécution en fonction de conditions.
Java fournit trois grandes catégories de constructions de contrôle du flux :
- Instructions décisionnelles —
if,if-else,switch - Instructions de boucle —
for,while,do-whileet leforamélioré - Instructions de branchement —
break,continueetreturn
Tip
Comprendre le flux de contrôle est essentiel pour voir comment les données circulent dans votre programme et comment chaque décision logique est évaluée étape par étape.
7.1 L’instruction if
L’instruction if est une structure conditionnelle de contrôle du flux qui exécute un bloc de code uniquement si une expression booléenne spécifiée est évaluée à true. Elle permet au programme de prendre des décisions à l’exécution.
Syntaxe :
if (condition) {
// exécuté uniquement lorsque la condition est true
}
Une clause else optionnelle gère le chemin alternatif :
if (score >= 60) {
System.out.println("Passed");
} else {
System.out.println("Failed");
}
Plusieurs conditions peuvent être chaînées à l’aide de else if :
if (grade >= 90) {
System.out.println("A");
} else if (grade >= 80) {
System.out.println("B");
} else if (grade >= 70) {
System.out.println("C");
} else {
System.out.println("D or below");
}
Note
La condition de if doit être évaluée comme un boolean ; les types numériques ou les objets ne peuvent pas être utilisés directement comme conditions.
Les accolades {} sont facultatives pour une seule instruction mais sont fortement recommandées afin d’éviter des erreurs logiques subtiles.
Une chaîne if-else est évaluée de haut en bas, et seul le premier branchement dont la condition est évaluée à true est exécuté.
7.2 L’instruction switch & l’expression
La construction switch est une structure de contrôle du flux qui sélectionne une branche parmi plusieurs alternatives en fonction de la valeur d’une expression (le selector).
Comparé aux longues chaînes de if-else-if, un switch :
- Est souvent plus facile à lire lorsqu’on teste de nombreuses valeurs discrètes (constantes, enums, chaînes).
- Peut être plus sûr et plus concis lorsqu’il est utilisé comme expression switch
parce que :
- Il produit une valeur.
- Le compilateur peut imposer l’exhaustivité et la cohérence de type.
Java 21 prend en charge :
- Le
switchclassique en tant qu’instruction (contrôle du flux uniquement). - Le
switchen tant qu’expression (produit un résultat). - Le pattern matching dans
switch, y compris les type patterns et les guards.
Les deux formes de switch partagent les mêmes règles concernant le selector (la variable cible du switch) et les valeurs case acceptables.
7.2.1 La variable cible du switch peut être
| Control Variable type |
|---|
byte / Byte |
short / Short |
char / Character |
int / Integer |
String |
Enum types (selectors of an enum) |
| Any reference type (with pattern matching) |
var (if it resolves to one of the allowed types) |
Warning
Non autorisé comme type de selector pour switch :
booleanlongfloatdouble
7.2.2 Valeurs case acceptables
Pour un switch non pattern, chaque étiquette case doit être une constante à la compilation compatible avec le type du selector.
Autorisé comme étiquettes case :
- Littéraux tels que
0,'A',"ON". - Constantes enum, par ex.
REDouColor.GREEN. - Variables constantes final (constantes à la compilation).
Une variable constante à la compilation :
- Doit être déclarée avec
finalet initialisée dans la même instruction. - Son initialiseur doit lui-même être une expression constante (généralement en utilisant des littéraux et d’autres constantes à la compilation).
7.2.3 Compatibilité de type entre selector et case
Le type du selector et chaque étiquette case doivent être compatibles :
- Les constantes numériques des case doivent être dans l’intervalle du type du selector.
- Pour un selector
enum, les étiquettes case doivent être des constantes de cetenum. - Pour un selector
String, les étiquettes case doivent être des constantes de chaîne.
7.2.4 Pattern Matching dans Switch
Le switch en Java 21 prend en charge le pattern matching, y compris :
- Type patterns :
case String s - Patterns avec garde :
case String s when s.length() > 3 - Pattern null :
case null
Exemple :
String describe(Object o) {
return switch (o) {
case null -> "null";
case Integer i -> "int " + i;
case String s when s.isEmpty() -> "empty string";
case String s -> "string (" + s.length() + ")";
default -> "other";
};
}
Points clés :
- Chaque pattern introduit une variable de pattern (comme
ious). - Les variables de pattern sont dans la portée uniquement à l’intérieur de leur arm (ou des chemins où le pattern est connu comme correspondant).
- L’ordre est important en raison de la dominance : les patterns plus spécifiques doivent précéder les plus généraux.
7.2.4.1 Noms de variables et portée entre les branches
Avec le pattern matching, la variable de pattern n’existe que dans la portée de l’arm dans lequel elle est définie. Cela signifie que vous pouvez réutiliser le même nom de variable dans différents branches case.
- Exemple :
switch (o) {
case String str -> System.out.println(str.length());
case CharSequence str -> System.out.println(str.charAt(0));
default -> { }
}
Note
Ce dernier exemple ne retourne pas de valeur, il s’agit donc d’un switch instruction et non d’une expression switch.
7.2.4.2 Ordonnancement, dominance et exhaustivité dans les switch à patterns
Lorsqu’on utilise le pattern matching, l’ordre des branches est crucial en raison de la dominance et du potentiel code inatteignable.
Un pattern plus général ne doit pas apparaître avant un pattern plus spécifique, sinon ce dernier devient inatteignable.
- Exemple (branche inatteignable) :
return switch (o) {
case Object obj -> "object";
case String s -> "string"; // ❌ DOES NOT COMPILE: unreachable, String is already matched by Object
};
- Autre exemple avec une garde :
return switch (o) {
case Integer a -> "First";
case Integer a when a > 0 -> "Second"; // ❌ DOES NOT COMPILE: unreachable, the first case matches all Integers
// ...
};
Lorsqu’on utilise le pattern matching, les switch doivent être exhaustifs ; c’est-à-dire qu’ils doivent gérer toutes les valeurs possibles du selector.
Cela peut être réalisé en :
- Fournissant un case
defaultqui gère toutes les valeurs non correspondantes aux autres cases. -
Fournissant une clause case finale avec un type de pattern qui correspond au type de référence du selector.
-
Exemple (non exhaustif) :
Number number = Short.valueOf(10);
switch (number) {
case Short s -> System.out.println("A"); // ❌ DOES NOT COMPILE: not exhaustive, selector is of type Number
}
Pour corriger cela, vous pouvez :
- Changer le type de référence de
numberenShort(l’exhaustivité est alors satisfaite par le seul case). - Ajouter une clause
defaultqui couvre toutes les valeurs restantes. - Ajouter une clause case finale couvrant le type de la variable selector, par exemple :
Number number = Short.valueOf(10);
switch (number) {
case Short s -> System.out.println("A");
case Number n -> System.out.println("B");
}
Warning
L’exemple suivant, qui utilise à la fois une clause default et une clause finale avec le même type que la variable selector, ne compile pas : le compilateur considère l’un des deux cases comme dominant toujours l’autre.
Number number = Short.valueOf(10);
switch (number) {
case Short s -> System.out.println("A");
case Number n -> System.out.println("B"); // ❌ DOES NOT COMPILE: dominated by either the default or the Number pattern
default -> System.out.println("C");
}
7.3 Deux formes de switch : switch Statement vs switch Expression
7.3.1 L’instruction switch
Une instruction switch est utilisée comme construction de contrôle du flux.
Elle ne s’évalue pas, en elle-même, comme une valeur, bien que ses branches puissent contenir des instructions return qui retournent depuis la méthode englobante.
switch (mode) { // switch statement
case "ON":
start();
break; // prevents fall-through
case "OFF":
stop();
break;
default:
reset();
}
Points clés :
- Chaque clause
caseinclut une ou plusieurs valeurs correspondantes séparées par des virgules,. Un séparateur suit, qui peut être soit deux-points:soit, moins couramment pour les instructions, l’opérateur flèche->. Enfin, une expression ou un bloc (entouré de{}) définit le code à exécuter lorsqu’une correspondance se produit. Si vous utilisez l’opérateur flèche pour une clause, vous devez l’utiliser pour toutes les clauses de cette instruction switch. - Le fall-through est possible pour les case de style deux-points à moins qu’une branche utilise
break,returnouthrow. Lorsqu’il est présent,breaktermine le switch après l’exécution de son case ; sans lui, l’exécution continue, dans l’ordre, vers les branches suivantes. - Une clause
defaultest optionnelle et peut apparaître n’importe où dans l’instruction switch. Elle s’exécute s’il n’y a pas de correspondance pour les cases précédents. - Une instruction switch ne produit pas de valeur comme une expression ; vous ne pouvez pas assigner directement une instruction switch à une variable.
7.3.1.1 Comportement de fall-through
Avec des case de style deux-points, l’exécution saute à l’étiquette case correspondante.
S’il n’y a pas de break, elle continue dans le case suivant jusqu’à ce qu’un break, return ou throw soit rencontré.
int n = 2;
switch (n) {
case 1:
System.out.println("1");
case 2:
System.out.println("2"); // printed
case 3:
System.out.println("3"); // printed (fall-through)
break;
default:
System.out.println("message default");
}
Sortie :
2
3
Note
Si, dans l’exemple précédent, nous supprimons le break sur le case 3, le message de la branche default sera également affiché.
7.3.2 L’expression switch
Une expression switch produit toujours une valeur unique comme résultat.
- Exemple :
int len = switch (s) { // switch expression
case null -> 0;
case "" -> 0;
default -> s.length();
};
Points clés :
- Chaque clause
caseinclut une ou plusieurs valeurs correspondantes séparées par des virgules,, suivies de l’opérateur flèche->. Puis une expression ou un bloc (entouré de{}) définit le résultat pour cet arm. - Lorsqu’elle est utilisée avec une assignation ou une instruction
return, une expression switch nécessite un point-virgule de terminaison;après l’expression. - Il n’y a pas de fall-through entre les arms avec flèche. Chaque arm correspondant est exécuté exactement une fois.
- Une expression switch doit être exhaustive : toutes les valeurs possibles du selector doivent être couvertes (via des case explicites et/ou
default). - Le type du résultat doit être cohérent entre toutes les branches. Par exemple, si un arm produit un
int, les autres arms doivent produire des valeurs compatibles avecint.
7.3.2.1 yield dans les blocs d’expression switch
Lorsqu’un arm d’une expression switch utilise un bloc au lieu d’une expression unique, vous devez utiliser yield pour fournir le résultat de cet arm.
int len = switch (s) {
case null -> 0;
default -> {
int l = s.trim().length();
System.out.println("Length: " + l);
yield l; // result of this arm
}
};
Note
yield est utilisé uniquement dans les expressions switch.
break value; n’est pas autorisé comme moyen de retourner une valeur depuis une expression switch.
7.3.2.2 Exhaustivité pour les expressions switch
Puisqu’une expression switch doit retourner une valeur, elle doit également être exhaustive ; en d’autres termes, elle doit gérer toutes les valeurs possibles du selector.
Vous pouvez garantir cela en :
- Fournissant un case
default. - Pour un selector enum : couvrant explicitement toutes les constantes enum.
- Pour des types sealed ou des pattern switch : couvrant tous les sous-types autorisés ou fournissant un
default.
Exemple, exhaustif via default :
int val = switch (s) {
case "one" -> 1;
case "two" -> 2;
default -> 0;
};
7.4 Gestion de null
Switch classique (sans patterns)
Si l’expression selector d’un switch classique (sans pattern matching) est évaluée à null, une NullPointerException est levée à l’exécution.
Pour éviter cela, vérifiez null avant d’effectuer le switch :
if (s == null) {
// handle null
} else {
switch (s) {
case "A" -> ...
default -> ...
}
}
Pattern switch (avec case null)
Avec le pattern matching, vous pouvez gérer null directement à l’intérieur du switch :
int len = switch (s) {
case null -> 0;
default -> s.length();
};
Note
Pour les expressions switch :
Si vous ne gérez pas null et que le selector est null, une NullPointerException est levée.
L’utilisation de case null rend le switch explicitement sûr vis-à-vis de null.
Warning
Chaque fois que case null est utilisé dans un switch, le switch est traité comme un pattern switch, et toutes les règles applicables aux pattern switch (y compris l’exhaustivité et la dominance) s’appliquent.