Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Mise en oeuvre du concept d'héritage

Introduction à la POO et principe d'encapsulation Définition de classes de type « record »


Accès rapide :
La vidéo
Introduction au concept d'héritage
Ce qu'il ne faut pas faire
Ce qu'il faut faire
Représentation UML de l'héritage
Et que vient faire la classe java.lang.Object dans l'histoire ?
Mise en oeuvre de l'héritage
L'utilisation du mot clé extends
On enrichit la classe avec ses spécificités
Définition de constructeur et utilisation du mot clé super
Assistance Eclipse à la production de constructeurs
Redéfinition de méthode
Emploi de l'annotation @Override
Appel d'une méthode de la classe parente, redéfinie par la classe fille
Le polymorphisme
Définition du polymorphisme
Utilisation de l'opérateur instanceof
Travaux pratiques
Le sujet
La correction
Interdire l'héritage ou la redéfinition de méthode
Interdire la dérivation d'une classe
Interdire la redéfinition d'une méthode

La vidéo

Cette vidéo vous montre comment mettre en oeuvre le concept d'héritage en Java. Les principes de rappels des constructeurs, de polymorphisme ainsi que l'opérateur instanceof vous sont aussi présentés.


Mise en oeuvre du concept d'héritage

Introduction au concept d'héritage

Nous allons débuter ce chapitre en reprenant la correction proposée lors des travaux pratiques du précédent chapitre : il s'agissait de définir une classe permettant de manipuler des personnes. Nous avions choisi de considérer quatre attributs (un identifiant, un prénom, un nom et un email) ainsi que les constructeurs et propriétés (getters/setters) adaptés et nous avions finalisé cette classe en y ajouter une méthode toString. Voici le code que je vous avais proposé en correction.

 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 
package fr.koor.poo;

import java.util.regex.Pattern;

public class Person {

    // On définie une expression régulière compilé une fois pour toute.
    // Elle est partagée par toutes les instances de Person
    private static final Pattern EMAIL_PATTERN = Pattern.compile( 
            "^[\\w.-]+@[\\w.-]+[a-z]{2,}$" 
    );
    
    private int identifier;
    private String firstName;
    private String lastName;
    private String email;
    
    
    public Person() {
        this( 0, "john", "doe", "unknown@anywhere.unk" );
    }
       
    public Person( int identifier, String firstName, String lastName, String email ) {
        this.setIdentifier( identifier );
        this.setFirstName( firstName );
        this.setLastName( lastName );
        this.setEmail( email );
    }


    public int getIdentifier() {
        return identifier;
    }
    
    public void setIdentifier( int identifier ) {
        if ( identifier < 0 )  {
            throw new RuntimeException( "identifier must be positive" );
        }
        this.identifier = identifier;
    }
    
    public String getFirstName() {
        return firstName;
    }
    
    public void setFirstName( String firstName ) {
        if ( firstName == null ) {
            throw new NullPointerException( "firstName cannot be null" );
        }
        firstName = firstName.trim();   // Pour supprimer les blancs inutiles
        if ( firstName.equals( "" ) ) {
            throw new RuntimeException( "firstName cannot be empty" );
        }
        this.firstName = firstName.toLowerCase();
    }
    
    public String getLastName() {
        return lastName;
    }
    
