Redéfinition de méthode

Introduction

Comme nous l’avons vu dans une théorie précédente, il est possible de préciser dans une classe qu’une méthode existera dans un objet, mais de laisser aux descendants de cette classe la tâche de préciser l’implémentation de la méthode en question. C’est ce que nous appelons une méthode abstraite. Par contre, un autre cas de figure est possible. Il s’agit du cas ou la classe a une implémentation d’une méthode précise, mais qu’un des descendants de cette classe nécessite une autre implémentation. Cette mécanique est possible et se nomme redéfinition.

Les redéfinitions

Supposons que vous avez une structure d’héritage contenant une classe parent et plusieurs classes enfants. Initialement, toutes les classes enfants ont une méthode en commun (même implémentation). Il est donc tout à fait logique de placer cette méthode dans la classe parent. Par contre, imaginez qu’on ajoute une nouvelle classe enfant et que, contrairement aux autres enfants, la méthode ait une implémentation différente.

Une solution pourrait être de replacer la méthode dans tous les enfants; mais cette solution implique beaucoup de duplication de code inutile. Une autre solution consisterait à laisser la méthode dans la classe parent et préciser, dans la nouvelle classe enfant, que la méthode est différente que celle du parent. C’est ce que nous appelons une redéfinition.

Prendre note qu’une autre solution aurait pu être de créer une classe intermédiaire entre le parent et les classes enfants d’origines. Cette technique fonctionnerait bien, mais je ne vais pas en tenir compte ici et me concentrer sur la redéfinition.

Pour redéfinir une méthode, il suffit de la définir à nouveau dans la classe enfant. Voici un exemple. Je pars encore une fois de la structure d’héritage que nous avons développé dans les dernières théories:

Supposons que je veux ajouter une méthode « deplacement » dans la classe Animal. Je peux l’implémenté comme ceci:

/**
 * Représente tous les spécimens animal.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, mercredi mar 10, 2021 16:58:57 EST
 * 
 * Distribué sous licence MIT.
 */

public abstract class Animal {

    ...

    /**
     * Effectue un déplacement de l'Animal.
     */
    public void deplacement(){
        System.out.println("L'animal commence à marcher.");
    }

    ...

}

On voit que cette méthode s’applique très bien aux animaux que j’ai déjà dans mon système. D’ailleur, les classes Chat et Chien resteront inchangée. Par contre, disons que je doivent ajouter un nouvel animal dans ma structure d’héritage: Poisson. On voit ici qu’un Poisson ne marche pas. Il nage. Nous allons donc devoir redéfinir la méthode « deplacement ». Voici la classe Poisson avec cette redéfinition.

/**
 * Animal qui vie dans l'eau.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, mercredi mar 10, 2021 17:03:49 EST
 * 
 * Distribué sous licence MIT.
 */

public class Poisson extends Animal{
	
    /**
     * Le son qu'émet l'Animal
     */
    public void cri() {
        System.out.println("Bloup!");
    }

    /**
     * Effectue un déplacement de l'Animal.
     */
    public void deplacement(){
        System.out.println("L'animal commence à nager.");
    }

    /**
     * Constructeur du Poisson
     *
     * @param aNom Valeur de nom
     * @param aAge Valeur de age
     */
    public Poisson(String aNom, int aAge) {
        super(aNom, aAge);
    }

}

Si nous utilisons le programme suivant:

/**
 * Programme principal de l'exemple des animaux.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, mercredi mar 10, 2021 17:14:16 EST
 * 
 * Distribué sous licence MIT.
 */

public class Programme {

    /**
     * Exécution du programme.
     *
     * @param aArguments Les paramêtres du programme.
     */
    public static void main(String[] aArguments) {
        Chien lChien = new Chien("Rex", 10);
        Chat lChat = new Chat("Minou", 3);
        Poisson lPoisson = new Poisson("Pachon", 1);

        lChien.deplacement();
        lChat.deplacement();
        lPoisson.deplacement();

    }
}

Nous obtiendrons le résultat suivant:

L'animal commence à marcher.
L'animal commence à marcher.
L'animal commence à nager.

On voit tout de suite que la redéfinition a fonctionné pour le poisson.

Un petit problème

Il est important de remarquer ici que, malgré la mécanique assez facile de la redéfinition en java, c’est une mécanique qui peut facilement générer des erreurs qui seront difficiles à déboguer par après.

Là où la redéfinition peut-être dangereuse est dans le cas où la classe parent ferait une modification à la signature de la méthode. Si par exemple je décidais de renommer ma méthode « deplacement » pour « avancer » comme ceci:

/**
 * Représente tous les spécimens animal.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, mercredi mar 10, 2021 16:58:57 EST
 * 
 * Distribué sous licence MIT.
 */

