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 de 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. Attention, si deux
objets
sont égaux, la valeur obtenue par la fonction de hachage doit
être
la même. Si vous redéfinissez equals(Object),
vous
devez donc redéfinir
hashCode() de façon à
ce que si pour deux objets o1 et o2 on a o1.equals(o2)
qui vaut true, alors o1.hashCode() == o2.hashCode()
vaille
également true.
- 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.
A lire