Précédent Remonter Suivant

Chapitre 3  Structuration

La programmation, c'est l'art d'organiser la complexité. E. Dijkstra
Il est déjà possible d'écrire des programmes importants avec les constructions de base que nous avons vues. Cependant, il nous manque des outils permettant de structurer nos programmes, pour maîtriser une complexité qui deviendrait sinon trop grande pour permettre une gestion aisée par un être humain. Comme nous l'avons déjà vu (cf. § 1.1), la structuration peut être définie comme l'art de regrouper des entités élémentaires en entités de plus haut niveau, qui puissent être manipulées directement, permettant ainsi de s'affranchir de la maîtrise des constituants élémentaires quand on travaille avec la structure.

En programmation, cette structuration s'applique suivant deux axes :
  1. On peut regrouper des opérations élémentaires, que l'on effectue régulièrement, en entités auxquelles on donnera un nom et une sémantique précise. C'est ce que nous verrons avec les fonctions et les procédures (§ 3.2).
  2. On peut aussi regrouper des données, qui ensemble servent à représenter un concept unitaire. Nous allons commencer par ce premier type de structuration, en définissant la notion de classe (§ 3.1).

3.1  La classe, première approche : un regroupement de variables

Il est extrêmement fréquent que l'on manipule un concept complexe, qui nécessite pour être représenté l'utilisation de plusieurs variables. Restons dans notre domaine d'application bancaire ; il est clair que pour être utilisé, le programme que nous avons écrit doit s'appliquer à un compte bancaire, qui est caractérisé au minimum par : On pourrait bien sûr avoir quatre variables indépendantes, nom, adresse, numéro et solde, mais cela n'est guère satisfaisant, car rien n'indique explicitement que ces quatre variables sont "liées". Que ferons-nous dans ces conditions pour nous y retrouver quand il s'agira d'écrire le programme de gestion d'une agence bancaire, dans laquelle il y a plusieurs centaines de comptes ?

La plupart des langages de programmation offrent donc la possibilité de regrouper des variables en une structure commune et nommée, que l'on peut ensuite manipuler globalement. En Java, cette structure s'appelle la classe1.

Si je souhaite définir le concept de compte bancaire, je peux définir une classe, avec le mot clé class. Si j'appelle cette classe CompteBancaire, elle doit être définie dans un nouveau fichier, CompteBancaire.java, comme nous l'expliquons au paragraphe H.1. Voici le contenu de ce fichier :
// Classe CompteBancaire - version 1.0

/**
 * Classe permettant de regrouper les éléments qui décrivent un
 * compte bancaire. Pas de programmation objet pour l'instant
 * @author Karl Tombre
 */

public class CompteBancaire {
    String nom;       // le nom du client
    String adresse;   // son adresse
    int numéro;       // numéro du compte (int pour simplifier)
    int solde;        // solde du compte (opérations entières pour simplifier)
}
La définition d'une classe définit un nouveau type ; nous pouvons donc maintenant déclarer une variable de ce nouveau type : CompteBancaire monCompte et travailler directement avec cette variable. Cependant, il faut être capable d'accéder aux éléments individuels de cette variable, par exemple le solde du compte. La notation pointée permet de le faire : monCompte.solde désigne le solde du compte représenté par la variable monCompte.

NB : À ce stade, on notera qu'on peut considérer une classe comme un produit cartésien.

3.1.1  Allocation de mémoire

Une classe peut en fait être considérée comme un "moule" ; la classe CompteBancaire décrit la configuration type de tous les comptes bancaires, et si on déclare deux variables :
CompteBancaire c1;
CompteBancaire c2;
chacune de ces variables possède son propre exemplaire de nom, d'adresse, de numéro et de solde. En effet, il ne serait pas normal que tous les comptes partagent le même numéro ou le même solde ! Donc c1.numéro et c2.numéro, par exemple, sont deux variables distinctes.

Une loi générale est que dans une "case" mémoire, on ne peut stocker que des nombres. Reste à savoir comment interpréter le nombre contenu dans une telle case ; cela va bien entendu dépendre du type de la variable :

En présence de types scalaires comme des entiers, l'interprétation est immédiate : le nombre contenu dans la case est la valeur de l'entier.