public abstract class Animal {

    ...

    /**
     * Effectue un déplacement de l'Animal.
     */
    public void avancer(){
        System.out.println("L'animal commence à marcher.");
    }

    ...

}

Le lien client/fournisseur que la classe Programme (le main) a avec Animal ne causerait aucun problème puisque lors de la compilation, une erreur m’indiquerait que je dois modifier mes appels à « deplacement » pour les remplacer par des appels à « avancer » (en fait, la compilation donnerait des erreurs de type « cannot find symbol » en spécifiant que le symbole manquant est « deplacement »).

Par contre, si j’oublie de modifier ma méthode « deplacement » dans Poisson, je me retrouverai avec une classe Poisson contenant deux méthodes différentes « deplacement » (déclarée dans Poisson) et « avancer » (déclarée dans Animal). Puisqu’il s’agit d’une syntaxe tout à fait valide, le compilateur de Java ne m’informera jamais de l’oublie et mon programme n’effectuera le bon travail. Par exemple, si je ne modifie pas ma classe poisson et que je lance le programme suivant:

/**
 * Programme principal de l'exemple des animaux.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, mercredi mar 10, 2021 17:14:16 EST
 * 
 * Distribué sous licence MIT.
 */

public class Programme {

    /**
     * Exécution du programme.
     *
     * @param aArguments Les paramêtres du programme.
     */
    public static void main(String[] aArguments) {
        Chien lChien = new Chien("Rex", 10);
        Chat lChat = new Chat("Minou", 3);
        Poisson lPoisson = new Poisson("Pachon", 1);

        lChien.avancer();
        lChat.avancer();
        lPoisson.avancer();

    }
}

J’obtiendrai le résultat:

L'animal commence à marcher.
L'animal commence à marcher.
L'animal commence à marcher.

On voit tout de suite que le Poisson s’est mis à marcher, ce qui n’est pas correct dans mon système.

Pour nous assurer d’éviter ce type d’oublie qui mène à des erreurs, nous allons utiliser la syntaxe « @Override » avant la signature de la redéfinition. Par exemple, si j’avais créé ma classe Poisson de cette manière:

/**
 * Animal qui vie dans l'eau.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, mercredi mar 10, 2021 17:03:49 EST
 * 
 * Distribué sous licence MIT.
 */

public class Poisson extends Animal{

    /**
     * Le son qu'émet l'Animal
     */
    public void cri() {
        System.out.println("Bloup!");
    }

    /**
     * Effectue un déplacement de l'Animal.
     */
    @Override
    public void deplacement(){
        System.out.println("L'animal commence à nager.");
    }

    /**
     * Constructeur du Poisson
     *
     * @param aNom Valeur de nom
     * @param aAge Valeur de age
     */
    public Poisson(String aNom, int aAge) {
        super(aNom, aAge);
    }

}

Le compilateur m’aurait informé à l’instant que la méthode redéfinie n’est plus présente dans le parent. Le programme recompilera seulement si je renomme la méthode « deplacement » dans Poisson (ou si je remets la méthode « deplacement » dans le parent). Voici la classe Poisson après avoir renommé la méthode redéfinie:

/**
 * Animal qui vie dans l'eau.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, mercredi mar 10, 2021 17:03:49 EST
 * 
 * Distribué sous licence MIT.
 */

public class Poisson extends Animal{

    /**
     * Le son qu'émet l'Animal
     */
    public void cri() {
        System.out.println("Bloup!");
    }

    /**
     * Effectue un déplacement de l'Animal.
     */
    public void avancer(){
        System.out.println("L'animal commence à nager.");
    }

    /**
     * Constructeur du Poisson
     *
     * @param aNom Valeur de nom
     * @param aAge Valeur de age
     */
    public Poisson(String aNom, int aAge) {
        super(aNom, aAge);
    }

}

Il est à noter que l’indicateur « @Override » doit toujours être inscrit lors d’une redéfinition de méthode.

Utilisation de la méthode redéfinie du parent

De manière similaire à l’utilisation du « super » pour lancer les constructeurs du parent à partir des constructeurs de l’enfant, il est possible de lancer la méthode redéfinie du parent à partir de la méthode de l’enfant. Pour se faire, nous utilisons encore le mot clé « super », mais en spécifiant quelle méthode du parent doit être lancée.

Voici un exemple, si je considère qu’après avoir commencer à marcher, je veux que le Chien renifle tout ce qu’il croise. Je pourrais donc redéfinir la méthode « avancer » dans Chien.java de cette manière:

/**
 * Animal de race canine.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, mercredi mar 10, 2021 17:03:49 EST
 * 
 * Distribué sous licence MIT.
 */

public class Chien extends Animal{