    public void setLastName( String lastName ) {
        if ( lastName == null ) {
            throw new NullPointerException( "lastName cannot be null" );
        }
        lastName = lastName.trim();   // Pour supprimer les blancs inutiles
        if ( lastName.equals( "" ) ) {
            throw new RuntimeException( "lastName cannot be empty" );
        }
        this.lastName = lastName.toUpperCase();
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail( String email ) {
        if ( email == null ) {
            throw new NullPointerException( "email cannot be null" );
        }
        if ( ! EMAIL_PATTERN.matcher( email ).matches() ) {
            throw new RuntimeException( "email parameter not match with classical pattern" );
        }
        this.email = email;
    }
    
    @Override public String toString() {
        return String.format( "%d: %s %s @ %s",
                this.identifier,
                this.firstName,
                this.lastName,
                this.email );
    }
    
}
La classe Person

Imaginons maintenant que nous cherchions à ajouter à notre logiciel (un outil de gestion de contacts pour une entreprise) les concepts de clients (classe Client) et de salariés de l'entreprise (classe Employee). La question étant de savoir comment ajouter ces deux nouveaux types de données ?

Ce qu'il ne faut pas faire

Il ne faut pas copier les codes de la classe Person dans les classes Client et Employee puis compléter ces deux nouvelles classes avec les spécificités de chaque classe. En effet, si vous procédez ainsi, vous allez dupliquer des sections de code et si, d'aventure, les contraintes partagées par tous les types de personnes (Person, Client et Employee) changent, alors il sera nécessaire de modifier l'ensemble des fichiers de codes, avec le risque qu'un jour ces classes finissent par ne plus fonctionner de manière similaire.

De plus, si la classe Person contenait un ou plusieurs bugs, alors vous aurez dupliqué ces éventuels problèmes. De manière plus générale, il faut absolument éviter de dupliquer des sections de code dans un même programme.

Ce qu'il faut faire

Le mieux, vous vous en doutez, est de procéder par héritage. Cela veut dire que nous allons construire nos classes Client et Employee par-dessus la classe Client en complétant ces classes avec les éléments manquants. En quelques sortes, nos deux nouvelles classes vont être des extensions de notre classe de base et elles posséderont l'ensemble des éléments de la classe héritée.

je vous propose une petite astuce simple à mettre en oeuvre. Si vous arrivez à dire « est un » ou « est une » alors il est très fortement probable que l'héritage soit une bonne solution pour mettre en relation deux classes. Par exemple, je peux dire qu'un client est une personne : je pense que cela doit aussi vous paraître logique et donc je peux faire hériter la classe Client de la classe Person.

on peut aussi dire que la classe Client dérive de la classe Person. En programmation, c'est la même chose que de dire que la classe Client hérite de la classe Person.

Représentation UML de l'héritage

En programmation orientée objet, il existe un formalisme graphique permettant de représenter chaque concept « objet ». Ce langage graphique s'appelle UML (Unified Modeling Language) et il permet donc de représenter les liens d'héritages entre classes.

Une classe se représente, en UML, avec une boîte rectangulaire divisée en un, deux ou trois compartiments. Le premier compartiment, celui du haut, contient normalement le nom de la classe. Le second (facultatif) contient la liste des attributs et le dernier contient normalement les méthodes de la classe considérée.

Une relation d'héritage se représente avec une flèche avec une pointe triangulaire fermée (la précision au sujet de la flèche est importante car si la flèche n'est pas fermée alors elle ne représentera pas un lien d'héritage). La flèche doit partir de la classe dérivée et allez vers la classe de base. Voici un diagramme UML montrant les liens d'héritage entre nos différentes classes.

ce diagramme UML a été produit via un modeleur UML (un éditeur UML). Il en existe plusieurs. Le choix d'un modeleur ou d'un autre peut déprendre de nombreux points : prix, langage de programmation, processus de développement utilisé, ... Dans le cas présent, j'ai choisi Eclipse Papyrus. Il fait partie du projet Eclipse et a le mérite d'être gratuit et d'être intégré à notre IDE favoris ;-)
Vous pouvez l'installer à partir du menu « Help / Install new software... ». Néanmoins, notez que de tels outils ne sont pas si simples que ça à maitriser et qu'il faut un certain temps avant d'y parvenir.
les outils permettant de générer les diagrammes UML sont aussi capables de produire des squelettes de code pour vos classes à partir des diagrammes de classes. C'est aussi le cas d'Eclipse Papyrus.

Enfin, notez quelques points de terminologies complémentaires.

Et que vient faire la classe java.lang.Object dans l'histoire ?

Bonne question. En fait, çà longtemps que vous utilisez l'héritage sans le savoir. Effectivement, si vous codez une nouvelle classe, pourquoi pas appelée Truc, et que vous n'y fournissiez pas de méthode toString, que se passe-t-il si vous tentez d'afficher une instance produite à partir de cette classe sur la console ? Normalement, ça doit parfaitement compiler et on devrait voir sur la console quelque chose du genre fr.koor.poo.Truc@00000000B0C0A4, ou la valeur hexadécimale affichée à la droite du caractère @ représente l'adresse en mémoire de l'instance.

Mais où cette classe a-t-elle trouvé son toString ? La réponse est simple : elle l'a reçue de la classe java.lang.Object par héritage. Effectivement, si vous ne spécifiez rien, sur votre classe, en termes d'héritage alors elle héritera implicitement de la classe Object. Donc, toutes les classes que nous avons produites depuis le début de ce cours sur le langage Java mettent en jeu le concept d'héritage.

on pourrait donc revoir notre diagramme UML pour y faire apparaître la classe java.lang.Object. Pour autant, cela est rarement fait et tout le monde s'accordant à dire que ce lien d'héritage est sous-entendu.

Mise en oeuvre de l'héritage

L'utilisation du mot clé extends

En Java, pour mettre en oeuvre l'héritage on utilise le mot clé extends. Voici comment définir notre classe Client en la faisant hériter de Person :

