Structures de contrôles

Introduction

Cette section vous propose de réaliser différents programmes pour aborder les structures de contrôles.

Les concepts abordés ici sont:

Nous allons également utiliser les tests unitaires pour tester nos programmes en utilisant la librairie JUnit4.

Environnement d’étude

Pour étudier les structures de contrôles, nous allons utiliser un framework de tests unitaires : JUnit version 4

Le fonctionnement de ce framework n’est pas expliqué ici. Vous devrez suivre les modèles donnés et les adapter aux demandes faites dans ce document.

Création et configuration du projet

Créez un projet nommé Controle.

Allez dans les propriétés du projet.

Allez dans Java Build Path > Libraries > Add Library...

Dans la fenêtre Add library sélectionnez JUnit puis cliquez sur Next

Choisissez la version de la librairie JUnit 4.

Cliquez sur Finish puis OK

Cliquez bouton droit sur le projet et sélectionnez New > Source Folder :

Création du paquet

Dans chacun des répertoires sources (src et test) ajoutez un package nommé com.example.controles

Remarque importante

Pour tout le code qui suit, le paquet à utiliser sera com.example.controles.

Deux répertoires contiendront les sources:

L’indication sur le répertoire (src ou test) indique que la classe sera créée dans le paquet com.example.controles du répertoire correspondant.

Organisation des tests unitaires

Pour une classe à tester, on crée la classe de test correspondante:

Emplacement des fichiers

Classe abstraite Controle

Nous allons nous doter d’une classe abstraite qui sera facilement réutilisable pour tester notre code.

Créez la classe abstraite Controle dans le répertoire src en utilisant le code source suivant:

package com.example.controles;

public abstract class Controle {
    public abstract Object lancer(String[] args);
}

Sous-classe de contrôle

package com.example.controles;

public class ControleIf extends Controle {
    public Object lancer(String[] args) {
        Object returnedValue = null;
        if(args.length <= 0)
            returnedValue = null;
        if(args.length <= 1)
            returnedValue = args[0];
        if(args.length == 2)
            returnedValue = args[1];
        if(args.length >= 3)
            returnedValue = args[2];
        return returnedValue;
    }
}

Comparaison valeur renvoyée / attendue

Pour tester, on utilise une assertion. La méthode assertEquals permet de tester l’égalité de deux valeurs :

On n’écrit en général qu’une seule assertion par test (même s’il est possible d’en écrire plusieurs):

@Test
public void testControleIfUneChaine(){
  assertEquals("Test une seule chaîne",
      "Un", controle.lancer(new String[]{"Un"}));
}

Toujours mettre @Test avant la méthode de test

Utilisation d’assertion

Assertion

assertEquals("Désignation du test",
    "Valeur attendue (String)", instance.renvoieValeurString());

Pour nos travaux, uniquement une méthode qui renvoie un String

Initialisation du test

Création de l’objet controle

public class TestControleIf {
  protected Controle controle ;
  @Before
  public void initControles(){
    controle = new ControleIf();
  }
  //...
}

@Before désigne la méthode pour initialiser

Méthode appelée avant chaque méthode de test (marquée par @Test)

Ceci permet de trouver plus facilement quelle assertion n’a pas été respectée.

Il existe d’autres méthodes que assertEquals. Voir la Javadoc de JUnit 4 qui est disponible sur cette page.

Code source des tests

Le code source des tests est déjà écrit et est disponible ici

Structures de contrôles

Dans cette partie, il vous est demandé de tester et vérifier si le fonctionnement des différents codes proposés est bien celui attendu.

Plus d’information sur les structures de contrôles est donné ici.

Branchement

if

Créez une classe ControleIf dans le répertoire src :

package com.example.controles;

public class ControleIf extends Controle {

    @Override
    public Object lancer(String[] args) {
    Object returnedValue = null;
        if(args.length <= 0)
            returnedValue = null;
        if(args.length <= 1)
            returnedValue = args[0];
        if(args.length == 2)
            returnedValue = args[1];
        if(args.length >= 3)
            returnedValue = args[2];
        return returnedValue;
    }
}

Tester manuellement

Dans le répertoire src:

package com.example.controles;

public class VerifierIf {
    private Controle controle ;
    public static void main(String[] args) {
        Controle controle = new ControleIf();
        System.out.println(controle.lancer(new String[]{}));
        System.out.println(controle.lancer(new String[]{"Un"}));
        System.out.println(controle.lancer(new String[]{"Un", "Deux"}));
        System.out.println(controle.lancer(new String[]{"Un", "Deux", "Trois"}));
    }
}

