Capsule : Opération et méthode

Introduction

Comme nous l’avons déjà vu, un type de données définit un ensemble de valeurs et un ensemble d’opérations qui peuvent être appliquées à ces valeurs. Nous avons également vu qu’avec un langage orienté objet, une classe peut être utilisée pour réaliser un module qui regroupe un ensemble de sous-programmes ou pour réaliser une structure de donnée.

Dans les sections qui suivent, ce document montre d’abord que les notions d’opération et de fonction sont très proches et peuvent être utilisées de manière interchangeable, puis la manière dont les deux aspects d’une classe – classe en tant que module et de classe en tant que structure de données – se combinent et permettent la réalisation de véritables types.

Opération et fonction

Une opération peut être définie comme un processus qui produit une valeur à partir d’une ou plusieurs valeurs appelées opérandes et qui est dénoté par un symbole appelé opérateur.

Cette définition est très proche de celle d’une fonction. En fait, une opération peut être considérée comme un cas particulier de fonction, les opérandes correspondant aux paramètres. On peut donc écrire n’importe quelle opération à l’aide d’une fonction.

En Java, en plus des opérations prédéfinies par le langage, diverses opérations sont définies dans des classes de la bibliothèque standard (Java Class Library ou JCL). Par exemple, la classe Math est un module qui regroupe des fonctions qui s’appliquent à des nombres, la classe Character est un module qui regroupe des fonctions qui s’appliquent à des valeurs de type char et la classe Arrays est un module qui regroupe des fonctions et des procédures qui s’appliquent à des tableaux.

Ces opérations implémentées sous la forme de fonctions et de procédures s’appliquent presque exclusivement à des valeurs dont le type est prédéfini, c’est-à-dire un type primitif ou un tableau. Pour les classes, les opérations sont implémentées sous forme de méthodes.

Notion de méthode

Les opérations qui s’appliquent à des objets, c’est-à-dire à des valeurs dont le type est une classe, ne sont généralement ni prédéfinies (à l’exception de l’opérateur de concaténation pour les chaînes de caractères), ni implémentées sous la forme de fonctions et de procédures comme celle que nous avons étudiée jusqu’ici. Les opérations qui s’appliquent à des objets sont implémentées sous la forme de fonctions et de procédures particulières appelées méthode (method).

Sans le savoir, vous avez tous déjà utilisé des méthodes. Par exemple, l’instruction suivante affiche un texte dans la sortie standard :

1
System.out.println("Un texte");

Dans cette instruction, la variable globale System.out référence un objet de type PrintStream et println est l’une des méthodes de la classe PrintStream, c’est-à-dire l’une des opérations que l’on peut appliquer à un objet de type PrintStream. On dit que la méthode println est appliquée à l’objet référencé par la variable System.out avec la chaîne "Un texte" passée en paramètre.

Avant d’aller plus loin, voyons encore un exemple qui illustre la similitude entre une opération prédéfinie comme l’addition, une fonction et une méthode. Considérons pour cela le code de la figure 1.

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

        // « Instancie » une valeur de type int à l’aide du littéral 2.
        int myInt = 2;

        // Additionne la valeur 4 à la valeur 2. L’opération d’addition 
        // est dénotée par le symbole +. La valeur du premier opérande (la 
        // valeur de la variable a) est la valeur à laquelle s’applique 
        // l’opération. La valeur du second opérande, 4, est la quantité que 
        // l’opération ajoute au premier opérande.
        int sum = myInt + 4;

        // Élève la valeur de la variable a à la puissance 3. L’opération
        // « élève à la puissance » est implémentée par la fonction
        // Math.pow. La valeur du premier paramètre est la valeur à
        // laquelle s’applique l’opération et celle du second paramètre
        // est la puissance.
        double res = Math.pow(myInt, 3);

        // Instancie un objet de type chaîne de chaîne de caractères avec
        // le littéral de chaîne "un texte".
        String myStr = "un texte";

        // Renvoie le caractère qui se trouve à la position 1 dans la
        // chaîne de caractères référencée par myStr. L’opération
        // « renvoie le caractère qui se trouve à la position » est
        // implémentée par la méthode charAt de la classe String. L’objet
        // référencé par la variable myStr est la valeur sur laquelle
        // s’applique l’opération et la valeur du paramètre de la méthode
        // est la position du caractère à renvoyer.
        char c = myStr.charAt(1);

        ...
Fig. 1 – Appliquer une fonction et appliquer une méthode

Les opérations des lignes 11, 22 et 31 peuvent se lire de manière similaire :

  • À la ligne 11 : l’opération + est appliquée à la valeur de la variable myInt avec la valeur 4 comme paramètre et produit la valeur 6 qui est affectée à la variable sum.
  • À la ligne 22 : l’opération Math.pow est appliquée à la valeur de la variable myInt avec la valeur 3 comme paramètre et produit la valeur 8 qui est affectée à la variable res.
  • À la ligne 31 : l’opération charAt est appliquée à l’objet référencé par la variable myStr avec la valeur 1 comme paramètre et produit la valeur 'U' qui est affectée à la variable c.

