Capsule : Type simple et type composé

Introduction

Le but d’un programme informatique est d’effectuer des traitements sur des données qui représentent des entités du monde réel. Ces entités peuvent être, par exemple, des véhicules d’une agence de location, des articles dans un commerce de détail, des clients d’une boutique en ligne ou des relations dans un réseau social.

Dans le code source du programme, ces données sont représentées par des variables. Comme vous le savez, une variable peut être définie comme l’association d’un nom et d’une valeur. De plus, certains langages (les langages à typage statique), dont Java, exige que le type de données d’une variable soit spécifié lors de sa déclaration. Un type de données (ou simplement type) est un mot du langage de programmation qui décrit le genre de valeurs qui peuvent être affectées à la variable. On en distingue les types simples et les types composés.

Les types simples sont utilisés pour des données telles que des nombres, des valeurs booléennes ou des chaînes de caractères. Ce sont des types d’usage très général qui sont prédéfinis par le langage de programmation. Ils correspondent souvent aux données qui peuvent être traitées directement par le processeur.

Les types composés sont utilisé pour des données plus complexes qui ne peuvent pas être décrites par une seule valeur de type simple. Deux cas de figure peuvent se présenter :

  • la donnée est une collection de valeurs de même type (p. ex. une liste de clients)
  • la donnée est composée de plusieurs valeurs de différents types (p. ex. la fiche d’un client qui contient des éléments d’information tels que numéro de client, nom, prénom, adresse, etc.)

Dans le premier cas, on utilise un tableau, dans le second cas, on utilise une structure. Une structure décrit un enregistrement (record) dans le même sens qu’un enregistrement dans une base de données relationnelle (les valeurs d’une ligne d’une table). Avec un langage orientés objet les structures sont réalisées avec des classes.

Les sections qui suivent abordent chacune de ces notions de manière plus détaillée, les types simples d’abord, puis les types composés avec les types tableau et les types enregistrement.

Types simples

Tous les langages disposent d’un certain nombre de types prédéfinis qui correspondent à des valeurs élémentaires telles que :

  • nombres entiers
  • nombres à virgule
  • valeur booléenne
  • chaînes de caractères

Les types « entiers » représentent des sous-ensembles plus ou moins étendus de l’ensemble des entiers naturels (non signé) ou de l’ensemble des entiers relatifs (signés). En Java, quatre types correspondent à des sous-ensembles de nombre entier relatif : byte (8 bits), short (16 bits), int (32 bits) et long (64 bits). Le type char (16 bits) représente l’ensemble des codes UTF-16.

Les type « virgule flottante » sont une manière de représenter des ensembles de nombres réels qui offre un compromis entre l’étendue et la précision (voir encadré). Presque tous les langages supportent deux types de nombres à virgule flottante correspondant aux formats IEEE 754 simples précision (32 bits) et double précision (64 bits). En Java, ces types sont, respectivement, float et double.

Le type booléen (boolean) permet de représenter les valeurs booléennes « vrai » et « faux » (true et false). En Java, la manière dont sont codées ces valeurs n’est pas précisément spécifiée et il n’est pas possible de convertir une valeur de type booléen en une valeur numérique.

Enfin, même s’il est un peu particulier, le type « chaîne de caractères » peut généralement être considéré comme un type simple.

En Java, les types byte, short, int, long, char, float, double et boolean sont des types particuliers appelés types primitifs. Le type String, quant à lui, n’est pas un type primitif, mais une classe un peu spéciale. En effet, les variable de ce type sont généralement initialisée par des valeurs littérale et non et non avec l’opération d’instanciation.

Types composés

Types tableaux

En programmation, une variable de type tableau est un moyen rudimentaire de représenter une collection homogène, c’est-à-dire une collection dont tous les éléments sont de même nature. Par exemple, une liste de clients ou une liste d’articles.

Un tableau est une structure de données qui consiste en un ensemble d’éléments de même type accessible par leur indice (index). La taille d’un tableau est généralement fixe. Pour étendre ou réduire sa taille, on doit allouer un nouveau tableau et copier les valeurs. En Java, la classe Arrays contient des fonctions et des procédures qui facilitent ces opérations.

L’instanciation (création) d’un tableau peut se faire de deux manières :

  • avec une valeur littérale (uniquement dans la définition d’une variable),
  • avec l’opérateur new.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
        ...
        // Définis (déclare et initialise) un tableau de valeurs 
        // de type int à une dimension en utilisant un littéral.
        int[] anArrayOfInt = { 1, 2, 3 };

        // Définis un tableau de valeurs de type int à deux
        // dimension en utilisant un littéral.
        int[][] aMatrixOfInt = { { 1, 2, 3 }, {3, 4, 5} };

        // Définis un tableau de valeurs de type double à une
        // dimension de 4 éléments en utilisant l’opération 
        // d’instanciation.
        double[] arrayOfDouble = new double[4];

        // Déclare un tableau de valeurs de type long
        // à deux dimensions.
        long[][] arrayOfLong;

        // Instancie un tableau de 4 x 6 éléments et affecte
        // sa référence à la variable arrayOfLong.
        arrayOfLong = new long[4][6]
        ...