 1 
 2 
 3 
 4 
package fr.koor.poo;
            
public class Client extends Person {
}
Utilisation du mot clé extends pour réaliser un héritage

Le simple fait d'avoir spécifié l'héritage lors de la déclaration de la classe Client garanti que nous avons déjà quatre propriétés et une méthode toString sur cette classe. Ces éléments ont été « hérités » de la classe parente. On peut donc lancer le test suivant.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {

          Client aClient = new Client();
          aClient.setIdentifier( 0 );
          aClient.setFirstName( "John" );
          aClient.setLastName( "Doe" );
          aClient.setEmail( "doe@unknown.com" );

          System.out.println( aClient );
                  
    }

}
Test du lien d'héritage

Et voici le résultat produit par cet exemple de code.

$> java Start
0: john DOE @ doe@unknown.com
$> 

Nous avons aussi hérité des quatre attributs. Si vous instanciez un client, vous réservez suffisamment de place en mémoire pour contenir chacun des attributs définis au niveau de la classe parente. Pour preuve, dans l'exemple ci-dessus, on affiche bien les données stockées dans notre client. Par contre, il vous sera impossible d'y accéder en direct. Effectivement, ces attributs sur définis comme étant privés (mot clé private) dans la classe Person. Donc seule cette classe Personpeut les manipuler directement sans passer par les propriétés (getter/setter).

Le code suivant ne compilera donc pas.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
package fr.koor.poo;

public class Client extends Person {

    public Client() {
        this.identifier = 0;
    }
    
}
Attention aux membres (attributs et méthodes) privés

Et voici le message d'erreur produit par le compilateur :

$> javac Start.java
fr/koor/poo/Client.java:6: error: identifier has private access in Person
        this.identifier = 0;
            ^
1 error
$> 

On enrichit la classe avec ses spécificités

Il faut maintenant ajouter les spécificités de la classe Client. Je vous propose d'y rajouter un seul attribut : le nom de l'entreprise dans lequel travaille notre client. Ce nom d'entreprise pourra valoir null, cela signifiant que le client n'est pas rattaché à une entreprise. Par contre cet attribut ne pourra pas valoir "" (chaîne vide). Un nom d'entreprise devra obligatoirement être stocké en majuscules. On est donc bien d'accord, il nous faudra aussi rajouter un getter et un setter. Voici une proposition de code.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
package fr.koor.poo;

public class Client extends Person {

    private String enterpriseName;
    
    
    public String getEnterpriseName() {
        return this.enterpriseName;
    }
    
    public void setEnterpriseName( String enterpriseName ) {
        if ( enterpriseName != null ) {
            if ( enterpriseName.equals( "" ) ) {
                throw new RuntimeException( "enterpriseName cannot be empty" );
            } else {
                enterpriseName = enterpriseName.toUpperCase();
            }
        }
        this.enterpriseName = enterpriseName;
    }

}
Attention aux membres (attributs et méthodes) privés

Définition de constructeur et utilisation du mot clé super

L'étape suivante consiste à rajouter un ou plusieurs constructeurs. Et là, une problématique apparait. Il va y avoir une chaîne d'appels entre certains constructeurs (ceux des classes Client, Person et Object). Effectivement, la première chose que doit faire un constructeur c'est de redonner la main au constructeur de la classe parente. Analysons le diagramme ci-dessous.

Donc la classe Client est basée sur trois classes. Du coup, elle est composée de trois paquets (trois ensembles) d'attributs : ceux définis dans Object, ceux définis dans Person et enfin, ceux définis dans Client. Chaque classe a normalement la responsabilité d'initialiser les attributs qu'elle définit. C'est pour cette raison que la première chose que fait (implicitement ou non) un constructeur, c'est de redonner la main au constructeur de la classe parente.

Dit autrement, si vous créez une instance de type Client, alors vous créez une personne et donc un objet Java. Donc pour initialiser un client, il faut commencer à initialiser une personne et donc un objet Java. D'où la chaîne d'appels entre constructeur. Dans le diagramme ci-dessus, vous pouvez voir des cercles rouges avec un chiffre pour chaque cercle : ils représentent l'ordre d'appels des constructeurs. Le cercle 4 représente l'utilisation de l'instance suite à sa création.