Pour les autres types simples (réels, caractères...) un codage standard de la valeur par un nombre permet également une interprétation immédiate (cf. annexe F).

Les choses se passent différemment pour toutes les variables de types définis à partir d'une classe : ces variables, telles que c1 et c2 dans notre exemple, sont techniquement des références. Cela signifie que les cases mémoires correspondantes ne "contiennent" pas directement les différentes données nécessaires pour représenter un compte, mais uniquement l'adresse d'un emplacement mémoire où un tel compte est représenté. Autrement dit, leur r-valeur contient une l-valeur.

Pour réserver un emplacement mémoire effectif, il faut alors passer par une opération d'allocation mémoire, grâce à l'instruction new. Aussi longtemps que celle-ci n'a pas été effectuée, la variable monCompte, par exemple, a une valeur spécifique appelée null, correspondant à une adresse indéfinie, et il se produit une erreur si on essaye d'accéder à monCompte.numéro. Il faut donc faire cette allocation mémoire, par exemple au moment de la déclaration de la variable :
CompteBancaire monCompte = new CompteBancaire();
avant de pouvoir continuer par exemple par :
monCompte.solde = 1000;

Aparté pour les petits curieux : que devient la mémoire qui n'est plus utilisée ?

Si vous avez l'expérience de certains langages de programmation comme Pascal, C ou C++, vous vous demandez peut-être comment procéder pour "rendre" la mémoire affectée par new, une fois que vous n'en avez plus besoin. En effet, en plus d'un mécanisme similaire à new, ces langages ont aussi une construction permettant de libérer de la mémoire.

Rien de tel en Java ; ses concepteurs ont choisi de le munir d'un mécanisme de ramasse-miettes (garbage collector en anglais). Un processus automatique, qui s'exécute en parallèle et en arrière-plan, sans que l'utilisateur ait à intervenir, entreprend de collecter, au fur et à mesure, les "cases mémoire" qui ne sont plus référencées par aucune variable.

3.1.2  Exemple : un embryon de programme de gestion de compte

Pour illustrer notre propos, reprenons notre exemple bancaire. Nous allons maintenant demander à l'utilisateur de donner les coordonnées du compte, et toutes les manipulations se feront sur une variable de type CompteBancaire. Notez bien que nous travaillons maintenant avec deux fichiers, Banque.java et CompteBancaire.java, selon un principe général en Java, qui impose qu'à chaque classe corresponde un fichier séparé :
// Classe Banque - version 2.0

/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * cette fois-ci une variable de type CompteBancaire
 * @author Karl Tombre
 * @see    CompteBancaire
 */

import java.io.*;       // Importer toutes les fonctionnalités de java.io