Tester avec des JUnit

Création d’une classe de test

Créez la classe de test TestControleIf dans le répertoire test en utilisant le code source suivant:

package com.example.controles;

import org.junit.Before;

public class TestControleIf {
  protected Controle controle ;
    @Before
    public void initControle(){
        controle = new ControleIf();
    }
}

Ajoutez les méthodes permettant de tester dans le corps de la classe TestControleIf (voir ci-dessous).

Les imports suivants seront nécessaires:

Une bonne pratique consiste à ne mettre qu’une seule assertion par méthode de test.

@Test
public void testControleIfUneChaine(){
  assertEquals("Test une seule chaîne",
      "Un", controle.lancer(new String[]{"Un"}));
}

@Test
public void testControleIfDeuxChaine(){
  assertEquals("Test deux chaînes",
      "Deux", controle.lancer(new String[]{"Un", "Deux"}));
}
Exécution des tests

Lancez les tests en allant dans le menu Run > Run As > JUnit Test

Ajoutez les cas pour les tableaux de chaînes suivants:

Modification du programme

Ajoutez la ligne suivante juste après le troisième if:

  if(args.length >= 3)
    System.out.println("Bonjour");
    returnedValue = args[2];

Relancez les tests. Que se passe-t-il ? Est-ce normal ?

Recommandation: utilisation des blocs d’instruction

Un bloc de code est une série d’instructions entre deux accolades {...}.

Bien qu’il soit parfaitement possible de n’utiliser qu’une seule instruction dans un if comme montré ci-dessus, il est plutôt recommandé d’utiliser des blocs de code systématiquement.

Il est parfaitement possible de ne mettre qu’une seule instruction dans un bloc, et c’est même recommandé dans les structures de contrôles:

if(args.length <= 0)    {
  returnedValue = null;
}
if(args.length <= 1){
  returnedValue = args[0];
}
if(args.length == 2){
  returnedValue = args[1];
}
if(args.length >= 3){
  System.out.println("Bonjour");
  returnedValue = args[2];
}
return returnedValue;

Entre le code ci-dessus et la précédente version de la classe ControleIf le fonctionnement est exactement le même. La seule différence est que si on ajoute une ligne en oubliant d’ajouter des accolades (ce qui arrive fréquemment), le code continue à fonctionner correctement.

Outre l’aération et la lisibilité du code, cela le rend donc plus robuste en cas de modification.

if-else

L’instruction else s’utilise comme suit:

package com.example.controles;

public class ControleIfElse extends Controle {
    @Override
    public Object lancer(String[] args) {
        Object returnedValue ;
        if (args.length > 0 && args[0] != ""){
            returnedValue = args[0];
        }
        else {
            returnedValue = "N/A";
        }
        return returnedValue;
    }
}

Tester manuellement

Dans le répertoire src:

package com.example.controles;

public class VerifierIfElse {
    private Controle controle ;
    public static void main(String[] args) {
        Controle controle = new ControleIfElse();
        System.out.println(controle.lancer(new String[]{}));
        System.out.println(controle.lancer(new String[]{"Un"}));
        System.out.println(controle.lancer(new String[]{"Un", "Deux"}));
        System.out.println(controle.lancer(new String[]{"Un", "Deux", "Trois"}));
    }
}

Tester avec JUnit

Créez la classe ControleIfElse et testez-la dans une méthode comme suit:

@Before
public void initControle(){
  controle = new ControleIfElse();
}
@Test
public void testControleIfElse(){
  assertEquals("Test une seule chaîne",
      "Un", controle.lancer(new String[]{"Un"}));
}

Ajoutez une instruction assertEquals permettant de vérifier l’exécution du bloc else (la valeur à tester doit être "N/A").

Testez au moins deux autres cas.

switch-case

Les cascades de if-else

On peut critiquer le code suivant du point de vue de :

package com.example.controles;

public class ControleCascadeIfElse extends Controle {
  public Object lancer(String[] args) {
    String returnedValue;
    int valeurChoix = Integer.parseInt(args[0]);
        if(valeurChoix == 1){
            returnedValue = "Menu Préférences";
        }
        else if (valeurChoix == 2){
            returnedValue = "Menu Edition";
        }
        else if (valeurChoix == 3){
            returnedValue = "Bonjour";
        }
        else if (valeurChoix == 4){
            returnedValue = "Au revoir";
        }
        else if (valeurChoix == 5){
            returnedValue = "Sortir";
        }
        else {
            returnedValue = "Saisir votre choix (1,2,3,4 ou 5) : ";
        }
        return returnedValue;
  }
}
Tester manuellement