    /**
     * Le son qu'émet l'Animal
     */
    public void cri() {
        System.out.println("Wouf Wouf!");
    }

    /**
     * Constructeur du Chien
     *
     * @param aNom Valeur de nom
     * @param aAge Valeur de age
     */
    public Chien(String aNom, int aAge) {
        super(aNom, aAge);
    }

    /**
     * Effectue un déplacement de l'Animal.
     */
    @Override
    public void avancer(){
        super.avancer();
        System.out.println("L'animal renifle partout où il passe.");
    }

}

Si j’exécute le code suivant:

Chien lChien = new Chien("Rex", 10);
lChien.avancer();

J’obtiens donc le résultat:

L'animal commence à marcher.
L'animal renifle partout où il passe.

Je peux mettre le « super » n’importe où dans le code de la méthode redéfinie. Par exemple, si je veux que le Chat s’étire avant de commencer à marcher, je pourrais mettre la méthode redéfinie suivante dans Chat.java:

    /**
     * Effectue un déplacement de l'Animal.
     */
    @Override
    public void avancer(){
        System.out.println("L'animal s'étire.");
        super.avancer();
    }

Si j’exécute le code suivant:

Chat lChat = new Chat("Minou", 10);
lChat.avancer();

J’obtiens le résultat:

L'animal s'étire.
L'animal commence à marcher.

Redéfinir les méthodes de la classe Object

Comme nous l’avons indiqué précédemment, toutes les classes, peu importe qu’on leur spécifie un lien d’héritage ou non, héritent de la classe Object. D’ailleurs, il est facile de s’en rendre compte avec un petit exemple comme ceci:

public void compareAnimaux(Animal aAnimal1, Animal aAnimal2) {
    if (aAnimal1.equals(aAnimal2)){
        System.out.println("Il s'agit du même animal.");
    }
}

Cette méthode compile sans problème malgré que la classe Animal ne contient pas de méthode « equals ». Par contre, puisque Animal hérite de Object et que la classe Object contient une méthode « equals« , il est possible d’utiliser cette méthode sur un objet de type Animal.

Il peut être utile de redéfinir les méthodes de la classe Object afin de les rendre plus utiles et plus conformes à la réalité. Les méthodes qui sont souvent utiles à redéfinir sont les méthodes « equals » , « clone » et « toString » .

Je vais donner des exemples de redéfinition de ces méthodes dans les sections suivantes.

Redéfinir « equals »  

La méthode de « equals » permet de savoir si deux objets sont logiquement équivalents. Par défaut, cette méthode retourne la même valeur que l’instruction « == ». Par contre, en redéfinissant « equals » , nous pouvons préciser les conditions qui font que logiquement deux objets sont logiquement équivalents.

C’est la raison pour laquelle dans le code suivant:

String lChaine1 = new String("Allo");
String lChaine2 = new String("Allo");
System.out.println(lChaine1 == lChaine2);
System.out.println(lChaine1.equals(lChaine2));

Le premier « println » retourne « False » et le second « println » retourne « True ». La classe String a redéfini la méthode « equals » afin de retourner « True » si les caractères contenus dans la chaîne sont les mêmes.

Supposons la classe suivante:

/**
 * Le résultat d'un achat.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, vendredi avr 09, 2021 10:45:11 EDT
 * 
 * Distribué sous licence MIT.
 */

public class Facture {

    /**
     * Le numéro unique de la Facture.
     */
    private int numero;

    /**
     * Le nom du client ayant fait l'achat.
     */
    private String client;

    /**
     * Ce que l'achat a couté au client (en sous).
     */
    private int montant;

    /**
     * Constructeur de Facture
     *
     * @param aNumero Valeur de numero.
     * @param aClient Valeur de client.
     * @param aMontant Valeur de montant.
     */
    public Facture(int aNumero, String aClient, int aMontant) {
        numero = aNumero;
        client = aClient;
        montant = aMontant;
    }

}

On obtient que le code suivant retourne « False »:

Facture lFacture1 = new Facture(1, "Louis", 1000);
Facture lFacture2 = new Facture(1, "Louis", 1000);
System.out.println(lFacture1.equals(lFacture2));

Ce résultat est normal parce que « lFacture1 » et « lFacture2 » sont deux objets distincts. Par contre, supposons que nous voudrions que ce code retourne « True » puisque les valeurs des objets sont identiques, je pourrais redéfinir la méthode « equals » comme ceci:

/**
 * Le résultat d'un achat.
 * 
 * @author Louis Marchand (prog@tioui.com)
 * @version 0.1, vendredi avr 09, 2021 10:45:11 EDT
 * 
 * Distribué sous licence MIT.
 */

public class Facture {
	
    /**
     * Le numéro unique de la Facture.
     */
    private int numero;

    /**
     * Le nom du client ayant fait l'achat.
     */
    private String client;