public class Banque {
    public static void main(String[] args) {
        InputStreamReader ir = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(ir);

        // On va travailler sur la variable monCompte
        CompteBancaire monCompte = new CompteBancaire();

        // Commencer par demander les valeurs des champs du compte
        System.out.print("Nom du titulaire = ");
        System.out.flush();
        try {
            monCompte.nom = br.readLine();
        } catch (IOException ioe) {}
        System.out.print("Son adresse = ");
        System.out.flush();
        try {
            monCompte.adresse = br.readLine();
        } catch (IOException ioe) {}
        System.out.print("Numéro du compte = ");
        System.out.flush();
        String reponse = "";
        try {
            reponse = br.readLine();
        } catch (IOException ioe) {}
        monCompte.numéro = Integer.parseInt(reponse);
        // Solde initial à 0
        monCompte.solde = 0;
        System.out.println(monCompte.nom + 
                           ", le solde initial de votre compte numéro " +
                           monCompte.numéro +
                           " est de " + monCompte.solde + " euros.");
        
        boolean fin = false;     // variable vraie si on veut s'arrêter
        while(true) {  // boucle infinie dont on sort par un break
            System.out.print("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
            String choix = "";
            try {
                choix = br.readLine();
            } catch (IOException ioe) {}

            boolean credit = false;   // variable vraie si c'est un crédit

            // Récupérer la première lettre de la chaîne saisie
            char monChoix = choix.charAt(0);
            switch(monChoix) {
            case 'C':
            case 'c':       // Même chose pour majuscule et minuscule
                credit = true; 
                fin = false;
                break;     // Pour ne pas continuer en séquence
            case 'd':
            case 'D':
                credit = false;
                fin = false;
                break;
            case 'f':
            case 'F':
                fin = true;
                break;
            default:
                fin = true;  // On va considérer que par défaut on s'arrête
            }

            if (fin) {
                break;    // sortir de la boucle ici
            }
            else {
                System.out.print("Montant à " + 
                                 (credit ? "créditer" : "débiter") + 
                                 " = ");
                System.out.flush();
                String lecture = "";
                try {
                    lecture = br.readLine();
                } catch (IOException ioe) {}

                int montant = Integer.parseInt(lecture);  // conversion en int

                if (credit) {
                    monCompte.solde += montant;
                }
                else {
                    monCompte.solde -= montant;
                }
                System.out.println(monCompte.nom + 
                                   ", le nouveau solde de votre compte numéro " +
                                   monCompte.numéro +
                                   " est de " + monCompte.solde + " euros.");
            }
        }
    }
}
Et voici un résultat d'exécution :
Nom du titulaire = Dominique Scapin
Son adresse = 1, rue des gros sous, 75002 Paris
Numéro du compte = 182456
Dominique Scapin, le solde initial de votre compte numéro 182456 est de 0 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Montant à créditer = 876234
Dominique Scapin, le nouveau solde de votre compte numéro 182456 est de 876234 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Montant à débiter = 542
Dominique Scapin, le nouveau solde de votre compte numéro 182456 est de 875692 euros.
Votre choix : [D]ébit, [C]rédit, [F]in ? F

Exercice 3   Définir des classes qui permettent de représenter :

3.2  Fonctions et procédures

3.2.1  Les fonctions -- approche intuitive

Vous avez probablement remarqué que chaque fois que nous effectuons une lecture d'un entier, nous sommes obligés d'écrire plusieurs lignes de code, toujours à peu près les mêmes, ce qui finit par alourdir le programme. Nous sommes typiquement en présence du besoin de structuration des instructions : l'opération de lecture au clavier d'un entier, par exemple, serait bien plus à sa place dans une fonction de lecture qu'en ligne.

Java permet d'écrire de telles fonctions, qui prennent éventuellement des paramètres en entrée -- ici nous choisirons la question à poser -- et qui rendent un résultat -- ici un entier.

Si vous avez bien suivi le dernier exemple que nous avons déroulé, vous avez probablement remarqué en plus que pour lire un entier, il faut d'abord lire une chaîne de caractères, puis convertir la chaîne résultat de cette opération de lecture en un entier. On peut donc écrire la fonction de lecture d'un entier à l'aide de la fonction de lecture d'une chaîne de caractères.

Où les mettre ?

Nous avons déjà vu qu'en Java, tout doit être défini au sein d'une classe. Se pose alors la question de la classe dans laquelle définir ces fonctions de lecture. On pourrait tout simplement les ajouter à la classe Banque ; mais elles sont d'un caractère assez général pour qu'elles puissent aussi servir dans d'autres contextes que la gestion d'un compte bancaire. Nous choisissons donc de définir une nouvelle classe -- notre troisième -- que nous appelons Utils et dans laquelle nous regrouperons, au fur et à mesure que nous progressons, les fonctions utilitaires qui peuvent servir dans nos programmes. Voici donc une première version de cette classe, définie -- faut-il le rappeler ? -- dans le fichier Utils.java :
// Classe Utils - version 1.0

/**
 *  Classe regroupant les utilitaires disponibles pour les exercices
 * de 1ere année du tronc commun informatique.
 * @author Karl Tombre
 */

import java.io.*;

public class Utils {
    // Les fonctions lireChaine et lireEntier
    // Exemple d'utilisation :
    // String nom = Utils.lireChaine("Entrez votre nom : ");
    public static String lireChaine(String question) {
        InputStreamReader ir = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(ir);
        
        System.out.print(question);
        System.out.flush();
        String reponse = "";
        try {
            reponse = br.readLine();
        } catch (IOException ioe) {}
        return reponse;
    }

