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
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.
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 ); } } |
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 ?
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.
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
.
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.
Enfin, notez quelques points de terminologies complémentaires.
Classe mère : la classe Person
est qualifiée de classe mère ou de classe parente ou, encore, de classe de base (c'est la même chose).
Classe fille : les classes Client
et Employee
sont qualifiée de classes filles ou de classes dérivées.
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.
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.
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 { } |
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 ); } } |
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 Person
peut 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; } } |
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 $>
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; } } |
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; } } |
Il faut absolument connaître et se rappeler les règles suivantes au sujet de vos constructeurs.
Règle 1 : si vous n'implémenter aucun constructeur dans votre classe, alors un constructeur (qualifié de « par défaut ») vide est produit automatiquement par le compilateur. Ce constructeur ne fera rien (excepté de commencer à donner la main au constructeur de la classe parente acceptant zéro paramètre).
Règle 2 : si vous définissez au moins un constructeur dans votre classe, alors le constructeur par défaut n'est plus produit. Si vous en avez besoin, il sera alors de votre responsabilité de le coder.
Règle 3 : un constructeur commence toujours à donner la main à un constructeur de la classe parente. Pour choisir le bon constructeur sur la
classe parente, vous devez utiliser l'instruction super
et elle devra toujours être en première instruction du bloc correspondant
à votre constructeur.
Règle 4 : si vous n'invoquez pas super
explicitement, alors un appel à super();
sera implicitement généré par le
compilateur.
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(); } } |
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(); } */ } |
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 !
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... ».
Normalement, il ne vous reste plus qu'à sélectionner le constructeur de la classe parente à partir duquel produire votre constructeur sur la classe fille.
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 } |
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.
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.
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(); } |
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.
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(); } |
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(); } } |
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"; } } |
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.
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 ); } } } |
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 ); } } } |
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 ); } } } |
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 $>
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.
center
).
-
, 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.
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. ;-)
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; } } |
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; } } |
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; } } |
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 ); } } } |
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 $>
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.
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" ); } } |
On tente de la dériver.
1 2 |
public class DerivedClass extends FinalClass { } |
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
.
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" ); } } |
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" ); } } |
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 $>
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 :