Dans le répertoire src, créer les deux classes suivantes:

package com.example.controles;

public abstract class VerifierDifferentsCas {
    protected Controle controle ;

    public abstract void initControle();

    public void verifier(){
        initControle();
        System.out.println(controle.lancer(new String[]{"1"}));
        System.out.println(controle.lancer(new String[]{"2"}));
        System.out.println(controle.lancer(new String[]{"3"}));
        System.out.println(controle.lancer(new String[]{"4"}));
        System.out.println(controle.lancer(new String[]{"5"}));
        System.out.println(controle.lancer(new String[]{"0"}));
    }
}

Classe de vérification:

package com.example.controles;

public class VerifierCascadeIfElse extends VerifierDifferentsCas {
    @Override
    public void initControle() {
        controle = new ControleCascadeIfElse();
    }
    public static void main(String[] args) {
        VerifierDifferentsCas verificateur = new VerifierCascadeIfElse();
        verificateur.verifier();
    }
}
Test avec JUnit

Ce code fonctionne et peut être testé avec les tests unitaires suivants:

package com.example.controles;

import org.junit.Test;
import org.junit.Before;
import static org.junit.Assert.assertEquals ;

public abstract class TestControleDifferentsCas {
    protected Controle controle ;

    @Before
    public abstract void initControle();

    @Test
    public void testControleMenuPreferences(){
        assertEquals("Test 1",
                "Menu Préférences", controle.lancer(new String[]{"1"}));
    }
    @Test
    public void testControleMenuEdition(){
        assertEquals("Test 2",
                "Menu Edition", controle.lancer(new String[]{"2"}));
    }
  /*
   * COMPLETEZ LES TESTS
   */
}

Créez une classe TestControleCascadeIfElse qui dérive de TestControleDifferentsCas et ajoutez y la méthode suivante :

@Override
public void initControle() {
    controle = new ControleCascadeIfElse();
}

Complétez les cas de test manquant dans le code précédent.

La structure de branchement switch-case

Voici un code équivalent avec une structure switch-case.

L’instruction break permet de stopper l’exécution du switch et d’en sortir immédiatement (vous pouvez essayer de la supprimer pour observer l’effet sur le déroulement du code).

package com.example.controles;

public class ControleSwitchCase extends Controle {

    @Override
    public Object lancer(String[] args) {
        String returnedValue;
        int valeurChoix = Integer.parseInt(args[0]);
        switch(valeurChoix){
        case 1:
            returnedValue = "Menu Préférences";
            break;
        case 2:
            returnedValue = "Menu Edition";
            break;
        case 3:
            returnedValue = "Bonjour";
            break;
        case 4:
            returnedValue = "Au revoir";
            break;
        case 5:
            returnedValue = "Sortir";
    default:
            returnedValue = "Saisir votre choix (1,2,3,4 ou 5) : ";
    }
    return returnedValue;
    }
}
Tester manuellement

Retestez ce code avec les tests précédents (en créant une nouvelle classe VerifierSwitchCase dérivant de VerifierDifferentsCas et en y instanciant ControleSwitchCase) et vérifiez que son fonctionnement est correct.

Ce code fonctionne-t-il ? Que manque-t-il ?

Test avec JUnit

Retestez ce code avec les tests précédents (en dérivant de TestControleDifferentsCas et en y instanciant ControleSwitchCase) et vérifiez que son fonctionnement est correct.

Ce code fonctionne-t-il ? Que manque-t-il ?

String et switch-case

Il est possible de faire des switch-case sur des String depuis la version 7 de Java. Vous serez peut-être amenés à maintenir du code avec une version plus ancienne.

Vous ne pourrez donc pas utiliser la syntaxe suivante avec ces versions :

    switch(choix){
    case "1": System.out.println("Menu Préférences");
      break;
    case "2": System.out.println("Menu Edition");
      break;
    default: System.out.println("Recommencez !");
    }

Opérateur ternaire

Etudiez maintenant le fonctionnement de cette méthode :

public void testChaines(String a, String b){
    if( a == null ? b == null : a.equals(b) ){
        System.out.println("Les chaînes '"+ a + "' et '"+b+ "' sont les mêmes");
    }
    else{
        System.out.println("Les chaînes '"+ a + "' et '"+b+ "' sont différentes");
    }
}

L’opérateur ternaire ? : qui est utilisé ici fonctionne comme suit.

Le code :

x = 1 ;
a = (x == 1 ? 2 : 3);
System.out.println(a);

Conduit à l’affichage suivant: 2