Pour donner la main au constructeur parent, on utilise l'instruction super. Elle doit être utilisée en première instruction du constructeur enfant (on commence par initialiser les attributs de la classe parente). Comme la classe parente peut exposer plusieurs constructeurs, on choisit celui à invoqué en passant les bons paramètres à super (c'est comme un appel de méthode classique). Si vous n'utilisez pas, explicitement, l'instruction super dans votre constructeur alors un appel à super(); sera produit implicitement par le compilateur. Quoi que vous fassiez, un constructeur commence toujours par appeler un constructeur de la classe parente. Et comme, il y a toujours une exception à la règle, sachez seul le constructeur de java.lang.Object dérogera à la règle (mais, c'est un peu normalement, cette classe est la racine de la hiérarchie de classes et n'a donc pas de classe parente).

Voici un exemple d'utilisation du chaînage d'appels de vos constructeurs.

 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 
package fr.koor.poo;

public class Client extends Person {

    private String enterpriseName;
    
    /* Empty constructor */
    public Client() {
        super();
        this.setEnterpriseName( null );
    }
    
    /* Another constructor */
    public Client( int identifier, String firstName, String lastName, String email, String enterpriseName ) {
        super( identifier, firstName, lastName, email );
        this.setEnterpriseName( enterpriseName );
    }

        
    public String getEnterpriseName() {
        return this.enterpriseName;
    }
    
    public void setEnterpriseName( String enterpriseName ) {
        if ( enterpriseName != null ) {
            if ( enterpriseName.equals( "" ) ) {
                throw new RuntimeException( "enterpriseName cannot be empty" );
            } else {
                enterpriseName = enterpriseName.toUpperCase();
            }
        }
        this.enterpriseName = enterpriseName;
    }

}
Implémentation des constructeurs

Il faut absolument connaître et se rappeler les règles suivantes au sujet de vos constructeurs.

Il en résulte que ce programme ne compilera pas : l'appel à super n'est pas la première instruction !

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
class Base {

    public Base() {
        System.out.println( "Base constructeur" );
    }

}

public class Derived extends Base {

    public Derived() {
        System.out.println( "Derived constructeur" );
        super();
    }

}
Mauvaise utilisation des constructeurs

Voici le message d'erreur produit par cet exemple de code.

$> javac Derived.java 
Derived.java:13: error: call to super must be first statement in constructor
        super();
             ^
1 error
$> 

De même, ce programme ne compilera pas non plus. Comme la classe dérivée ne définit pas de constructeur, alors le compilateur en produira un qui réalisera un appel implicite à super();. Or, dans la classe parente il n'y a qu'un seul et unique constructeur acceptant un entier.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
class Base {

    public Base( int value ) {
        System.out.println( "Base constructeur" );
    }

}

public class Derived extends Base {

    /* public Derived() {       // Ce constructeur est sous-entendu
        super();
    } */

}
Mauvaise utilisation des constructeurs

Et voici le message d'erreur produit par ce second exemple de code problématique.

