En reprenant l'exemple de notre classe Date, supposons que l'on veuille définir une classe DateEvénement qui associe à une date donnée, un évènement qui la caractérise. La solution triviale serait de définir la classe DateEvénement en redéfinition entièrement cette classe et ce en prenant exemple sur la classe Date et en rajoutant les nouvelles fonctionnalités que l'on juge utile.
Cette approche est à l'opposé de l'esprit de la programmation objet et du génie logiciel. Dans la mesure où l'on dispose déjà de la classe, les langages orientés objets offre un moyen bien plus simple pour définir cette nouvelle classe. Il s'agit de l'héritage .
classe DateEvenement { private int jour, mois, année ; private String event = null ; public DateEvenement(int m, int m, int j, String e) { jour = j ; mois = m ; année = a ; event = e ; } public affecter(int m, int m, int j, String e) { jour = j ; mois = m ; année = a ; event = e ; } ... public void imprimer() { System.out.println(jour + "/" + mois + "/" + année + "->" + event) ; } }
L'héritage est une caractéristique des langages orientés objet. Une classe obtenue par héritage possèdent la totalité des membres de la classe de base ainsi toutes ses méthodes. Une classe B peut donc se définir par rapport une autre classe A. On dira que la sous classe B hérite des attributs et fonctionnalités de la classe de base A. L'ensemble des classes sont organisés de manière hiérarchique permettant de structurer l'ensemble des informations manipulées par un programme.
Limiter une sous classe aux caractéristiques de la classe de base n'a évidemment aucun intérêt. Une sous classe complètent, en quelque sorte, la classe de base. Elle se doit de définir des attributs et des services supplémentaires. Elle pourra modifier, éventuellement, le comportement d'une des méthodes de la classe de base.
Reprenons notre exemple de la classe DateEvenement : cette classe possède beaucoup de caractéristiques de la classe. En ce qui concerne les champs, seul un champ supplémentaire event est rajoutée à cette classe. Quant aux méthodes, le constructeur et les autres méthodes de cette nouvelle classe doivent être complétés ou adaptées en fonction de l'ajout de ce nouveau champ. Une partie importante du code écrit pour la classe Date reste inchangée. On peut donc définir la classe DateEvenement par dérivation ou extension de la classe Date et ce avec le mot clé extends.
Avec de cette définition, les objets de la classe DateEvenement possèdent les champs et méthodes de la classe Date. Il appartient au concepteur de cette nouvelle de définir les champs et méthodes propre à cette classe. Dans notre exemple, la classe DateEvenement doit définir un champ event et les méthodes d'accès et de modification de ce champ.
class DateEvenement extends Date { ... }
class DateEvenement extends Date { private String event = null ; ... public String quelEvent() { return event ; } ... }
Lorsqu'on définit une classe dérivée , il faut s'assurer que, lors de la création des objets de cette nouvelle classe, les champs propres à cette classe dérivée ainsi que les champs de la classe de base soient initialisés correctement. Les champs de la classe Date sont des champs privés, la sous classe ne peut, en aucun cas, se charger toute seule de l'initialisation de ces membres. Les constructeurs d'une classe dérivée devront forcément utiliser, pour les champs qui ne sont propres à cette classe, les constructeurs de la classe de base. Pour invoquer le constructeur de la classe de base, on fera appel à l'instruction super(...). Un constructeur d'une classe dérivée se compose généralement deux parties : celle concernant les champs de la classe de base et celle concernant les champs propres de la classe dérivée.
L'invocation de super(...) doit être la première instruction du constructeur de la classe dérivée.
L'ordre dans lequel les différents constructeurs et initialisations sont effectués est le suivant :
class DateEvenement extends Date { ... public DateEvenement(int j, int m, int année, int e) { super(j, m, a) ; // appel au constructeur de la classe de base event = e ; // Initialisation des champs propres de la classe dérivée. } ... }
est automatiquement transformé en
public DateEvenement(String e) {event = e ; }
Dans le cas où un tel constructeur n'existe pas dans la classe de base, une erreur de compilation est générée. Il existe un cas où l'absence de l'instruction super(...) ne conduit pas cet appel implicite : celui où le corps du constructeur commence par l'instruction this(...).
public DateEvenement(String e) { super() ; event = e ; }
Si aucun constructeur n'est défini dans la classe dérivée, un constructeur sans argument est quant même invoqué. Tout se passe comme si un constructeur implicite était défini ; constructeur de la forme :
public DateEvenement() { super() ; }
La classe Object est la mère de toutes les classes ; toute classe est dérivée directement ou indirectement de la classe Object. Ainsi, lors de la création d'un objet, le premier constructeur invoqué est celui de la classe Object suivi des autres constructeurs dans l'ordre de la hiérarchie de dérivation des classes.
Les champs déclarés dans la classes dérivée sont toujours des champs supplémentaires. Si l'on définit un champ dans la sous classe ayant le même nom qu'un champ de la classe de base, il existera deux champs de même noms. le nom de champ désignera toujours le champ déclaré dans la classe dérivée. Pour avoir accès au champ de la classe de base, il faudra changer le type de la référence pointant sur l'objet ou en utilisant le mot clé super.
Cette technique peut s'appliquer en cascade de la manière suivante :
class A { public int i ; ... } class B extends A { public int i ; ... public void uneMethode() { i = 0 ; // i est le champ défini dans la classe B this.i = 0 ; // i est le champ défini dans la classe B super.i = 1 ; // i est le champ défini dans la classe A ( (A) this ).i = 1 // i est le champ défini dans la classe A ... } }
Par contre, l'instruction suivante est incorrecte :
class C extends B { public int i ; ... public void uneMethode() { i = 0 ; // i est le champ défini dans la classe C this.i = 0 ; // i est le champ défini dans la classe C super.i = 1 ; // i est le champ défini dans la classe B ( (B) this ).i = 1 // i est le champ défini dans la classe B ( (A) this ).i = 1 // i est le champ défini dans la classe A ... } }
Tout comme l'utilisation du mot clé this, le mot clé super ne peut être utilisé dans les méthodes static.
super.super.i = 1 ; // Incorrect syntaxiquement !
La redéfinition d'une méthode consiste à fournir une implantation différente de la méthode de même signature fournie par la classe de base. Dans cet exemple, la méthode imprimer des classes Date et DateAnglais ont la même signature ; celle de la classe DateAnglais redéfinit celle de la classe Date.
La rédéfinition d'une méthode ne concerne que méthodes ayant la même signature dans la classe de base et la sous classe.
class DateAnglais extends Date { public void imprimer() { System.out.println(quelMois() + "/" + quelJour() + "/" + quelAnnée() + "/" + event) ; } }
La redéfinition des méthodes est un mécanisme puissant : le parallèle avec la redéfinition des champs peut être trompeur. En effet, lorsqu'une méthode est redéfinie, on ne peut invoquer la méthode définie dans la classe de base par un simple changement de type de la référence.
Dans cet exemple, même en changement de le type de la référence de l'objet Pomme et Poire en une référence vers un Fruit, la méthode quiEsTu invoqué est toujours celle de l'objet référencé. Ce comportement peut paraître surprenant de prime abord ; mais on se rend vite compte ce comportement est tout à fait souhaitable. Par exemple, si l'on dispose d'un tableau de fruits (des pommes et des poires), les éléments de ce tableau sont des pommes et des poires mais sont définis dans la définition du tableau, comme des fruits. Pourtant, sans ce comportement, il nous serait impossible de connaître la nature exacte des fruits rangés dans le tableau.
class Fruit { public void quiEsTu() { System.out.println("Je suis un fruit") ; } class Pomme extends Fruit { public void quiEsTu() { System.out.println("Je suis une pomme") ; } class poire extends fruit { public void quiEsTu() { System.out.println("Je suis une poire") ; } class test { public void main(String args[]) { Pomme pm = new Pomme() ; Poire pr = new Poire() ; Fruit f ; f.quiEsTu() ; // Je suis un fruit f = (Fruit)pm ; f.quiEsTu() ; // Je suis une pomme f = (Fruit)pr ; f.quiEsTu() ; // Je suis une poire } }
Pour avoir accès à une méthode redéfinie de la classe de base, à l'intérieur d'une méthode de la classe dérivée, il faudra utiliser le mot clé super. Comme pour les champs redéfinis, il suffit de préfixer le nom de méthode par le mot clé super pour invoquer la méthode de la classe de base.
class DateEvenement extends Date { private String event = null ; public DateEvenement(int j, int m, int a, int e) {super(j, m, a) ; event = e ; } public void imprimer() { super.imprimer(); System.out.println(e); } ... }
Rappelons que dans la majorité des acs, on ne se souciera pas des destructeurs puisque Java dispose d'un système de récupération de mémoire automatique.
class B extends A { public finalize() { super.finalize() ; // Code de finalize pour la classe dérivée.... ... } }
Une méthode est final si elle ne peut être redéfinie dans aucune des sous classes. Ainsi, le concepteur de la classe de base exprime son souhait de figer définitivement l'implantation de cette méthode.
On peut également décider figer la définition d'une classe entière en la déclarant final. Cela implique qu'il ne sera plus possible de dériver une nouvelle classe à partir de cette dernière. Et comme on peut dériver cette classe, il est évidemment plus possible de rédéfinir les méthodes de cette classe dans une sous classe.
class Date { private int jour, mois, année ; ... public final quelJour() { return jour ; } ... }
L'intérêt de déclarer final les classes et les méthodes est fournir un module sûr. On est assuré que le comportement ne peut être modifié. Une classe finale est une classe en qui on peut avoir confiance. Par contre, celui réduit la souplesse et l'extensibilité des modules proposés. Une meilleure approche est parfois de définir toutes les méthodes finales sans déclarer final la classe elle-même. Ainsi, on pourra dériver de nouvelles classes mais changer avoir la possibilités de changer le comportement des méthodes de la classe de base.
final class DateEvenement { ... }
Un second intérêt des méthodes et classes finales est le souci d'efficacité. Les méthodes et classes finales permettent au compilateur de produire du code optimisé. En particulier, la détermination statique des méthodes peut être effectuée dans ce cas et parfois même des évaluations partielles de votre programme.
Une opération de cast permet de modifier le type d'une référence. Les changement de nature ne permise que dans des cas bien précis.
Un opération de cast a le droit d'affiner le type d'une référence. Par exemple, une référence vers un objet de type Date peut être changé en une référence vers un objet de type DateEvénement. Ce dont il est question ici, ce n'est que la perception que la machine Java a de la référence. L'objet référencé est toujours le même ; on essaye tout juste de faire croire à la machine Java que l'objet référencé est d'une autre nature. En aucun cas, l'objet lui ne subit de modification par cette opération de cast.
On ne peut changer le type d'une référence en une référence vers un objet d'une classe dérivée que si l'on est sûr que l'objet référencé est bien du type prétendu. Une référence vers un objet de type DateEvénement peut être changé en une référence vers un objet de type Date que si l'on est assuré que l'objet référencé est effectivement un objet de la classe DateEvénement. Sinon, l'erreur ClassCastException est générée.
On trouvera de amples détails, dans le chapitre 12, sur l'ensemble des conversions permises par le langage Java .
Date d; DateEvenement de; ... de = d ; // Erreur de comilation ! d = de ; // O.K. de = (DateEvenement)d ; // O.K. d contient une référence vers de // un objet de type DateEvenement
On peut parfois utiliser les classes pour définir, non pas un type d'objet bien précis, mais un concept. On pourrait, par exemple, définira une classe Forme avec des méthodes abstraites comme superficie, dessiner, tourner, etc. On ne sait pas implanter la méthode superficie dans le cas d'une forme générale. Par contre, une fois connue une forme précise (un carré, un cercle, etc.), on sait implanter cette méthode pour cette forme. Autrement dit, un objet de type forme n'a aucun intérêt en soi. Les objets utiles sont les carrés, les cercles, etc. Par contre, il existe souvent des caractéristiques et comportements communs à toutes les formes.
Le concepteur de la classe Forme définira donc cette classe en la qualifiant d'abstraite (abstract). Il regroupera dans cette classe tous les champs communs à toutes les formes ainsi que les méthodes pour lesquelles une implantation est possible. Quant aux méthodes qui ne peuvent être implantées et qui doivent absolument l'être pour des formes bien précises, il les qualifiera d'abstraites.
Une méthode abstraite est une méthode dont la définition est supposée être donnée par redéfinition dans les classes dérivées. On considère dans ce cas là que la classe de base ne peut fournir une méthode par défaut.
Il ne sera jamais possible créer un objet de type Forme. Les objets qui sont susceptible d'exister sont des formes bien précises : des carrés, des cercles, des lignes, etc. Ces formes effectives, seront des objets des classes obtenus en dérivant la classe Forme. Les classe Carrée, Cercle, Ligne etc. seront des classes dérivées de la classe Forme.
Une classe abstraite est une classe partiellement implantée i.e. que certaines des méthodes sont abstraites. Le langage Java impose de qualifier la classe d'abstraite lorsqu'une de ses méthode est abstraite.
Tout ceci permet de regrouper des données et méthodes communes dans une classe et de spécifier les méthodes qu'une classe dérivée de celle-ci doit absolument implanter. Si l'on définit une sous classes sans implanter toutes les méthodes abstraites de la classe de base, une erreur de compilation est générée.
La redondance de ce qualifier abstract, à la fois pour la méthode et pour la classe, est là pour permettre au programmeur de déterminer au premier coup d'oeil, en voyant la classe de savoir s'il existe une méthode abstraite dans cette classe. En effet, il ne sera pas nécessaire de parcourir l'ensemble des méthodes d'une classe pour savoir s'il s'agit d'une classe abstraite. Le mot clé abstract devant obligatoirement figurer dans l'entête de la classe, il sera donc facile de repérer les classes abstraites. Il n'est pas permis de créer des objets d'une classe abstraite.
abstract class Forme { ... public abstract void superficie() ; ... } class Carrée extends Forme { ... public void superficie () { ... } ... }
package java.lang; public class Object { public String toString() public boolean equals(Object obj) public final native Class getClass() public native int hashCode() protected native Object clone() protected void finalize throws Throwable()
public final native void notify() public final native void notifyAll() public final native void wait(long timeout)throws InterruptedException public final void wait(long timeout, int nanos)throws InterruptedException public final void wait()throws InterruptedException }
La méthode toString est utilisée pour donner une représentation textuelle d'un objet. Pour reprendre notre exemple de la classe Date, au lieu d'avoir une méthode spécifique pour afficher une date avec un format bien particulier avec la méthode afficher, on aurait plutôt intérêt à redéfinir la méthode toString pour la classe Date. Ainsi, on pourra afficher un objet de type Date comme tout autre objet avec la méthode System.out.print.
public String toString() { return jour + " /" + mois + " /" +année; }
Comme la méthode toString, la méthode equals rend la valeur true si deux l'objet sur lequel la méthode equals est invoquée est égale à l'objet passé en paramètre. Qu'est ce que l'égalité ? La notion de l'égalité dépend des objets concernés. Par défaut, la sémantique de la méthode equals fournie dans la classe Object est l'égalité entre les valeurs des références : les deux références désignent le même objet. Cette méthode equals est redondante par rapport au simple test d'égalité que l'on effectue avec l'opérateur ==. Par exemple, on pourrait vouloir définir une méthode equals pour la classe Date dont la sémantique porte, non pas sur les valeurs des références mais, sur les valeurs des champs des objets. Il suffit alors de redéfinir la méthode equals dans la classe Date comme suit :Avec cette redéfinition de la méthode equals appliquée à deux objets de type Date rend la valeur true lorsque les champs jour, mois et année ont respectivement les mêmes valeurs.
public boolean equals(Object d) { if ( d != null && d instanceof Date) { return this.jour == d.jour && this.mois == d.mois && this.année == d.année; } else return false; }
public final native Class getClass ()
La méthode getClass est une méthode final qui ne peut donc pas être redéfinie. Elle retourne la classe à laquelle appartient l'objet sur lequel elle s'applique. La valeur retournée est un objet de la classe java.lang.Class (voir) qui dispose d'un certain nombre de méthode pour examiner et extraire des informations sur les classes.
On remarquera qu'il affiche bien DateEvenement et non pas Date.
class test { public static void main(String args[]) { Date d = new DateEvenement(25,12,1997,"Noel 97"); System.out.println(d.getClass()); } } produit l'affichage suivant~: class DateEvenement
newInstance
La méthode hashCode rend un entier représentant le code de hachage de l'objet sur lequel s'applique la méthode. Cet entier est généralement unique pour chaque objet. A priori, il n'est pas nécessaire de redéfinir cette méthode pour les classes que l'on définit. Il existe certaines applications où l'on aimerait faire correspondre un même entier à un ensemble d'objets d'une classe : il s'agit une relation d'équivalence sur les objets. Nous verrons plus loin les détails d'implantation des hashcodes avec la classe HashTableclasse.HashTable.protected native Object clone () throws CloneNotSupportedException
La méthode clone retourne une copie de l'objet sur lequel la méthode est invoquée. L'implantation de la méthode clone pour les objets d'une classe que l'on définit. L'attitude d'une classe face au clonange des objets peuvent être variée :
A TERMINER
protected void finalize () throws Throwable
A faire