    /**
     * Ce que l'achat a couté au client (en sous).
     */
    private int montant;

    /**
     * Retourne vrai si aAutre est équivalent à la Facture.
     *
     * @param aAutre La facture à comparer.
     * @return Vrai si la aAutre est équivalent à la Facture.
     */
    @Override
    public boolean equals(Object aAutre) {
        boolean lResultat = false;
        if (aAutre instanceof Facture) {
            Facture lAutre = (Facture)aAutre;
            lResultat = 
                numero == lAutre.numero &&
                client.equals(lAutre.client) &&
                montant == lAutre.montant;
        }
        return lResultat;
    }

    /**
     * Constructeur de Facture
     *
     * @param aNumero Valeur de numero.
     * @param aClient Valeur de client.
     * @param aMontant Valeur de montant.
     */
    public Facture(int aNumero, String aClient, int aMontant) {
        numero = aNumero;
        client = aClient;
        montant = aMontant;
    }

}

En effet, si nous exécutons à nouveau ce code:

Facture lFacture1 = new Facture(1, "Louis", 1000);
Facture lFacture2 = new Facture(1, "Louis", 1000);
System.out.println(lFacture1.equals(lFacture2));

Pour que la méthode soit bel et bien une redéfinition, il faut également s’assurer que l’argument reçu est de type « Object » et non du même type que la classe qui redéfinit le « equals » (dans mon cas, Facture). Donc, en général, la première chose que nous faisons est de s’assurer que l’objet reçu en argument est du bon type (avec un test de type) et d’utiliser un « cast » pour accéder aux attributs de l’argument (voir la théorie sur le Polymorphisme pour plus de détails).

Note: Il est à noter que normalement, lorsque la méthode « equals » est redéfinie, il faudrait également redéfinir la méthode « hashCode » (puisque deux objets égaux devraient avoir le même « hashCode »). Mais, nous garderons la théorie sur les « hashCode » pour un autre cours du programme.

Nous obtenons maintenant « True ».

Redéfinir « clone » 

La méthode « clone » permet de faire une copie d’un objet. Prenons par exemple le code suivant:

ArrayList<String> lListe1 = new ArrayList<String> (5);
lListe1.add("1");
lListe1.add("2");
lListe1.add("3");
lListe1.add("4");
lListe1.add("5");
List<String> lListe2 = (List<String>)lListe1.clone();
System.out.println(lListe1 == lListe2);
System.out.println(lListe1.equals(lListe2));

Le premier « println » retourne « False » puisqu’il s’agit de deux objets distincts. Le second « println » montre que les deux listes contiennent les mêmes éléments. On voit donc que le « clone » a effectivement créé une nouvelle liste « lListe2 » contenant les mêmes éléments que la liste « lListe1 ».

Pour bien comprendre ce code, il est important de comprendre que la méthode « clone » dans la classe Object retourne une valeur de type Object et non du type de la variable ayant effectué le « clone » (ArrayList dans mon exemple). Il est donc nécessaire d’effectuer un « cast » de type pour préciser au langage de quel type l’objet est en réalité (voir la théorie sur le polymorphisme pour plus de détails).

Voici la méthode « clone » redéfinie dans la classe Facture:

    /**
     * Retourne une copie de l'objet.
     *
     * @return Une copie de la facture.
     */
    @Override
    public Object clone(){
        return new Facture(numero, client, montant);
    }

Redéfinition de la méthode « toString » 

La méthode « toString » permet de retourner une représentation texte de l’objet. Par exemple, prenons le code suivant:

Facture lFacture1 = new Facture(1, "Louis", 1023);
System.out.println(lFacture1);

En exécutant le programme, j’obtiens le résultat:

Facture@3764951d

On voit que ça ne représente rien du tout. En fait, ça représente le type de l’objet ainsi que son pointeur mémoire.

En redéfinissant la méthode « toString » , je peux préciser la forme que prendra la représentation texte de l’objet. Si par exemple, j’ajoute cette méthode dans la classe Facture:

/**
 * Retourne une chaîne représentant la Facture.
 *
 * @return Chaîne représentant la Facture.
 */
@Override
public String toString() {
    return
        "Facture " + numero + " pour le client " + client +
        " au montant de " + (montant / 100) + "." +
        (montant % 100) + "$";
}

et que je ré-exécute le programme présenté plus haut:

Facture lFacture1 = new Facture(1, "Louis", 1023);
System.out.println(lFacture1);

j’obtiens donc le résultat suivant:

Facture 1 pour le client Louis au montant de 10.23$

Retour

 


Auteur: Louis Marchand
Creative Commons License
Sauf pour les sections spécifiées autrement, ce travail est sous licence Creative Commons Attribution 4.0 International.