    // La lecture d'un entier n'est qu'un parseInt de plus !!
    public static int lireEntier(String question) {
        return Integer.parseInt(lireChaine(question));
    }
}
Continuons pour l'instant à prendre comme un acquis la syntaxe public static ; nous y reviendrons pour en donner l'explication (cf. § 4.3).

Vous remarquerez que la fonction lireChaine prend en entrée une variable de type String, à savoir la question que l'on souhaite poser, et rend une variable de type String également, à savoir la réponse donnée par l'utilisateur à la question posée. Vous remarquerez aussi que la fonction lireEntier s'écrit par un simple appel à la fonction précédente, lireChaine, suivi d'un appel à une fonction de conversion d'une chaîne en un entier. Vous avez donc noté que les fonctions que nous avons définies incluent elles-mêmes des appels à d'autres fonctions définies dans les bibliothèque standard de Java, telles que Integer.parseInt justement. Enfin, vous noterez l'emploi du mot clé return, pour indiquer la valeur qui est renvoyée comme résultat de la fonction.

Reprenons maintenant notre programme bancaire et notons comment il se simplifie grâce à l'emploi des fonctions. Vous noterez entre autres que les variables rébarbatives de type BufferedReader et autres n'ont plus lieu d'être déclarées dans ce programme, puisqu'elles sont propres aux fonctions de lecture. Notez aussi l'emploi dans ce programme de la notation pointée Utils.lireEntier, pour indiquer dans quelle classe il faut aller chercher la fonction lireEntier :
// Classe Banque - version 2.1

/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * cette fois-ci une variable de type CompteBancaire
 * @author Karl Tombre
 * @see    CompteBancaire
 */

public class Banque {
    public static void main(String[] args) {
        // On va travailler sur la variable monCompte
        CompteBancaire monCompte = new CompteBancaire();

        // Commencer par demander les valeurs des champs du compte
        monCompte.nom = Utils.lireChaine("Nom du titulaire = ");
        monCompte.adresse = Utils.lireChaine("Son adresse = ");
        monCompte.numéro = Utils.lireEntier("Numéro du compte = ");

        // Solde initial à 0
        monCompte.solde = 0;
        System.out.println(monCompte.nom + 
                           ", le solde initial de votre compte numéro " +
                           monCompte.numéro +
                           " est de " + monCompte.solde + " euros.");
        
        boolean fin = false;     // variable vraie si on veut s'arrêter
        while(true) {  // boucle infinie dont on sort par un break
            String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
            boolean credit = false;   // variable vraie si c'est un crédit

            // Récupérer la première lettre de la chaîne saisie
            char monChoix = choix.charAt(0);
            switch(monChoix) {
            case 'C':
            case 'c':       // Même chose pour majuscule et minuscule
                credit = true; 
                fin = false;
                break;     // Pour ne pas continuer en séquence
            case 'd':
            case 'D':
                credit = false;
                fin = false;
                break;
            case 'f':
            case 'F':
                fin = true;
                break;
            default:
                fin = true;  // On va considérer que par défaut on s'arrête
            }

            if (fin) {
                break;    // sortir de la boucle ici
            }
            else {
                int montant = Utils.lireEntier("Montant à " + 
                                               (credit ? "créditer" : "débiter") + 
                                               " = ");
                if (credit) {
                    monCompte.solde += montant;
                }
                else {
                    monCompte.solde -= montant;
                }
                System.out.println(monCompte.nom + 
                                   ", le nouveau solde de votre compte numéro " +
                                   monCompte.numéro +
                                   " est de " + monCompte.solde + " euros.");
            }
        }
    }
}

3.2.2  Les fonctions -- définition plus formelle

La notion de fonction rejoint en fait la fonction telle qu'elle est connue en mathématiques. Nous avons déjà vu les opérateurs du langage (§ 2.4), qui définissent en fait des fonctions intrinsèques ; par exemple + sur les entiers correspond à la fonction2
+ : Z × Z ® Z

Plus généralement, une fonction func qui serait définie mathématiquement par :
func : Z × Bool × Char ® R
aura comme définition en Java :
static double func(int x, boolean b, char c)
Cela s'étend à tous les types, aussi bien ceux qui sont prédéfinis que ceux qui sont définis dans une bibliothèque ou par le programmeur.

Le passage des paramètres

Prenons un autre exemple : la fonction puissance entière d'un réel pourra s'écrire par exemple :
static double puiss(double x, int n) {
    /* Il manque ici un test de n positif */
    double res = 1.0;
    int p = 0;
    while (p < n) {
        p++;
        res *= x;
    }
    return res;
}
Que se passe-t-il alors lors de l'appel de cette fonction ? Le cas le plus simple est celui de l'utilisation de constantes ; on peut par exemple écrire :
double x = puiss(5.4, 6);
ce qui va attribuer à x la valeur calculée de 5,46. Mais on pourrait aussi écrire :
double r = Utils.lireReel("Donnez un nombre réel = ");
int p = Utils.lireEntier("Donnez un nombre entier = ");
double y = puiss(r, p);
en supposant bien entendu que nous ayons écrit également la fonction lireReel pour lire au clavier une valeur réelle.

Dans ces deux cas, il est important de noter comment les valeurs sont passées à la fonction. Dans la définition de celle-ci, x et n sont appelés les paramètres formels de la fonction. L'appel puiss(5.4, 6) revient à affecter à x la valeur 5.4 et à n la valeur 6, avant d'exécuter la fonction. De même, l'appel puiss(r, p) revient à faire les affectations implicites
x    (de la fonction)    ¬    r    (du monde extérieur à la fonction)
et
n    (de la fonction)    ¬    p    (du monde extérieur à la fonction)

Il faut bien noter que cette affectation implicite revient à dire qu'on passe la r-valeur des paramètres avec lesquels on appelle la fonction -- ici r et p -- et que ceux-ci ne sont donc pas modifiés par la fonction, qui ne "récupère" qu'une copie de leur r-valeur (cf. § 2.5). On dit que le passage des paramètres se fait par valeur, et en Java c'est le seule mode de passage des paramètres3.

Une fois que la fonction a rendu une valeur au "monde extérieur" (à celui qui l'a appelée), les paramètres x et n de la fonction n'ont plus aucune existence -- ils n'en ont qu'au sein de la fonction.

3.2.3  Les procédures

La fonction correspond à une définition mathématique précise ; mais il arrive aussi souvent qu'on soit amené à effectuer à plusieurs endroits du programme une même opération, définie par un même ensemble d'instructions. On souhaite là aussi regrouper ces instructions et leur donner un nom. Une fois de plus, il est possible que ce regroupement d'instructions reçoive un ou plusieurs paramètres, mais à la différence d'une fonction, il ne "rend" aucune valeur particulière.

On appelle un tel regroupement une procédure. En Java, les procédures sont définies suivant la même syntaxe que les fonctions ; on se contente d'utiliser un mot clé particulier, void, à la place du type rendu par la fonction. On peut donc dire par souci de simplicité, même si c'est en fait un abus de langage, qu'en Java, une procédure est une fonction qui ne retourne rien...

Continuons de simplifier notre programme bancaire. Plusieurs informations sont maintenant associées au compte bancaire, et chaque fois que l'on souhaite afficher son solde, il faut énumérer ces informations. Nous allons regrouper toutes les instructions nécessaires pour afficher l'état d'un compte bancaire dans une procédure étatCompte, à laquelle nous passerons en paramètre un compte bancaire. Nous en profitons pour améliorer l'affichage de cet état :
// Classe Banque - version 2.2

/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * une variable de type CompteBancaire
 * @author Karl Tombre
 * @see    CompteBancaire, Utils
 */

public class Banque {
    public static void étatCompte(CompteBancaire unCompte) {
        System.out.println("Compte numéro " + unCompte.numéro + 
                           " ouvert au nom de " + unCompte.nom);
        System.out.println("Adresse du titulaire : " + unCompte.adresse);
        System.out.println("Le solde actuel du compte est de " +
                           unCompte.solde + " euros.");
        System.out.println("************************************************");
    }

    public static void main(String[] args) {
        CompteBancaire monCompte = new CompteBancaire();

        // Commencer par demander les valeurs des champs du compte
        monCompte.nom = Utils.lireChaine("Nom du titulaire = ");
        monCompte.adresse = Utils.lireChaine("Son adresse = ");
        monCompte.numéro = Utils.lireEntier("Numéro du compte = ");

        // Solde initial à 0
        monCompte.solde = 0;

        // Afficher une première fois
        étatCompte(monCompte);
        
        boolean fin = false;     // variable vraie si on veut s'arrêter
        while(true) {  // boucle infinie dont on sort par un break
            String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
            boolean credit = false;   // variable vraie si c'est un crédit

            // Récupérer la première lettre de la chaîne saisie
            char monChoix = choix.charAt(0);
            switch(monChoix) {
            case 'C':
            case 'c':       // Même chose pour majuscule et minuscule
                credit = true; 
                fin = false;
                break;     // Pour ne pas continuer en séquence
            case 'd':
            case 'D':
                credit = false;
                fin = false;
                break;
            case 'f':
            case 'F':
                fin = true;
                break;
            default:
                fin = true;  // On va considérer que par défaut on s'arrête
            }

            if (fin) {
                break;    // sortir de la boucle ici
            }
            else {
                int montant = Utils.lireEntier("Montant à " + 
                                               (credit ? "créditer" : "débiter") + 
                                               " = ");
                if (credit) {
                    monCompte.solde += montant;
                }
                else {
                    monCompte.solde -= montant;
                }
                // Afficher le nouveau solde
                étatCompte(monCompte);
            }
        }
    }
}
Une exécution de cette nouvelle version du programme done :
Nom du titulaire = Facture Portails
Son adresse = 165 rue M*crosoft, Seattle (WA), USA
Numéro du compte = 2000
Compte numéro 2000 ouvert au nom de Facture Portails
Adresse du titulaire : 165 rue M*crosoft, Seattle (WA), USA
Le solde actuel du compte est de 0 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Montant à créditer = 189000
Compte numéro 2000 ouvert au nom de Facture Portails
Adresse du titulaire : 165 rue M*crosoft, Seattle (WA), USA
Le solde actuel du compte est de 189000 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Montant à débiter = 654
Compte numéro 2000 ouvert au nom de Facture Portails
Adresse du titulaire : 165 rue M*crosoft, Seattle (WA), USA
Le solde actuel du compte est de 188346 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? F

3.2.4  Le cas particulier de main

Depuis le début, nous écrivons public static void main(String[] args) en début de programme. Il est temps de l'expliquer. En fait, un programme est une succession d'appels à des procédures et des fonctions. Il faut donc bien un "commencement", c'est-à-dire une procédure qui appelle les autres, qui est à l'origine de l'enchaînement des appels de procédures et de fonctions. Cette procédure particulière s'appelle toujours main. Elle prend en paramètre un tableau de chaînes de caractères, qui correspond à des arguments avec lesquels on peut éventuellement lancer le programme. Nous n'utiliserons jamais cette faculté dans ce cours -- ce qui signifie que ce tableau d'arguments est vide dans notre cas -- mais nous sommes bien obligés de donner la déclaration exacte de main pour que le système s'y retrouve...

3.2.5  Surcharge

En Java, rien n'interdit de définir plusieurs fonctions et procédures portant le même nom, à condition que leurs signatures -- c'est-à-dire le nombre et le type de leurs paramètres formels -- soient différentes. Ainsi, on pourra définir (par exemple dans la classe Utils) toutes les fonctions et procédures suivantes, de nom max :
public static int max(int a, int b) {...}
public static float max(char a, char b) {...}
public static double max(double a, double b) {...}
public static void max(char c, int y) {...}
public static void max(String s, int y) {...}
public static char max(String s) {...}
À noter toutefois que Java interdit de définir deux fonctions ayant la même signature mais des types de résultat différents, comme par exemple
public static int max(int a, int b) {...}
public static String max(int a, int b) {...}
Le type des paramètres à l'appel et la signature permettent au compilateur de déterminer laquelle de ces fonctions ou procédures doit être choisie lors de l'appel. Ainsi max(3, 4) va appeler la première fonction, alors que max("Bonjour", 3) va provoquer l'appel de la procédure de l'avant-dernière ligne.

Attention cependant à ne pas abuser de cette faculté ; pour des raisons de lisibilité par vous-même et par d'autres humains, et pour éviter les confusions, il vaut mieux n'utiliser la surcharge que dans les cas où les différentes fonctions ou procédures de même nom correspondent à la même opération sémantique, mais appliquée à des types différents.
Exercice 4   Écrire en Java les 3 fonctions dont les définitions mathématiques sont les suivantes :
max : Z × Z ® Z                  
  (n,m) |® n    si    n > m                  
  (n,m) |® m    sinon                  
f : Z ® Z                  
  n |® 2n + 3n                  
se : R+ ® N                  
  x |® ë x û                  

3.3  Les tableaux

Le tableau est une structure de données de base que l'on retrouve dans beaucoup de langages. Il permet de stocker en mémoire un ensemble de valeurs de même type, et d'y accéder directement.

À tout type T de Java correspond un type T[ ] qui indique un tableau d'éléments de type T. Pour créer un tableau, on fera une fois de plus appel à l'instruction new, en précisant cette fois-ci le nombre d'éléments que doit contenir le tableau. Ainsi, on écrira :
byte[] octetBuffer = new byte[1024];
pour définir un tableau de 1024 entiers codés sur un octet4.

On accède à un élément du tableau en donnant son indice, sachant que l'indice du premier élément d'un tableau est toujours 0 en Java ! Ainsi octetBuffer[0] désigne le premier élément, octetBuffer[1] le deuxième, et octetBuffer[1023] le dernier. Par ailleurs, la taille d'un tableau est donnée par la construction nomDuTableau.length : ainsi, dans l'exemple ci-dessus, octetBuffer.length vaut 1024.

Attention ! Si vous déclarez un tableau d'objets, c'est-à-dire de variables déclarées d'un type défini par une classe, il ne faut pas oublier de créer les objets eux-mêmes ! Imaginons que nous voulions gérer un tableau de 10 comptes bancaires. En écrivant :
CompteBancaire[] tabComptes = new CompteBancaire[10];
on crée un tableau de 10 références à des comptes bancaires. La variable tabComptes[0], par exemple, est de type CompteBancaire, mais si je veux qu'elle désigne un objet effectif et qu'elle ne vaille pas null, je dois également écrire :
tabComptes[0] = new CompteBancaire();
comme nous l'avons vu précédemment.

Illustrons tout cela en faisant une fois de plus évoluer notre célèbre programme bancaire. Nous allons maintenant justement gérer 10 comptes dans un tableau. Il faut donc commencer par initialiser ces 10 comptes. Ensuite, le programme est modifié en demandant l'indice dans le tableau avant de demander le montant à créditer ou débiter. Notez que je devrais en fait vérifier que l'indice saisi est bien dans les bornes valides, c'est-à-dire dans l'intervalle [0,10[.

En prime, cet exemple me donne l'occasion d'illustrer l'emploi d'une itération avec la construction for :
// Classe Banque - version 2.3

/**
 * Classe contenant un programme de gestion bancaire, utilisant
 * un tableau de comptes bancaires
 * @author Karl Tombre
 * @see    CompteBancaire, Utils
 */

public class Banque {
    public static void étatCompte(CompteBancaire unCompte) {
        System.out.println("Compte numéro " + unCompte.numéro + 
                           " ouvert au nom de " + unCompte.nom);
        System.out.println("Adresse du titulaire : " + unCompte.adresse);
        System.out.println("Le solde actuel du compte est de " +
                           unCompte.solde + " euros.");
        System.out.println("************************************************");
    }

    public static void main(String[] args) {
        CompteBancaire[] tabComptes = new CompteBancaire[10];

        // Initialisation des comptes
        for (int i = 0 ; i < tabComptes.length ; i++) {
            // D'abord créer le compte !!
            tabComptes[i] = new CompteBancaire();
            // Et puis lire les données
            tabComptes[i].nom = Utils.lireChaine("Nom du titulaire = ");
            tabComptes[i].adresse = Utils.lireChaine("Son adresse = ");
            tabComptes[i].numéro = Utils.lireEntier("Numéro du compte = ");
            // Solde initial à 0
            tabComptes[i].solde = 0;
        }

        boolean fin = false;     // variable vraie si on veut s'arrêter
        while(true) {  // boucle infinie dont on sort par un break
            String choix = Utils.lireChaine("Votre choix : [D]ébit, [C]rédit, [F]in ? ");
            boolean credit = false;   // variable vraie si c'est un crédit

            // Récupérer la première lettre de la chaîne saisie
            char monChoix = choix.charAt(0);
            switch(monChoix) {
            case 'C':
            case 'c':       // Même chose pour majuscule et minuscule
                credit = true; 
                fin = false;
                break;     // Pour ne pas continuer en séquence
            case 'd':
            case 'D':
                credit = false;
                fin = false;
                break;
            case 'f':
            case 'F':
                fin = true;
                break;
            default:
                fin = true;  // On va considérer que par défaut on s'arrête
            }

            if (fin) {
                break;    // sortir de la boucle ici
            }
            else {
                int indice = Utils.lireEntier("Indice dans le tableau des comptes = ");
                int montant = Utils.lireEntier("Montant à " + 
                                               (credit ? "créditer" : "débiter") + 
                                               " = ");
                if (credit) {
                    tabComptes[indice].solde += montant;
                }
                else {
                    tabComptes[indice].solde -= montant;
                }
                // Afficher le nouveau solde
                étatCompte(tabComptes[indice]);
            }
        }
    }
}
Une exécution de ce programme donne la trace suivante :
Nom du titulaire = Karl
Son adresse = Sornéville
Numéro du compte = 123

\emph{... Je vous passe un certain nombre de lignes - c'est fastidieux}

Nom du titulaire = Bart
Son adresse = Bruxelles
Numéro du compte = 76243
Votre choix : [D]ébit, [C]rédit, [F]in ? D
Indice dans le tableau des comptes = 4
Montant à débiter = 8725
Compte numéro 524 ouvert au nom de François
Adresse du titulaire : Strasbourg
Le solde actuel du compte est de -8725 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Indice dans le tableau des comptes = 9
Montant à créditer = 8753
Compte numéro 76243 ouvert au nom de Bart
Adresse du titulaire : Bruxelles
Le solde actuel du compte est de 8753 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? C
Indice dans le tableau des comptes = 0
Montant à créditer = 7652
Compte numéro 123 ouvert au nom de Karl
Adresse du titulaire : Sornéville
Le solde actuel du compte est de 7652 euros.
************************************************
Votre choix : [D]ébit, [C]rédit, [F]in ? F
NB : Habituellement, on n'initialise pas un tableau au moment de sa création, mais on le "remplit" comme dans l'exemple que nous venons de donner. Cependant, il y a des cas où l'on souhaite initialiser un tableau ; on donnera alors entre accolades la liste des valeurs initiales, séparées par des virgules, comme l'illustre l'exemple suivant, où l'on définit un tableau de taille 12, initialisé directement par 12 valeurs (la taille du tableau est déduite par le compilateur du nombre de valeurs données en initialisation) :
  static String[] moisEnFrançais = {"janvier", "février", "mars", "avril",
                                    "mai", "juin", "juillet", "août",
                                    "septembre", "octobre", "novembre",
                                    "décembre"};

Exercice 5   Écrire les fonctions :


1
En fait, vous comprendrez au chapitre 4 que la classe est bien plus que cela, puisque c'est l'élément de base de la programmation objet. Mais nous nous contenterons dans un premier temps de cette vision simplifiée de la classe, qui la rend analogue à un RECORD en Pascal, par exemple.
2
En toute rigueur, du fait du codage des entiers sur un ensemble fini de bits, on opère en Java sur une partie finie de Z.
3
Dans d'autres langages, on a parfois le choix entre passage par valeur -- passage de la r-valeur -- et passage par référence. Dans ce deuxième cas, on passe en fait la l-valeur du paramètre d'appel à la fonction, et celle-ci peut donc agir directement sur la variable passée en paramètre, et non seulement sur ses propres variables, qui désignent les paramètres formels.
4
En fait, on peut aussi écrire byte octetBuffer[] ; c'est-à-dire mettre les crochets après le nom de la variable et non après le type. Si vous êtes amené plus tard à "jongler" entre les langages C++ et Java, vous préférerez sans doute cette seconde solution, pour éviter la dyslexie, car c'est la seule notation autorisée en C++. Néanmoins, je conseille plutôt d'utiliser la notation donnée ici, qui a le mérite de désigner clairement la variable octetBuffer comme étant de type byte[], c'est-à-dire tableau d'octets.

Précédent Remonter Suivant