Fig. 1 – Instanciation de tableaux

Les tableaux sont généralement des structures de données de bas niveau. En Java, l’utilisation d’un tableau n’est justifiée que dans le cas où le type des éléments est un type primitif. Dans tous les autres cas, on préfère recourir à une instance de la classe ArrayList. Cette dernière est une implémentation du type abstrait List qui utilise un tableau pour stocker les éléments et qui prend en charge le redimensionnement du tableau en cas de besoin.

Types enregistrements

Les entités du monde réel ne peuvent généralement pas être représentées par un seul nombre ou une seule chaîne de caractères. Par exemple, pour représenter un client dans une boutique en ligne, on a besoin d’avoir au moins un numéro de client, un nom, un prénom et une adresse elle-même composée d’une rue, d’une ville et d’un numéro postal. Ces éléments d’information sont hétérogènes, c’est-à-dire qu’il ne sont pas tous de même nature. En effet, même si le nom d’une personne et le nom d’une rue peuvent tous deux être décrits avec une chaîne de caractères, un nom de personne n’est pas un nom de rue.

Pour représenter de telles entités, les langages de programmation permettent de définir des types appelés enregistrement (record) ou structure. Le terme « enregistrement » doit être compris dans le même sens que dans le contexte des bases de données. Lorsqu’un type est défini par un programmeur pour une application particulière, on dit qu’il s’agit d’un type défini par l’utilisateur (user-defined type), le programmeur étant l’utilisateur du langage de programmation.

Dans les langages orientés objet, ces types peuvent être définis à l’aide de classes. Le code de la figure 2 montre la réalisation d’un type Customer avec une classe Java.

1
2
3
4
5
6
7
8
9
10
package ch.epai.ict.m404. ... ;

public class Customer {
    public int customerNber;
    public String firstname;
    public String lastname;
    public String street;
    public String city;
    public String postalCode;
}
Fig. 2 – Définition d’un type Customer à l’aide d’une classe.

Les composants du type (firstname, lastname, etc.) sont appelés champs (fields), attributs ou variables membres (member variables). L’opération d’instanciation (opérateur new) permet de créer une nouvelle valeur de ce type, ou en termes plus techniques, d’un nouvel objet de ce type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package ch.epai.ict.m404. ... ;

public class App {

    public static void main(String[] args) {

        // Déclare une variable de type Customer.
        // Ici l’identificateur Customer est le nom du type.
        Customer p1;

        // Instancie un nouvel objet de type Customer. 
        // Ici l’identificateur Customer est le nom du constructeur.
        // Les variables membres de l’objet renvoyé par l’opération
        // d’instanciation ont toutes la valeur null.
        // Le type de la variable p1 est `Customer`, un un type 
        // référence. La valeur de p1 est donc une référence à 
        // l'objet nouvellement créé.
        p1 = new Customer();

        // Affecte des valeurs aux variables membres.
        p1.firstname = "James";
        p1.lastname = "Gosling";
        p1.street = "Sand Hill Road";
        p1.city = "Menlo Park";
        p1.postalCode = "CA 94025";
        
        // Déclare une nouvelle variable de type Customer et
        // initialise sa valeur avec celle de la variable p1.
        // Comme la valeur de p1 est une référence à un objet,
        // la valeur de p2 est une référence à ce même objet.
        // Les variables p1 et p2 référencent un seul et même
        // objet. Seule l’opération d’instanciation (opérateur
        // new) permet de créer (instancier) un nouvel objet.
        Customer p2 = p1;

        // Affiche le texte "Nom : James Gosling"
        System.out.printf("Nom : %s %s%n", p2.firstname, p2.lastname);
    }
}
Fig. 3 – Utilisation du type Customer

La variable p1 est une variable de type Customer, et Customer comme tous les types composés (tableaux et classes) est un type référence (voir encadré), c’est-à-dire que la valeur de p1 n’est pas l’objet lui-même, mais une référence à un objet. Si l’on affecte la valeur de cette variable à une autre variable, cette autre variable devient un alias de la première, les deux référencent un seul et même objet. Seul l’opération d’instanciation permets de créer un nouvel objet.

À la ligne 18 de la figure 3, l’expression new Customer() est un exemple d’opération d’instanciation. Dans cette expression new est l’opérateur d’instanciation et Customer() est le constructeur par défaut de la classe Customer.

Un constructeur est un sous-programme particulier qui porte le même nom que la classe, qui n’a pas de type (même pas void) et dont le rôle est d’initialiser les variables membres lors de l’instanciation d’un nouvel objet avec l’opérateur new. Lorsque l’on ne définit pas de constructeur explicitement, Java crée un constructeur par défaut sans paramètre. Ce constructeur initialise les variables membres de type numérique avec un 0 et les variables membres de type booléen avec la valeur false. Les variables membres dont le type est un type référence sont initialisées avec la valeur null.

