Classes et Objets
La programmation dans un style objet utilise la création dynamique
d'objets. Lorsque l'on utilise un langage objet comme Java ou C++, ces
objets sont les instances d'une classe.
Par exemple, il est apparu dans le code de Conversion.java
qu'il était intéressant de disposer d'objets représentant
des devises, composés d'un nom et d'un taux de change en Euro.
Le code correspondant est :
class Devise {
double tauxDeChangeEnEuro;
String nom;
}
tauxDeChangeEnEuro et nom sont les champs,
aussi appelés variables d'instance
de la classe Devise.
On peut alors créer des objets de la classe Devise
à l'aide de l'opérateur new :
Devise franc = new Devise();
franc.tauxDeChangeEnEuro = 6.55957;
franc.nom = "Francs";
Devise() est un constructeur,
et dans ce cas précis le constructeur fourni par défaut,
sans argument (on parle de constructeur vide). Un tel constructeur dans
le cas de Devise n'est pas souhaitable car on va répéter
après chaque instantiation les instructions d'affectation
des champs tauxDeChangeEnEuro et nom. Il convient donc
de fournir un constructeur prenant en paramètre les valeurs à
affecter aux champs.
D'autre part, l'encapsulation de données préconise que
les classes utilisant la classe Devise ne puissent avoir accès
directement à ses données. Le fait d'interdire cet accès
direct permettra de garantir la possibilité de représenter
les données sous une forme différente sans avoir à
réécrire les classes clientes. Il est donc nécessaire
d'interdire l'utilisation directe des champs de Devise par une
classe cliente, grâce au mot clé private et donc
en contrepartie, de fournir des méthodes permettant de récupérer
les données d'une devise et de les modifier si besoin.
Pour terminer, il paraît naturel de fournir directement une méthode
de conversion en Euro et une méthode de conversion depuis l'Euro
comme méthode de la classe Devise.
Contrôle d'accès
Les classes disposent de deux niveaux de contrôle
d'accès.
-
Par défaut, une classe n'est utilisable (par exemple instanciable)
que dans le code des classes appartenant à son paquetage (si aucun
paquetage n'est précisé, la classe appartient à une
sorte de paquetage "par défaut"). C'est le cas par exemple de la
classe Devise définie ci-dessus, ce qui n'est sans doute
pas le bon choix.
-
Si devant class, on trouve le mot clé public alors
la classe est utilisable partout.
De plus, les différents éléments d'une classe (variables
et méthodes) possèdent eux-mêmes quatre niveaux de
contrôle d'accès.
-
Lorsqu'un élément est déclaré privé,
grâce au mot-clé private, il n'est utilisable que
dans le code la classe auquel il appartient.
-
Par défaut, un élément n'est utilisable que dans le
code des classes qui appartiennent au même paquetage que sa propre
classe.
-
L'utilisation d'un élément peut-être étendue
aux classes hors-paquetage qui héritent de sa classe, grâce
au mot-clé protected (l'élément reste utilisable
dans tout le code du paquetage).
-
Enfin, un élément peut-être utilisable partout en utilisant
le mot-clé public.
Il faut remarquer que déclarer public un élément
d'une classe qui ne l'est pas ne modifie pas l'accessibilité de
l'élément.
Variables et méthodes de classe et d'instance
Lorsqu'un objet est instancié, ses champs sont accessibles à
partir d'un de ses identificateurs associé à l'opérateur
".".
Par exemple,
Devise d = new Devise();
d.nom = "Francs";
Si aucune valeur d'initialisation n'est précisée, la
valeur des champs lors de l'instantiation est selon leur type false,
0
ou null.
De même, l'appel d'une méthode d'instance d'une classe
doit être fait à travers l'objet concerné, via l'opérateur
".". Dans le code d'une méthode d'instance, l'appel d'une
méthode d'instance ou l'utilisation d'un champ, sans précision
de l'objet concerné, porte sur l'objet à partir duquel on
a appelé la méthode. De plus, si un champ n'est pas accessible
par ce moyen car "caché" par un paramètre portant le même
nom, il peut être utilisé à l'aide du mot-clé
this
et de l'opérateur ".".
Certaines variables et méthodes peuvent être communes
à toutes les instances, c'est-à-dire ne pas concerner une
instance en particulier. Ce sont les variables
et méthodes de classes, qui sont définies à
l'aide du mot-clé static. Dans une classe, ses propres
variables et méthodes de classes peuvent être utilisées
directement. A partir d'une autre classe, elles devront être utilisées
à l'aide de l'opérateur "." précédé
du nom de la classe ou d'un identificateur d'une instance de cette classe.
Toutefois, il est recommandé, pour une question de lisibilité,
de préférer l'usage du nom de la classe plutôt qu'un
identificateur d'instance, qui peut laisser penser qu'il s'agit d'une variable
ou d'une méthode d'instance et donc que le choix de l'identificateur
est important.
Plusieurs méthodes peuvent avoir le même identificateur,
avec des signatures (liste de paramètre) différentes. C'est
ce que l'on appelle la surcharge. Le
nom complet d'une méthode est donc son identificateur + sa signature.
Deux méthodes avec le même nom et des signatures différentes
peuvent avoir des retours de type différent.
Bloc d'initialisation statique
Il est possible dans le code d'une classe de déclarer des blocs
static
qui seront exécutés au chargement de la classe.
Utilisation de final
Le mot clé final devant un élément signifie
que celui-ci ne peut être modifié.
Dans le cas d'une variable de classe, il s'agira donc d'une constante
dont la valeur doit être spécifiée, éventuellement
par un appel de méthode, à la déclaration.
Pour les variables d'instance, il s'agit de champs dont la valeur doit
être spécifiée soit à la déclaration,
soit dans les premières instructions des constructeurs. Ces champs
ne pourront plus être modifés par la suite. Cette contrainte
a pour effet secondaire que, si l'on fournit plusieurs constructeurs, il
est impossible que l'un d'eux puisse en utiliser un autre, ce qui nuit
à la factorisation du code. Si les variables sont privées,
il est alors généralement préférable de renoncer
à l'utilisation de final. Notons que l'aspect constant
d'une variable ne concerne que sa valeur. Si cette valeur est une référence,
l'objet référencé pourra être modifié
: s'il s'agit d'une instance, il sera toujours possible de modifier l'un
de ses champs et s'il s'agit d'un tableau la valeur d'un de ses éléments.
Pour les méthodes, final aura pour effet d'interdire
à une classe héritière de redéfinir la méthode.
Ordre des mots-clés
Il n'y a pas d'ordre d'utilisation des mots-clés imposés
par le compilateur. Toutefois, il est important pour la lisibilité
de garder toujours le même ordre, et l'ordre préconisé
est celui mettant en avant l'importance du contrôle d'accès
:
<contrôle d'accès> <static> <final>
<type> identificateur
Tout objet est un Object ! Ces qui a deux conséquences
:
la première est que si une méthode doit porter sur n'importe
quel objet, le paramètre déclaré aura pour type Object.
La deuxième conséquence est que tout objet possède,
par un mécanisme d'héritage implicite, les méthodes
suivantes :
-
boolean equals(Object obj)
Cette méthode teste l'égalité de deux objets.
Le code fourni par la classe Object est basé sur l'égalité
physique de deux objets. Deux objets seront égaux si et seulement
si ils ont la même adresse. En d'autre terme, exécuter equals
revient à faire un ==. Si l'on désire que l'égalité
de deux objets soit une égalité sémantique, par exemple
qu'ils aient les mêmes valeurs dans leurs champs, il faut redéfinir
cette méthode au niveau de la classe. C'est ce qui est fait par
exemple pour la classe String. Si l'on instancie deux String de
la façon suivante :
String s1 = new String("Bonjour");
String s2 = new String("Bonjour");
alors
s1 == s2 a pour valeur false et s1.equals(s2)
a pour valeur true.
-
Class getClass()
Cette méthode renvoie la classe de l'objet (toute classe est
une instance de la classe Class).
-
int hashCode()
Cette méthode renvoie une valeur de hachage, utilisée
en particulier par la classe Hashtable. Il est vraisemblable que
si vous redéfinissez equals(Object), vous redéfinirez
hashCode().
-
String toString()
Cette méthode renvoie une chaîne de caractère représentant
l'objet. En particulier, System.out.println(Object o) utilise
cette méthode. La chaîne de caractère retournée
par Object.toString() est composée de la classe de l'objet
concerné suivi de son adresse. Par exemple
Devise d = new Devise();
System.out.println(d);
affichera
Devise@bd15cb1f
d'ou l'on peut déduire que d est physiquement stocké
à l'adresse bd15cb1f.
-
protected Object clone()
Cette méthode permet la duplication d'un objet, via l'utilisation
de l'interface Cloneable. Son utilisation telle quelle engendrera
la levée d'une exception CloneNotSupportedException.
Enfin, la classe Object fournit également les méthodes
finalize(), liée à la destruction d'un objet
par le ramasse-miette,
notify(), notifyAll() , wait(), wait(long)
et wait(long, int) liées à l'utilisation des processus
légers (thread).
Destruction d'objets
Java utilise un ramasse-miette pour
détruire les objets. Le ramasse-miette a pour but de récupérer
la mémoire utilisée par des objets qui ne sont plus référencés
depuis la pile d'exécution. L'utilisation de cette technique supprime
ainsi l'une des principales causes de bogues.
Il arrive parfois lors de la suppression d'un objet qu'il faille faire
d'autres actions que récuperer sa mémoire et détruire
des objets référencés uniquement à partir de
cet objet. C'est par exemple le cas lorsque l'objet est un identificateur
de flot de caractères obtenu par l'ouverture d'un fichier en lecture.
Sa destruction doit entraîner la fermeture du fichier. Ceci peut
être fait grâce à la méthode finalize()
qui est appelée depuis un objet quand celui-ci est détruit.