$> javac Derived.java 
Derived.java:9: error: constructor Base in class Base cannot be applied to given types;
public class Derived extends Base {
       ^
  required: int
  found: no arguments
  reason: actual and formal argument lists differ in length
1 error
$> 

Conclusion : il faut vraiment faire attention à la définition de vos constructeurs !

Assistance Eclipse à la production de constructeurs

L'IDE Eclipse peut aider à produire des constructeurs à partir des constructeurs existant au niveau de la classe parente. Pour utiliser cet assistant, placer vous dans le code de votre classe fille et cliquez avec le bouton droit de la souris : un menu contextuel doit apparaître. Cliquez sur le menu « Source » puis « Generate Constructors from Superclass... ».

Ouverture de l'assistant de génération de constructeur

Normalement, il ne vous reste plus qu'à sélectionner le constructeur de la classe parente à partir duquel produire votre constructeur sur la classe fille.

Boîte de dialogue de l'assistant de génération de constructeur

Si vous sélectionner le constructeur à quatre paramètres, comme suggéré dans la capture d'écran précédente, voici le code du constructeur qui sera produit. La ligne de commentaire nous indique que nous avons à faire à un squelette de constructeur et que maintenant c'est à vous de jouer (// TODO) en rajoutant les spécificités de votre constructeur.

 1 
 2 
 3 
 4 
public Client( int identifier, String firstName, String lastName, String email ) {
    super( identifier, firstName, lastName, email );
    // TODO Auto-generated constructor stub
}
Code d'un constructeur produit par l'assistant Eclipse

Redéfinition de méthode

J'espère que jusque-là tout est clair pour vous. Nous allons maintenant parler d'un principe important, lié à l'héritage : la redéfinition de méthode. Effectivement, quand nous héritons d'une classe, nous récupérons l'intégralité des méthodes qui y sont définies. Pour une bonne partie de ces méthodes, c'est très bien et nous pouvons maintenant les utiliser telles quelles. C'est les cas de propriétés relatives à l'identifiant de la personne, son nom, son prénom et son email.

Par contre, certaines méthodes peuvent poser problème et peut-être que vous ne souhaitez pas les utiliser telles quelles. Pour autant, elles risquent de nous être fort utiles. C'est clairement le cas de la méthode toString. Elle permet d'afficher une personne et donc un client, mais le problème réside dans le fait qu'on ne sait pas afficher le nom de l'entreprise avec cette méthode. On va donc redéfinir cette méthode pour l'enrichir au niveau de la classe client.

Emploi de l'annotation @Override

Quand on redéfinit une méthode, on le dit ! Pour ce faire, on utilise l'annotation @Override. Il est vrai qu'elle existe que depuis le Java SE 5.0 et donc son usage n'est pas obligatoire, mais je vous conseille grandement de l'avoir. Qui plus est, les générateurs de code proposés par Eclipse l'ajoute systématiquement : utilisez donc les assistants afin de vous simplifier la vie.

Vous avez deux moyens pour obtenir de l'assistance pour redéfinir vos méthodes. La première manière consiste à cliquer avec le bouton droit de la souris à l'emplacement ou vous souhaitez faire votre redéfinition, de sélectionner « Source » dans le menu contextuel proposé puis de cliquer sur « Override/Implement Methods... ». La boîte de dialogue suivante doit s'ouvrir. Il ne vous reste plus qu'à sélectionner les méthodes à redéfinir.

Boîte de dialogue de l'assistant de génération de constructeur
il est possible d'y redéfinir des méthodes spécifiées à un quelconque niveau d'héritage (notez la présence du type Object).

La seconde technique, pour produire une redéfinition de méthode, passe par l'éditeur de code. Placez-vous à l'endroit souhaité pour l'injection de code. Commencer à taper les premières lettres du nom de la méthode à redéfinir puis tapez simultanément sur CTRL+ESPACE. Un assistant doit apparaître dans le menu contextuel proposé. Par exemple, saisissez toS puis enclenchez la séquence magique : un assistant « toString » doit vous être proposé. Sélectionnez le et voici le code qui devrait être produit.

 1 
 2 
 3 
 4 
 5 
@Override
public String toString() {
    // TODO Auto-generated method stub
    return super.toString();
}
Exemple de redéfinition d'un toString

Appel d'une méthode de la classe parente, redéfinie par la classe fille

Une fois le code de votre redéfinition de toString produit, une nouvelle possibilité syntaxique devrait attirer votre attention. Effectivement, le générateur a produit le code suivant : super.toString();. Remarquez bien le caractère . à la suite du mot clé super : cela garantit que ce n'est pas un appel de constructeur (sinon, on aurait directement eut une paire de parenthèses). En fait, il s'agit d'un appel à la méthode toString de la classe parente.

il ne s'agit pas d'un appel récursif dans le sens où il existe bien deux méthodes toString distinctes (il en existe, même plus que çà, pensez à la classe Object). Une est définie sur la classe Person et l'autre sur la classe Client. Dans le cas d'un appel récursif, c'est la même méthode que si rappelle).

Je pense qu'on sera tous d'accord pour dire que pour afficher un client, il faut afficher une personne suivi des spécificités du client. Voici en conséquence une possibilité de codage de la méthode toString pour notre classe Client.

 1 
 2 
 3 
 4 
@Override 
public String toString() {
    return super.toString() + " - work at " + this.getEnterpriseName();
}
Exemple de redéfinition d'un toString

Voici le code complet de notre classe Client.

 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 
package fr.koor.poo;

public class Client extends Person {

    private String enterpriseName;
    
    /* Empty constructor */
    public Client() {
        super();
        this.setEnterpriseName( null );
    }
    
    /* Another constructor */
    public Client( int identifier, String firstName, String lastName, String email, String enterpriseName ) {
        super( identifier, firstName, lastName, email );
        this.setEnterpriseName( enterpriseName );
    }

    public String getEnterpriseName() {
        return this.enterpriseName;
    }
    
    public void setEnterpriseName( String enterpriseName ) {
        if ( enterpriseName != null ) {
            if ( enterpriseName.equals( "" ) ) {
                throw new RuntimeException( "enterpriseName cannot be empty" );
            } else {
                enterpriseName = enterpriseName.toUpperCase();
            }
        }
        this.enterpriseName = enterpriseName;
    }
    
    
    @Override 
    public String toString() {
        return super.toString() + " - work at " + this.getEnterpriseName();
    }
    
}
La classe Client complète

Et voici le code pour la classe Employee. Une contrainte particulière doit être garantie sur le salaire : un salaire est forcément positif.

 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 
package fr.koor.poo;

public class Employee extends Person {

    private double salary;
    
    /* Empty constructor */
    public Employee() {
        super();
        this.setSalary( 0 );
    }
    
    /* Another constructor */
    public Employee( int identifier, String firstName, String lastName, String email, double salary ) {
        super( identifier, firstName, lastName, email );
        this.setSalary( salary );
    }
    
    public double getSalary() {
        return salary;
    }
    
    public void setSalary( double salary ) {
        if ( salary < 0 ) {
            throw new RuntimeException( "A salary must be positive" );
        }
        this.salary = salary;
    }
    
    @Override 
    public String toString() {
        return super.toString() + " - win " + this.getSalary() + " euros";
    }
}
La classe Employee complète

Le polymorphisme

Un autre point important à comprendre, dans ce chapitre, est le polymorphisme. Ce terme vient du grec ancien polús (plusieurs) et morphê (forme). Le polymorphisme est induit par l'héritage.

Définition du polymorphisme

Pour comprendre ce concept, le mieux est d'analyser l'exemple suivant. On y créer un ensemble de personnes : comme un client est une personne, on devrait pouvoir aussi stocker des instances de la classe Client dans la Collection. Il en va de même pour des instances de la classe Employee.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
package fr.koor.poo;

import java.util.ArrayList;

public class Start {

    public static void main( String [] args ) {

        ArrayList<Person> persons = new ArrayList<>();
        persons.add( new Person() );
        persons.add( new Employee( 1, "Jason", "Bourne", "supremacy@cia.us", 10_000 ) );
        persons.add( new Client( 7, "James", "Bond", "007@mi6.uk", "MI6" ) );
        
        for( Person person : persons ) {
            System.out.println( person );
        }
        
    }

}
Mise en évidence du polymorphisme
si vous utilisez encore un compilateur Java SE 6 (ou inférieur), la ligne 9 pourra poser problème. Effectivement, il fallait, à l'époque, rappeler le type des objets contenu dans la collection ou niveau du new. Il aurait donc fallu écrire : ArrayList<Person> persons = new ArrayList<Person>();. De même, le groupage de digits (de chiffres), utilisé pour définir le salaire de James (ligne 11), n'existe en Java que depuis sa version 7.0.

La collection persons peut contenir, par polymorphisme, n'importe qu'elle instance basée sur une classe dérivant de Person. On a un ensemble hétérogène d'objects (basés sur les classes Person, Client, Employee) mais partageant tous un point commun : on doit être une personne.

Du coup la question, qu'on est en droit de se poser, est la suivante : quel est la version de toString qui va être invoquée pour chaque objet de la collection ? Effectivement, la collection est typée comme contenant des personnes. Sauf que les new, eux , ont été fait sur différentes classes.

La réponse est simple : Java va toujours invoquer la méthode la plus spécifique pour chaque instance de la collection. Voici les résultats produits par l'exemple précédent.

$> java Start
0: john DOE @ unknown@anywhere.unk
1: jason BOURNE @ supremacy@cia.us - win 10000.0 euros
7: james BOND @ 007@mi6.uk - work at MI6
$> 

Dans cet exemple, on n'utilise explicitement que la méthode toString (en ligne 15). Du coup, on aurait pu typer autrement la collection. Dans l'exemple suivant, nous utilisons une collection d'objets Java. Mais du coup, attention : par polymorphisme, celle-ci pourra contenir d'importe quel type d'objet, pour peu qu'il dérive de la classe java.lang.Object. Du coup, je peux envisager pousser une date dans la collection. Il sera donc de votre responsabilité de bien typer la collection (et la variable dans la boucle for).

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
package fr.koor.poo;

import java.util.ArrayList;
import java.util.Date;

public class Start {

    public static void main( String [] args ) {

        ArrayList<Object> persons = new ArrayList<>();
        persons.add( new Person() );
        persons.add( new Employee( 1, "Jason", "Bourne", "supremacy@cia.us", 10_000 ) );
        persons.add( new Client( 7, "James", "Bond", "007@mi6.uk", "MI6" ) );
        persons.add( new Date() );
        
        for( Object person : persons ) {
            System.out.println( person );
        }
        
    }

}
Mise en évidence du polymorphisme

Utilisation de l'opérateur instanceof

Si vous utilisez des collections (ou des variables) polymorphiques, il est alors possible de tester la nature de chaque instance de la collection. Pour ce faire, on utilise l'opérateur instanceof. Dans l'exemple suivant, on cherche à afficher toutes les personnes, quel que soit la classe utilisée pour produire chaque instance, à l'exception des salariés (on ne veut pas afficher Jason). Voici comment faire en utilisant cet opérateur instanceof (ligne 15).

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
package fr.koor.poo;

import java.util.ArrayList;

public class Start {

    public static void main( String [] args ) {

        ArrayList<Person> persons = new ArrayList<>();
        persons.add( new Person() );
        persons.add( new Employee( 1, "Jason", "Bourne", "supremacy@cia.us", 10_000 ) );
        persons.add( new Client( 7, "James", "Bond", "007@mi6.uk", "MI6" ) );
        
        for( Person person : persons ) {
            if ( person instanceof Employee ) continue;
            System.out.println( person );
        }
        
    }

}
Mise en évidence du polymorphisme

Et voici les résultats produit par l'exemple de code ci-dessus.

$> java Start
0: john DOE @ unknown@anywhere.unk
7: james BOND @ 007@mi6.uk - work at MI6
$> 

Travaux pratiques

Le sujet

En guise de travaux pratiques, je vous propose de mettre en oeuvre les classes décrites dans le modèle UML ci-dessous. Celui-ci définit trois classes. La classe de base, nommée Shape, représente le concept de figure géométrique : une figure géométrique étant positionnée via son centre (attribut center). Attention la connaissance du centre de la figure est obligatoire et vous ne devez pas accepter de pointeur nul. De cette classe doivent dériver deux sous-classes : Circle et Square, permettant de représenter respectivement des cercles et des carrés. Les informations de rayon et de longueur ne devront pas être négatives : si tel est le cas, changez le signe des valeurs problématiques.

les valeurs placées entre crochets et à droite des attributs, dans le diagramme UML ci-dessus, représentent les cardinalités des attributs. Par exemple, et dit autrement, une figure géométrique contient un seul et unique centre (attribut center).
les caractères -, placés devant les attributs représentent une visibilité privée, contrairement aux caractères + devant les méthodes toString qui, eux, représentent une visibilité publique.
la classe java.awt.Point est déjà existante dans le Java SE. Ne la recodez surtout pas !

Ensuite, veuillez produire une classe Start qui exposera une méthode main permettant de tester vos classes de figures géométriques. Cette méthode devra définir une collection de figures géométriques de natures diverses. Enfin, parcourez cet ensemble de figures géométriques pour les afficher à l'écran.

Prennez soin de bien produire ces codes (formatage, commentaires, ...), car ils vont nous resservir dans le prochain chapitre et nous devront les compléter. Jouez le jeu et ne passer pas directement à la correction. ;-)

en termes de documentations, ceux qui connaissent déjà Javadoc peuvent d'ors et déjà produire des commentaires Javadoc. Pour les autres, rien de grave : nous parlerons de ce sujet dans un futur chapitre.

La correction

La première classe proposée est, bien entendu, la classe Shape. Rien de particulier à rajouter sur cette classe.

 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 
package fr.koor.poo;

import java.awt.Point;

public class Shape /* extends Object */ {