La valeur null indique qu’une variable ne référence aucun objet. Si l’on essaie d’accéder à un élément du tableau ou à un membre de l’objet qu’elle est censée référencer, le compilateur ne dit rien, puisque la syntaxe est correcte, mais l’exécution du code produit une erreur. Ce type d’erreur est appelée erreur d’exécution (runtime error). L’un des moyens de se prémunir contre ce type d’erreur est d’adopter un style de programmation défensif qui consiste à s’assurer que la valeur de la référence n’est pas null avant d’accéder à un élément ou à un membre ou de s’assurer que la valeur de cette référence ne peut pas être null.

C’est dans ce but qu’il est recommandé de définir explicitement un constructeur pour initialiser les variables membres. Le code de la figure 4 montre la définition de la classe Customer avec un constructeur qui prend deux paramètres : un pour le nom et un pour le prénom. Les deux paramètres servent à initialiser les variables membres firstname et lastname. Les autres variables membres sont initialisées avec des chaînes vides. Les paramètres ne doivent pas nécessairement correspondre à des variables membres, on peut définir les paramètres que l’on veut.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package ch.epai.ict.m404. ... ;

public class Customer {
    // Variables membres (field members)
    public String firstname;
    public String lastname;
    public String street;
    public String city;
    public String postalCode;

    // Constructeur (constructor)
    public Customer(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.street = "";
        this.city = "";
        this.postalCode = "";
    } 
}
Fig. 4 – Constructeur pour le type Customer

La figure 5 montre un exemple d’utilisation du type Customer avec le nouveau constructeur. Le mot clé this est une variable particulière qui représente l’objet créé par l’opérateur new, et dont il faut initialiser les variables membres. Par rapport à la version précédente, l’avantage de ce code réside dans le fait qu’aucune variable membre de type référence n’a la valeur null, elles référencent toutes un objet existant.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package ch.epai.ict.m404. ... ;

public class App {

    public static void main(String[] args) {

        // Déclare une variable de type Customer.
        // Ici l’identificateur Customer est le nom du type.
        Customer p1;

        // Instancie un nouvel objet de type Customer avec le
        // nouveau constructeur. L’opération d’instanciation
        // renvoie un objet complètement initialisé.
        p1 = new Customer("James", "Gosling");

        // Utilise une fonction pour créer une chaîne qui
        // contient le nom complet de la Customer.
        System.out.printf("Nom : %s %s%n", p2.firstname, p2.lastname);
    }
}
Fig. 5 – Utilisation du type Customer avec le nouveau constructeur

Un type composé peut être utilisé comme n’importe quel autre type. On peut bien sûr l’utiliser comme type d’une variable locale, mais aussi comme type d’une variable membre, comme type d’une fonction, ou comme type d’un paramètre d’un sous-programme. La figure 6 montre un exemple de procédure et un exemple de fonction qui utilisent le type Customer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package ch.epai.ict.m404. ... ;

public class CustomerOperations {

    /**
     * Renvoie le nom complet du client passée en paramètre.
     * 
     * @param p un objet de type Customer
     * @return le nom complet (firstname lastname)
     */
    public static String getFullName(Customer p) {
        return String.format("%s %s", p.firstname, p.lastname);
    }

    /**
     * Modifie l’adresse de l’objet passé en paramètre.
     * 
     * @param p          un objet de type Customer
     * @param street     le nom de la rue de la nouvelle adresse
     * @param city       le nom de la ville de la nouvelle adresse
     * @param postalCode le code postal de la nouvelle adresse
     */
    public static void changeAddress(Customer p, String street, String city, String postalCode) {
        p.street = street;
        p.city = city;
        p.postalCode = postalCode;
    }

    /**
     * Renvoie une copie (un clone) de l’objet passé en paramètre.
     * 
     * @param p un objet de type Customer
     * @return une copy de l’objet de type Customer
     */
    public static Customer clone(Customer p) {
        Customer clone = new Customer(p.firstname, p.lastname);

        clone.street = p.street;
        clone.city = p.city;
        clone.postalCode = p.postalCode;

        return clone;
    }
}
Fig. 6 – Procédure et fonctions d'appliquant au type Customer

La procédure changeAddress modifie l’objet de type Customer passé en paramètre. Cette opération est possible, car la valeur qui est passée en paramètre n’est pas l’objet de type Customer lui-même, mais une référence à cet objet. Comme dans le cas des tableaux, l’objet référencé par le paramètre formel est le même que l’objet référencé par la variable dont la valeur a été passée en paramètre.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package ch.epai.ict.m404. ... ;

public class App {

    public static void main(String[] args) {

        Customer p1 = new Customer("James", "Gosling");

        CustomerOperation.changeAddress(p1,
                "Sand Hill Road",
                "Menlo Park", "CA 94025")

        String fullname = CustomerOperation.getFullName(p1)

    }
}
Fig. 7 – Utilisation de la classe CustomerOperations