Le code :

x = 4 ;
a = (x == 1 ? 2 : 3);
System.out.println(a);

Conduit à l’affichage suivant: 3

Le code équivalent à (x == 1 ? 2 : 3) est :

int f(int x){
  if(x == 1){
    return 2;
  }else{
    return 3;
  }
}

Les deux codes suivants sont donc équivalents:

//Première version
a = (x == 1 ? 2 : 3);

//Seconde version
a = f(x);

Quel est l’intérêt de cette notation ?

Quel peut être son inconvénient ?

Exercice

Créez les tests unitaires permettant de vérifier le fonctionnement de la méthode testChaines(String, String)

Réécrivez cette méthode en n’utilisant que des if-else et vérifiez son fonctionnement.

Boucles

for

Le premier mode d’utilisation de la boucle for se fait comme en langage C:

for ( initialisation; condition; mise à jour ) {
   instructions;
}

Ainsi le parcours d’un tableau se fait ainsi:

public Object lancer(String[] args) {
  StringBuffer sb = new StringBuffer();
  for(int i = 0 ; i < args.length; i++){
    sb.append(args[i]);
  }
  return sb.toString();
}

Une utilisation pour le parcours de listes (vues au prochain chapitre) est la suivante:

for (<Type> membreListe : liste){
  instructions;
}

Exemple avec un StringBuffer:

public Object lancer(String[] args) {
  StringBuffer sb = new StringBuffer();
  for(String str : args){
    sb.append(str);
  }
  return sb.toString();
}

while / do-while

La structure while permet de réaliser une boucle tant-que :

public Object lancer(String[] args) {
  StringBuffer sb = new StringBuffer();

  int i = args.length - 1;
  while(i >= 0){
    sb.insert(0, args[i]);
    i --;
  }
  return sb.toString();
}

La boucle répéter-tant-que est réalisable en utilisant la structure do-while:

public Object lancer(String[] args) {
  StringBuffer sb = new StringBuffer();

  int i = args.length - 1;

  do
  {
    sb.insert(0, args[i]);
    i --;
  } while(i >= 0);

  return sb.toString();
}

Testez cette boucle avec le tableau suivant:

Corrigez le code si nécessaire.

Contrôle des boucles

break

Cette instruction (déjà vue avec switch), permet de stopper l’exécution en cours d’une boucle.

Exemple d’utilisation de break:

public Object lancer(String[] args) {
  StringBuffer sb = new StringBuffer();
  for(int i = 0 ; i < args.length; i++){
    if(i == 2){
      break ;
    }
    sb.append(args[i]);
  }
  return sb.toString();
}

L’exécution du code précédent :

continue

Cette instruction (déjà vue avec switch), permet de ne pas exécuter la suite du bloc d’instruction pour l’itération courante.

Exemple d’utilisation de continue:

public Object lancer(String[] args) {
  StringBuffer sb = new StringBuffer();
  for(int i = 0 ; i < args.length; i++){
    if(i == 2){
      continue ;
    }
    sb.append(args[i]);
  }
  return sb.toString();
}

L’exécution du code précédent :

Enum

Dans cette partie, nous allons étudier une fonctionnalité importante du langage Java:

Les Enum sont notamment associés à la structure de contrôle switch.

Pourquoi les Enum

Les constantes (avant les Enum)

Nous pouvons remplacer les éléments pour chaque case d’un switch par des constantes:

public class AvantLesEnums {
    public static final String CHOIX_MENU_PREFERENCES = "1";
    public static final String CHOIX_MENU_EDITION     = "2";
    public static final String CHOIX_SORTIR           = "2";

    public void appliquerChoix(String choix){
        if(choix == CHOIX_SORTIR){
            System.out.println("Sortie du programme");
            System.exit(0);
        }
        switch(choix){
        case CHOIX_MENU_PREFERENCES: System.out.println("Menu Préférences");
        break;
        case CHOIX_MENU_EDITION: System.out.println("Menu Edition");
        break;
        default: appliquerAutreCommande(choix);
        }
    }
    public void appliquerAutreCommande(String choix){}

    public static void main(String[] args) {
        AvantLesEnums avantLesEnums = new AvantLesEnums();

        avantLesEnums.appliquerChoix("2");
    }
}

Ceci rend le code plus lisible. Cependant, il exite un risque pour que deux constantes soient de même valeur (ici CHOIX_SORTIR est identique à CHOIX_MENU_EDITION)

Dans ce cas, le risque de dysfonctionement du programme est important, sans qu’il soit nécessairement détecté par le compilateur.

