Jusqu'ici, nous pouvons considérer que nous avons défini deux sortes de classes : des classes simples et des classes dérivées. En réalité, il existe une classe nommée Object dont dérive implicitement toute classe simple. Ainsi, lorsque vous définissez une classe Point de cette manière :
class Point {
...
}
tout se passe en fait comme si vous aviez écrit (vous pouvez d'ailleurs le faire) :
class Point extends Object {
...
}
Voyons les conséquences de cette propriété.
Compte tenu des possibilités de compatibilité exposées précédemment, une variable de type Object peut être utilisée pour référencer un objet de type quelconque :
Point p = new Point (...) ;
Pointcol pc = new Pointcol (...) ;
Fleur f = new Fleur (...) ;
Object o ; o = p ; OK
o = pc ; OK
o = f ; OK
Cette particularité peut être utilisée pour manipuler des objets dont on ne connaît pas le type exact (au moins à un certain moment). Cela pourrait être le cas d'une méthode qui se contente de transmettre à une autre méthode une référence qu'elle a reçu en argument.
Bien entendu, dès qu'on souhaitera appliquer une méthode précise à un objet référencé par une variable de type Object, il faudra obligatoirement effectuer une conversion appropriée. Voyez cet exemple où l'on suppose que la classe Point dispose de la méthode déplace
Point p = new Point (...) ;
Object o ;
...
o = p ;
o.déplace() ; erreur de compilation
((Point)o).déplace() ; OK en compilation (attention
aux parenthèses)
Point p1 = (Point) o ; OK : idem ci-dessus, avec création
d'une référence
p1.déplace() ; intermédiaire dans p1
Notez bien les conséquences des règles relatives au polymorphisme. Pour pouvoir appeler une méthode f par o.f(), il ne suffit pas que l'objet effectivement référencé par f soit d'un type comportant une méthode f, il faut aussi que ladite méthode existe déjà dans la classe Object. Ce n'est manifestement pas le cas de la méthode déplace.
Une méthode importante de la classe Object est la méthode toString(), qui renvoie une chaîne de caractères représentant la valeur de l'objet. Cette méthode est intéressante puisque lorsque vous demander d'afficher l'objet en entier, c'est cette méthode toString() qui est invoquée. L'intérêt, c'est que sans écriture particulière, vous obtenez malgré tout un affichage par défaut.
import java.awt.Point; public class Calcul { public static void main(String[] args) { System.out.println(new Point(15, 23)); } }
Ainsi, la méthode toString() de la classe Point renvoie une chaîne comme celle-ci :
java.awt.Point[x=15,y=23]
Vous remarquez, que les classes de la JDK redéfinisse la méthode toString() de la classe de base Object. La plupart (mais pas toutes) des méthodes toString() redéfinies dans les différents objets propose un format similaire : le nom de la classe, suivi des valeurs des attributs incluts entre crochets.
Que se passe-t-il si lorsque nous créons nos propres classes et que nous tentons de les afficher directement, comme au travers de l'exemple ci-dessous ?
import java.awt.Point; public class Calcul { public static void main(String[] args) { System.out.println(new Personne("REMY", "Emmanuel", 46)); } } class Personne { private String nom, prénom; private int âge; public Personne(String nom, String prénom, int âge) { this.nom = nom; this.prénom = prénom; this.âge = âge; } }
L'affichage est alors le suivant :
Personne@82ba41
En fait, la classe Object définit la méthode toString() pour afficher le nom de la classe et le code de hachage de l'objet (ce code est un numéro de référence qui représente l'objet au sein de la JDK). Comme notre classe ne redéfinit par cette méthode toString(), c'est celle de la classe Object (par héritage : toute classe hérite au moins de la classe de base Object) qui est sollicité.
Ainsi, l'affichage de base de la classe Personne n'est pas très agréable. Il est préférable de proposer sa propre version et donc de redéfinir la méthode toString(). En voici une solution possible :
import java.awt.Point; public class Calcul { public static void main(String[] args) { System.out.println(new Personne("REMY", "Emmanuel")); } } class Personne { private String nom, prénom; public Personne(String nom, String prénom) { this.nom = nom; this.prénom = prénom; } public String toString() { return "Personne[nom="+nom+", prénom="+prénom+"]"; } }
Voici le résultat obtenu :
Elève[nom=REMY, prénom=Emmanuel][notes = 15.0 8.0 12.5 ]
En réalité, vous pouvez faire mieux. Au lieu de coder en dur le nom de la classe dans la méthode toString(), appelez getClass().getName() pour obtenir une chaîne avec le nom de la classe. Ainsi, la méthode toString() s'applique également aux sous-classes.
Bien entendu, le programmeur de la sous-classe doit définir sa propre méthode toString() et ajouter les attributs de la sous-classe. Si la superclasse utilise getClass().getName(), la sous-classe peut simplement appeler super.toString().
import java.awt.Point; import java.util.ArrayList; public class Calcul { public static void main(String[] args) { Elève élève = new Elève("REMY", "Emmanuel"); élève.ajout(15.0).ajout(8.0).ajout(12.5); System.out.println(élève); } } class Personne { private String nom, prénom; public Personne(String nom, String prénom) { this.nom = nom; this.prénom = prénom; } public String toString() { return getClass().getName()+"[nom="+nom+", prénom="+prénom+"]"; } } class Elève extends Personne { private ArrayList<Double> notes = new ArrayList<Double>(); public Elève(String nom, String prénom) { super(nom, prénom); } public Elève ajout(double note) { notes.add(note); return this; } public String toString() { String chaîne = super.toString()+"[notes = "; if (notes.isEmpty()) chaîne+="aucune ]"; else for (Double x : notes) chaîne = chaîne + x +" "; return chaîne+="]"; } }
La classe Elève récupère le comportement de l'affichage par défaut de sa classe parente Personne. Cette dernière toutefois préfère donner le nom de la classe de façon polymorphe, donc soit d'elle même, soit d'une de ses sous-classe.
Ainsi, voici le résultat obtenu :
Elève[nom=REMY, prénom=Emmanuel][notes = 15.0 8.0 12.5 ]
La méthode toString() est omniprésente pour une raison essentielle : chaque fois qu'un objet est concaténé avec une chaine à l'aide de l'opérateur "+", le compilateur Java invoque automatiquement la méthode toString() pour obtenir une représentation de l'objet sous forme de chaîne de caractères.
import java.awt.Point; import java.util.ArrayList; public class Calcul { public static void main(String[] args) { Personne moi = new Personne("REMY", "Emmanuel"); System.out.println("Mon nom est : "+moi); } } class Personne { private String nom, prénom; public Personne(String nom, String prénom) { this.nom = nom; this.prénom = prénom; } public String toString() { return getClass().getName()+"[nom="+nom+", prénom="+prénom+"]"; } }
Au lieu d'écrire x.toString(), vous pouvez écrire ""+x. Cette instruction concatène la chaîne vide avec la représentation textuelle de x, qui correspond exactement à la représentation obtenue par x.toString(). A la différence de toString(), cette instruction fonctionne, même si x est de type primitif.
Si x est un objet quelconque et que vous appeliez
System.out.println(x) ;
La méthode println() appelle simplement x.toString() et affiche la chaîne de carcatères résultante.
La méthode toString() est un outil précieux de consignation. de nombreuses classes de la bibliothèque de classes standard définissent la méthode toString() comme fournisseur d'informations utiles sur l'état d'un objet. Il est fortement recommandé d'ajouter une méthode toString() à chacune des classes que vous écrivez. Vous et tous les programmeurs utilisant vos classes apprécierez le support de consignation.
Nous avons déjà vu comment le mot clé final pouvait s'appliquer à des variables locales ou à des champs de classe. Il interdit la modification de leur valeur. Ce mot clé peut aussi s'appliquer à une méthode ou à une classe, mais avec une signification totalement différente :
Une méthode déclarée final ne
peut pas être redéfinie dans une classe dérivée.
.
Le comportement d'une méthode finale est donc complètement défini et il ne peut plus être remis en cause, sauf si la méthode appelle elle-même une méthode qui n'est pas déclarée final. L'écriture est la suivante :
public final void méthode() {
...
}
Une classe déclarée final ne peut plus être dérivée.
.
On est ainsi certain que le contrat de la classe sera respecté. L'écriture est la suivante :
public final class ExempleClasse {
...
}
On pourrait croire qu'une classe finale est équivalente à une classe non finale dont toutes les méthodes seraient finales. En fait, ce n'est pas vrai car :
Par son caractère parfaitement défini, une méthode finale permet au compilateur
En revanche, il va de soi que le choix d'une méthode ou d'une classe finale est très contraignant et ne doit être fait qu'en toute connaissance de cause. Beaucoup de classes intégrées dans le JDK sont finales, c'est le cas notamment de la classe String de la classe Math, etc...
Une classe abstraite est une classe qui ne permet pas d'instancier des objets. Elle ne peut servir que de classe de base pour une dérivation. Elle se déclare ainsi
abstract class A {
...
}
Dans une classe abstraite, on peut trouver classiquement des méthodes et des champs, dont héritera toute classe dérivée. Mais on peut aussi trouver des méthodes dites abstraites, c'est-à dire dont on ne fournit que la signature et le type de la valeur de retour. Par exemple :
Bien entendu, on pourra déclarer une variable de type A :
A a ; // OK : a n'est qu'une référence sur un objet de type A ou dérivé
En revanche, toute instanciation d'un objet de type A sera rejetée par le compilateur :
a = new A(...) ; // erreur : pas d'instanciation d'objets d'une classe abstraite
En revanche, si on dérive de A une classe B qui définit la méthode abstraite g :
on pourra alors instancier un objet de type B par new B(..) et même affecter sa référence à une variable de type A :
A a = new B(...) ; // OK
Malgré tout, A est considérée comme abstraite et une expression telle que new A(..) sera rejetée.
Ici, B définit f1, mais pas f2. La classe B reste abstraite (même si on ne l'a pas déclarée ainsi).
Le recours aux classes abstraites facilite largement la Conception Orientée Objet. En effet, on peut placer dans une classe abstraite toutes les fonctionnalités dont on souhaite disposer pour toutes ses descendantes :
C'est cette certitude de la présence de certaines méthodes qui permet d'exploiter le polymorphisme, et ce dès la conception de la classe abstraite, alors même qu'aucune classe dérivée n'a peut-être encore été créée. Notamment, on peut très bien écrire des canevas recourant à des méthodes abstraites. Par exemple, si vous avez défini :
vous pourrez écrire une méthode (d'une classe quelconque) telle que :
Bien entendu, la redéfinition de f devra, comme d'habitude, respecter la sémantique prévue dans le contrat de X.
Voici un exemple de programme illustrant l'emploi d'une classe abstraite nommée Affichable, dotée d'une seule méthode abstraite affiche(). Deux classes Entier et Flottant dérivent de cette classe. La méthode main() utilise un tableau hétérogène d'objets de type Affichable qu'elle remplit en instanciant des objets de type Entier et Flottant.
Résultat :
Je suis un entier de valeur 25
Je suis un flottant de valeur 1.25
Je suis un entier de valeur 42
Pour connaître les classes internes