Méthode d’instance et méthode de classe

Dans la terminologie de l’orienté objet, on appelle méthode n’importe quel sous-programme défini dans une classe. En Java, puisqu’il n’est possible de définir un sous-programme que dans une classe et nulle part ailleurs, tous les sous-programmes sont des méthodes.

Toutefois, on voit bien une différence entre l’appel de fonction Math.pow(2, 3)Math est le nom d’une classe et l’appel de fonction myStr.charAt(1)myStr est le nom d’une variable de type String.

Dans le premier cas, on parle de méthode de classe ou de méthode statique, définie avec le mot clé static (toutes les procédures et les fonctions que nous avons réalisé jusqu’ici sont des méthodes de classe). Dans le second cas, on pale de méthode d’instance. Dans ce contexte, le mot « instance » est synonyme du mot « objet ». Dire « une instance de la classe Xyz » est équivalent à dire « un objet de type Xyz ».

Il existe donc deux sortes de méthodes :

  • Méthode de classe (<nom de classe>.<nom de méthode>) : méthode définie AVEC le mot clé static que l’on appelle sur le nom de la classe dans laquelle elle est définie.
  • Méthode d’instance (<nom de variable>.<nom de méthode>) : méthode définie SANS le mot clé static que l’on appelle sur le nom d’une variable qui référence une instance de la classe dans laquelle elle est définie.

Définir des méthodes d’instance

Reprenons l’exemple de la classe Customer. Au lieu de définir les opérations de ce type dans un module séparé comme nous l’avions fait, on peut maintenant définir les opérations dans la classe elle-même comme méthode d’instance.

Procédons en plus étape. Dans un premier temps, ajoutons l’opération getFullName comme méthode statique de la Customer comme le montre le code de la figure 2.

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
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 = "";
    } 

    /**
     * Renvoie le nom complet du client passé 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);
    }
}
Fig. 2 – Étape 1 : Opération getFullName comme méthode statique de la classe Customer

Remarquez que l’on a laissé le mot clé static. Il s’agit donc d’une méthode de classe. Pour utiliser cette opération, on doit donc l’appeler sur le nom de la classe dans laquelle elle est définie. Cette classe est maintenant la classe Customer. Le code de la figure 3 montre comment on pourrait utiliser cette méthode.

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

        ...
        // Crée une nouvelle instance de la classe Customer.
        Customer myCustomer = new Customer("James", "Gosling");

        // Appelle la méthode statique getFullname
        String fullname = Customer.getFullName(myCustomer);
        ...
}
Fig. 3 – Utilisation de la méthode statique getFullName.

La deuxième étape consiste à supprimer le mot clé static pour en faire une méthode d’instance, mais sans rien changer d’autre (figure 4).

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
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 = "";
    } 

    /**
     * Renvoie le nom complet du client passé en paramètre.
     * 
     * @param p un objet de type Customer
     * @return le nom complet (firstname lastname)
     */
    public String getFullName(Customer p) {
        return String.format("%s %s", p.firstname, p.lastname);
    }
}
Fig. 4 – Étape 2 : Opération getFullName comme méthode d'instance de la classe Customer

Puisqu’il s’agit maintenant d’une méthode d’instance, on ne peut plus appeler cette méthode sur le nom de la classe, mais on doit l’appeler sur le nom d’une variable de type Customer. Le code de la figure 5 illustre l’appel de la méthode getFullName sur la variable myCustomer.

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

        ...
        // Crée une nouvelle instance de la classe Customer.
        Customer myCustomer = new Customer("James", "Gosling");

        // Appelle de la méthode d'instance getFullname sur 
        // la variable myCustomer (avec paramètre)
        String fullname = myCustomer.getFullName(myCustomer);
        ...
}
Fig. 5 – Utilisation de la méthode d'instance getFullName.

Dans ce code, on remarque immédiatement une redondance : le nom de la variable myCustomer est utilisé à la fois pour appeler la méthode et comme paramètre de la méthode. En fait, cette redondance est inutile. En effet, une méthode d’instance a toujours un paramètre caché. Ce paramètre caché est une référence à l’objet sur lequel la méthode est appliquée et est accessible par le mot clé this. On peut donc modifier la définition de la méthode en supprimant le paramètre p et en remplaçant les occurrences de ce paramètre par le mot clé this (figure 6).

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
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 = "";
    } 

    /**
     * Renvoie le nom complet du client.
     * 
     * @return le nom complet (firstname lastname)
     */
    public String getFullName() {
        return String.format("%s %s", this.firstname, this.lastname);
    }
}
Fig. 6 – Étape 3 : Suppression du paramètre de la méthode getFullName

On peut maintenant appeler la méthode getFullName sans argument. La valeur renvoyée est produite à l’aide des valeurs des variables membres de l’objet auquel la méthode est appliquée, c’est-à-dire l’objet référencé par la variable sur laquelle on appelle la méthode.

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

        ...
        // Crée une nouvelle instance de la classe Customer.
        Customer myCustomer = new Customer("James", "Gosling");

        // Appelle de la méthode d'instance getFullname sur 
        // la variable myCustomer (sans paramètre)
        String fullname = myCustomer.getFullName();
        ...
}
Fig. 7 – Utilisation de la méthode d'instance getFullName.