    // --- The center attribute ---
    private Point center;
    
    // --- 2 constructors ---
    public Shape() {
        this( new Point( 0, 0 ) );
    }
    
    public Shape( Point center ) {
        // super();     // Appel au constructeur parent sous-entendu
        this.setCenter( center );
    }
      
    // --- The center property ---
    public Point getCenter() {
        return center;
    }
    
    public void setCenter( Point center ) {
        if ( center == null ) {
            throw new NullPointerException( "center parameter cannot be null" );
        }
        this.center = center;
    }
    
    // --- A method for compute a shape representation string ---
    @Override public String toString() {
        return "Unknown shape placed at " + center;
    }
    
}
Notre classe de base Shape, permettant de définir le concept de figures géométriques

Voici maintenant le code de la classe Circle. Donc cette classe dérive de Shape et rajoute la notion de rayon du cercle.

 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 fr.koor.poo;

import java.awt.Point;

public class Circle extends Shape {

    // --- The radius attribute ---
    private double radius;
    
    
    // --- 2 constructors ---
    public Circle() {
        this( new Point( 0, 0 ), 1 );
        
        // Ou bien (au final, ça revient au même) :
        
        // super();
        // this.setRadius( 1 );
    }
    
    public Circle( Point center, double radius ) {
        super( center );    // Rappel du constructeur parent acceptant un paramètre
        this.setRadius( radius );
    }
    