Pour palier à cela, nous pouvons utiliser les Enum.

Définition des Enum

Les Enum permettent de définir un ensemble de valeurs pour lesquels il est certain que chacune d’elles seront distinctes, tout en étant typée.

De plus, elle peuvent être utilisées dans une structure switch...case comme n’importe quelle valeur constante, à la différence qu’on est assuré qu’aucune valeur ne peut être confondue avec une autre.

Utilisation particulièrement utile dans un switch...case

Enum: exemple de déclaration

On définit un Enum à la manière d’une classe, mais avec le mot clé enum:

public enum ItemMenuPrincipal {
    MENU_PREFERENCES, MENU_EDITION, MENU_SORTIR;
}

Intérêt :

import static com.example.enumeration.ItemMenuPrincipal.*;

public class AvecLesEnums {
    public void appliquerChoix(ItemMenuPrincipal choix){
        if(choix == MENU_SORTIR){
            System.out.println("Sortie du programme");
            System.exit(0);
        }
        switch(choix){
        case MENU_PREFERENCES: System.out.println("Menu Préférences");
        break;
        case MENU_EDITION: System.out.println("Menu Edition");
        break;
        default: appliquerAutreCommande(choix);
        }
    }
}

Méthodes statiques dans les Enum

On peut ajouter des méthodes de classe à un enum

public enum ItemMenuPrincipal {
    MENU_PREFERENCES, MENU_EDITION, DIRE_BONJOUR, DIRE_AU_REVOIR, MENU_SORTIR;

    public static ItemMenuPrincipal getItem(String identifiant){
        switch (identifiant) {
        case "1":
            return MENU_PREFERENCES;
        case "2":
            return MENU_EDITION;
        case "3":
            return MENU_SORTIR;
        }
        return null;
    }
}

Ajoutez un main dans la classe AvecLesEnums sur le modèle du main de la classe AvantLesEnums et vérifiez ce programme.

Enum comme des instances

On peut gérer les enum comme des objets, et leur ajouter des méthodes, des constructeurs:

public enum ItemMenuPrincipal {
    MENU_PREFERENCES("Menu Préférences"),
    MENU_EDITION("Menu Edition"),
    MENU_SORTIR("Sortie du programme");

    private String representation;

    private ItemMenuPrincipal(String representation){
        this.representation = representation;
    }

    public String toString(){
        return representation;
    }

    public static ItemMenuPrincipal getItem(String identifiant){
        switch (identifiant) {
        case "1":
            return MENU_PREFERENCES;
        case "2":
            return MENU_EDITION;
        case "3":
            return MENU_SORTIR;
        }
        return null;
    }
}

Modifiez la méthode appliquerChoix de la classe AvecLesEnums en supprimant les chaînes écrites en dur.

On peut même ajouter des comportements (nous reprenons ici les classes Calcul et dérivées :

public enum Operation {
    ADDITION("+", new Addition()),
    SOUSTRACTION("-", new Soustraction()),
    MULTIPLICATION("x", new Multiplication());

    private String representation;
    private Calcul calcul ;

    private Operation(String representation, Calcul calcul){
        this.representation = representation;
        this.calcul = calcul;
    }

    public int execute(int operandeA, int operandeB){
        calcul.calculer(operandeA, operandeB);
        return calcul.getResultat();
    }
}

Ce qui permet notamment de simplifier le code par ailleurs :

Operation op = ADDITION;
op.execute(10, 23);

Attention: pour que ce code compile, il faut ajouter un import static en début de fichier.

import static com.example.calculatrice.Operation.*;

Application : nombre de jours dans le mois

Écrire un enum Mois permettant de récupérer :

On rappelle que :

Vous écrirez les tests unitaires permettant de tester cet enum.

Exercices

Gestion d’une liste d’entiers

Nous allons définir une classe TableauEntier permettant de stocker des entiers dans une structure dynamique.

Cette classe doit empêcher tout modification non contrôlée de ses attributs (encapsulation).

Pour cela, vous allez utiliser un tableau d’entiers (int[]) dans lequel vous devrez gérer les opérations d’ajout, de suppression et d’insertion des valeurs.

La taille initiale de ce tableau sera 5 et sera incrémentée de 5 en 5, quand le nombre d’éléments dépassera la taille disponible.

Pour cela nous allons définir les méthodes suivantes:

Si le tableau int[] devient trop petit, vous devrez en définir un nouveau plus grand et recopier son contenu.

La classe EssaiTableauEntier comportera une méthode main(String[] args) et permettra de tester votre classe, par exemple en demandant à l’utilisateur de saisir des informations.