En résumé, pour définir une méthode d’instance, on définit une méthode sans le mot clé static. Une méthode d’instance a un paramètre caché accessible par le mot clé this qui représente l’instance sur laquelle la méthode est appliquée.

Uniformité de l’accès et masquage d’information

Il reste à aborder deux principes importants de la programmation orientée objet. Le premier est le principe d’uniformité de l’accès (uniform access principle) et le second, le principe de masquage de l’information (information hidding).

Pour illustrer en quoi consiste le premier principe, observons le code de la figure 8. Les lignes 9 et 13 sont toutes deux des affectations dont la partie droite est une expression dont la valeur provient de l’objet référencé par la variable myCustomer.

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

        ...
        // Crée une nouvelle instance de la classe Customer.
        Customer myCustomer = new Customer("James", "Gosling");

        // Affecte la valeur de la variable membre lastname
        // à la variable locale lastname.
        String lastname = myCustomer.lastname;

        // Affecte la valeur renvoyée par la méthode getFullName
        // à la variable fullname.
        String fullname = myCustomer.getFullName();
        ...
}
Fig. 8 – Différence de syntaxe entre l'accès à une variable membre et l'appel d'une méthode.

Toutefois, on remarque que la syntaxe qui permet d’obtenir le nom (lastname) et la syntaxe qui permet d’obtenir le nom complet (fullname) ne sont pas identiques. En effet, dans le premier cas, il s’agit de la valeur d’une variable membre alors que dans le second, il s’agit d’une valeur calculée produite par une méthode. Cependant, rien n’interdirait que le nom complet soit également maintenu dans une variable membre. C’est une décision qui appartient au concepteur de la classe et qui ne regarde en rien l’utilisateur. Le principe d’uniformité de l’accès stipule qu’il ne doit pas y avoir de différence entre l’accès à une valeur calculée ou l’accès une valeur d’une variable membre.

En Java, cela se traduit par deux règles qu’il faut observer de manière stricte :

  • On déclare toujours une variable membre avec le mot clé private.
  • On accède à une variable membre uniquement à l’aide d’une méthode.

On appelle accesseur (getter) une méthode qui sert à obtenir la valeur d’une variable membre et on appelle mutateur (setter) une méthode qui sert à affecter une valeur à une variable membre. Les termes anglais getter et setter sont d’usage plus courant et le terme accesseur est parfois utilisé pour désigner indifféremment un getter ou un setter. En Java, le nom d’un getter commence toujours par get suivi du nom de la variable membre et le nom d’un setter commence toujours par set suivi du nom de la variable membre.

La figure 9 montre une implémentation de la classe Customer avec des setters et des getters.

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package ch.epai.ict.m404.activity5 ;

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

    // Constructeur (constructor)
    public Customer(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.street = "";
        this.city = "";
        this.postalCode = "";
    } 

    /**
     * Renvoie le prénom du client
     * 
     * @return le prénom du client
     */
    public String getFirstname() {
        return this.firstname;
    }

    /**
     * Renvoie le nom du client
     * 
     * @return le nom du client
     */
    public String getLastname() {
        return this.lastname;
    }

    /**
     * Renvoie le nom de la rue du client
     * 
     * @return le nom de la rue du client
     */
    public String getStreet() {
        return this.street;
    }

    /**
     * Modifie le nom de la rue du client
     * 
     * @param value le nom de la rue du client
     */
    public void setStreet(String value) {
        if (value != null) {
            this.street = value;
        } else {
            this.street = "";
        }
    }

    /**
     * Renvoie le nom de la ville du client
     * 
     * @return le nom de la ville du client
     */
    public String getCity() {
        return this.city;
    }

    /**
     * Modifie le nom de la ville du client
     * 
     *  @param value le nom de la ville du client
     */
    public void setCity(String value) {
        if (value != null) {
            this.city = value;
        } else {
            this.city = "";
        }
    }

    /**
     * Renvoie le code postal du client
     * 
     * @return le code postal du client
     */
    public String getPostalCode() {
        return this.postalCode;
    }

    /**
     * Modifie le code postal du client
     * 
     *  @param value le code postal du client
     */
    public void setPostalCode(String value) {
        if (value != null) {
            this.postalCode = value;
        } else {
            this.postalCode = "";
        }
    }

    /**
     * Renvoie le nom complet du client.
     * 
     * @return le nom complet (firstname lastname)
     */
    public String getFullName() {
        return String.format("%s %s", this.firstname, this.lastname);
    }
}
Fig. 9 – Classe Customer avec des accesseurs et des mutateurs

Pour finir, le principe de masquage de l’information stipule qu’un type ne doit pas exposer sa structure interne et ne doit exposer que les méthodes strictement nécessaires. La première partie est respectée dès lors que l’on respecte la règle que l’on s’est imposée de toujours déclarer les variables membres avec le mot clé private. La seconde partie est liée aux principes de conception SOLID qui sont abordés dans le module 226.