    // --- The radius property ---
    public double getRadius() {
        return radius;
    }
    
    public void setRadius( double radius ) {
        if ( radius < 0 ) {
            // Caution: a radius must be positive
            radius = -radius;
        }
        this.radius = radius;
    }
    
    // --- A method for compute a circle representation string ---
    @Override public String toString() {
        return "A circle placed at " + getCenter() + " and with a radius " + radius;
    }
    
}
Notre classe Circle, permettant de définir le concept de cercles

C'est maintenant le tour de la classe Square. Elle dérive aussi de Shape et rajoute la notion de longueur de côté du carré.

 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 fr.koor.poo;

import java.awt.Point;

public class Square extends Shape {

    // --- The length attribute ---
    private double length;
    
    
    // --- 2 constructors ---
    public Square() {
        this( new Point( 0, 0 ), 1 );
        
        // Ou bien (au final, ça revient au même) :
        
        // super();
        // this.setLength( 1 );
    }
    
    public Square( Point center, double length ) {
        super( center );    // Rappel du constructeur parent acceptant un paramètre
        this.setLength( length );
    }
    
    // --- The length property ---
    public double getLength() {
        return length;
    }
    
    public void setLength( double length ) {
        if ( length < 0 ) {
            // Caution: a length must be positive
            length = -length;
        }
        this.length = length;
    }
    
    // --- A method for compute a square representation string ---
    @Override public String toString() {
        return "A square placed at " + getCenter() + " and with a length " + length;
    }
    
}
Notre classe Square, permettant de définir le concept de carré

Ensuite, voici le code de la classe Start permettant de réaliser quelques tests sur nos classes de figures géométriques.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
package fr.koor.poo;

import java.awt.Point;
import java.util.ArrayList;

public class Start {

    public static void main( String [] args ) {
        
        ArrayList<Shape> shapes = new ArrayList<>();
        
        shapes.add( new Shape() );
        shapes.add( new Circle( new Point( 5, 5 ), 1 ) );
        shapes.add( new Square( new Point( 1, 1 ), -2 ) );
        
        for( Shape shape : shapes ) {
            System.out.println( shape );
        }
        
    }
    
}
Notre classe Start, permettant de tester nos figures géométriques

Et enfin, voici les résultats produits par notre classe de test.

$> java fr.koor.poo.Start
Unknown shape placed at java.awt.Point[x=0,y=0]
A circle placed at java.awt.Point[x=5,y=5] and with a radius 1.0
A square placed at java.awt.Point[x=1,y=1] and with a length 2.0
$> 

Interdire l'héritage ou la redéfinition de méthode

Pour clore ce chapitre, j'aimerais vous parler de l'utilisation du mot clé final dans un contexte lié à l'héritage. Effectivement, ce mot clé peut être utilisé dans plusieurs contextes différents et notamment pour définir des constantes (possibilité que nous avons déjà étudié). Mais vous pouvez aussi placer ce mot clé devant une classe ou devant une méthode : dans ces deux cas, il prend des significations différentes.

Interdire la dérivation d'une classe

Il est possible d'interdire l'héritage (la dérivation) d'une classe en la préfixant du mot clé final. Si vous tentez malgré tout de dériver cette classe, une erreur de compilation sera produite.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
public final class FinalClass {

    public void aMethod() {
        System.out.println( "A simple méthod" );
    }

}
Définition d'une classe finale

On tente de la dériver.

 1 
 2 
public class DerivedClass extends FinalClass {
}
Définition d'une classe finale

Et voici le message d'erreur produit par le compilateur.

$> javac *.java
DerivedClass.java:1: error: cannot inherit from final FinalClass
public class DerivedClass extends FinalClass {
                                  ^
1 error
$>

De nombreuses classes de la librairie standard de Java sont finalisées : c'est notamment le cas de la classe java.lang.String.

Interdire la redéfinition d'une méthode

Placé devant le type de retour d'une méthode, le mot clé final précise qu'on ne pourra plus redéfinir cette méthode. Voici un exemple d'utilisation.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
public class BaseClass {

    public final void aMethod() {
        System.out.println( "A simple méthod" );
    }

}
Définition d'une méthode finale

Et maintenant, on tente de redéfinir cette méthode.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
public class DerivedClass extends BaseClass {

    @Override
    public void aMethod() {
        System.out.println( "Is not possible" );
    }

}
Tentative de redéfinition d'une méthode finalisée

Et voici maintenant le message d'erreur produit par le compilateur.

$> javac *.java
DerivedClass.java:4: error: aMethod() in DerivedClass cannot override aMethod() in BaseClass
    public void aMethod() {
                ^
  overridden method is final
1 error
$>


Introduction à la POO et principe d'encapsulation Définition de classes de type « record »