Next: 7 Héritage
Up: Java: Le langage
Previous: 5 Les structures de
Subsections
Dans le langage C , on a l'habitude
créer des objets complexes à l'aide de structures . Dans les
langages orientés objets, on créera plutôt des classes . Les
classes constituent le concept de base de le programmation
objet. Elles permettent de définir des nouveaux types de données qui
doivent se comporter ``comme'' des types pré définis et dont les
détails d'implantation sont cachés aux utilisateurs de ces classes
Seule l'interface fournie par son concepteur pourra être
utilisée. Comme nous le verrons plus loin, contrairement aux types
prédéfinis, les classes peuvent être créées de manière hiérarchique :
une classe est souvent une sous-classe d'une autre classe.
Un objet est une
instance d'une certaine classe ; au lieu de parler d'une variable
d'une certaine classe, on dira plutôt un objet d'une certaine classe.
En Java , on ne peut accéder aux objets qu'à travers une
référence vers celui-ci. Une référence est, en quelque
sorte, un pointeur vers la structure de donnée ; la différence entre
une référence et un pointeur est qu'il n'est pas permis de manipuler
les références comme les pointeurs de C ou C++ . On ne peut
connaître la valeur de la référence et on ne peut évidemment pas
effectuer d'opérations arithmétiques sur les références. La seule
chose permise est de changer la valeur de la référence pour pouvoir
``faire référence'' à un autre objet.
Une classe définit généralement deux choses :
- les structures de données associées aux objets de la classe ;
les variables désignant ses données sont appelés champs.
- les services que peuvent rendre les objets de cette classe qui
sont les méthodes définies dans la classe.
Java (comme C++ ) possède trois mots clés pour l'encapsulation des
données : public, private et protected.
Les données et méthodes déclarées private ne sont accessibles
que par les méthodes de sa propre classe. Inversement les
informations déclarées public sont accessibles par toutes les
classes. Nous verrons plus loin la signification du mot clé
protected.
Imaginons que l'on veuille déclarer une structure de donnée
Date constituée de trois entiers codant le jour, le mois et
l'année. Nous allons pour ce faire, définir une classe Date
de la manière suivante :
class Date {
private int mois ;
private int jour ;
private int année ;
...
}
|
Les données mois, jour et année sont
des données privées. Elles ne sont accessibles que par les seules
méthodes de cette classe. Pour que l'on puisse modifier ces
champs, il faut que l'on fournisse les méthodes permettant de
manipuler ces données privées.
Comme en C++ , les méthodes
sont définies par :
- un nom constitué par un identificateur
- des paramètre formels : ceux-ci sont séparés par des
``,''. Lorsque la méthode n'a pas de paramètre, contrairement au
langage C , il ne faut pas préciser void. Le nombre de
paramètres est fixe : il n'est pas possible de définir des méthodes
à arguments variables.
- du type du retour est soit void (si la méthode
ne retourne aucune valeur), soit un type primitif ou une référence
vers un objet.
- du corps de la méthode.
Comme les champs d'une classe, les méthodes doivent être qualifiées de
public, private ou protected. Les méthodes
private ne peuvent être invoquées que par les seules méthodes de cette
classe.
class Date {
private int mois ;
private int jour ;
private int année ;
...
public void affecter(int m, int j, int a) {
mois = m ; jour = j ; année = a ;
}
}
|
La méthode affecter fait
partie de la classe Date ; il lui est donc permis
d'accéder aux champs privés mois, jour et
année. Grâce à cette méthode affecter, puisque elle est
déclarée public, on pourra désormais affecter les champs
mois, jour et année d'un objet de type
Date. Les méthodes publiques de la classe Date
constituent l'interface publique de cette classe.
class Date {
private int mois ;
private int jour ;
private int année ;
public void affecter(int m, int j, int a) {
mois = m ; jour = j ; année = a ;
}
public int QuelJour() { return jour ; }
public int QuelMois() { return mois ; }
public int QuelleAnnée() { return année ; }
public void JourSuivant() { ... }
public void imprimer() { ... }
}
|
Contrairement au langage C++ , la définition effective des
méthodes de la classe doit se faire dans la définition de la
classe elle-même.
class Date {
...
public void imprimer() { // imprimer la date
System.out.println(jour + "/" + mois + "/" + année) ;
}
}
|
Une fois la classe déclarée, pour pouvoir utiliser un
objet de cette classe, il faut définir une instance (ou un
objet ) de cette classe. Nous avons dit que les objets ne sont
accessibles qu'à travers des références . Une définition qui se
contente de définir un objet comme ``une variable ayant le type
de la classe choisie '' ne fait que définir une référence vers un
éventuel objet de cette classe.
La variable d représente une référence vers un objet de
type Date ; Si l'on veut un objet effectif, il faut la
créer explicitement avec le mot clé new et le
constructeur de la classe Date.
Date d ;
d = new Date() ;
|
Comme on l'a déjà dit, une méthode est un message que l'on envoie
à un objet. Ainsi, pour afficher la date contenue dans l'objet
d, on lui envoie le message imprimer :
De telles méthodes sont appelées méthodes d'instance ; nous
verrons plus loin qu'il existe un autre type de méthode qu'on
appelle méthode de classe .
Cette méthode n'est utilisable (ailleurs que les méthodes de la
classe Date) que parce qu'elle fait partie de l'interface
de cette classe i.e. qu'elle fait partie des méthodes
publiques. Par contre, il ne sera pas possible d'accéder aux
champs d.jour, d.mois et d.annee : ce
sont des données privées .
Les structures de données d'une classe donnée, sont dupliqués pour
chaque objet de cette classe. Si l'on définit deux objets de type
Date, ils possèdent, tous les deux, leur propre
exemplaire des champs jour, mois et
année. La modification d'un de ces champs pour un objet
n'affecte évidemment pas la valeur du même champ pour l'autre
objet. De tels champs sont appelés variables d'instance.
Nous verrons plus loin que nous pourront définir des champs où
toutes les instances d'une même classe partagent le même champ
(champs static).
Quant aux méthodes, elles ne sont évidemment pas dupliquées pour
chaque objet. Un seul exemplaire de toutes les méthodes définies
dans une classe suffit. On remarquera également (pour le moment
du moins) que les méthodes ne peuvent être invoquées qu'en
utilisant un objet. Nous verrons plus loin que des méthodes
particulières n'obéissent pas à cette restriction (méthodes
static).
Lorsque l'on définit
un objet d'une classe, il est souvent utile de pouvoir initialiser cet
objet. Avec la définition de notre classe Date, il est
évidemment possible, avec la méthode affecter, d'affecter les
champs jour, mois et année.
Date d ;
d = new Date() ;
d.affecter(10, 3, 87) ;
|
Mais cette façon de faire, n'est pas la plus agréable. Une
meilleure façon de faire consiste à définir une méthode spécifique
d'initialisation des champs qui sera automatiquement appelée lors
de la création d'un objet. Cette fonction s'appelle constructeur.
class Date { ...
public Date(int j, int m, int a) { jour = j ; mois = m ; année = a ; }
}
|
Notez que le constructeur se reconnaît par le fait qu'il porte le
même nom que la classe et qu'il n'a pas de valeur de retour (pas
même void). Voici, à présent, des exemples d'utilisation
correcte et incorrecte :
Date noel97, dateDeNaissance ;
noel97 = new Date(25, 12, 97) ; // Correct
dateDeNaissance = new Date() ; // Incorrect
|
La création de l'objet référencé par dateDeNaissance est
incorrecte. En effet, le constructeur de la classe Date
dont nous disposons requiert trois arguments ; il n'est donc pas
possible de créer un objet de la classe Date sans donner
le jour, le mois et l'année en argument. On doit contourner ce
problème en fournissant soit plusieurs constructeurs (à 0, 1, 2 et
3 arguments) :
class Date {
...
public Date(int j, int m, int a) { jour = j ; mois = m ; année = a ; }
public Date(int j, int m) { jour = j ; mois = m ; année = 57 ; }
public Date(int j) { jour = j ; mois = 9 ; année = 57 ; }
public Date() { jour = 15 ; mois = 9 ; année = 57 ; }
}
|
ou plus simplement par :
class Date {
...
public Date(int j, int m, int a) { jour = j ; mois = m ; année = a ; }
public Date(int j, int m) { this(j, m, 75) }
public Date(int j) this(j, 9, 57) ; }
public Date() { this(15, 9, 57) ; }
}
|
Pourquoi alors, avant la définition de nos propres constructeurs,
nous avons pu créer un objet de type Date sans lui passer
de paramètres ? Il ne vous rester plus qu'arriver à la section
concernant les constructeurs par défaut
(voir 7.2.2).
Plus que pour l'initialisation des membres de la classe, le
constructeur est particulièrement utile lorsque l'objet que l'on
veut créer requiert d'autres structures de données alloués
dynamiquement. Par exemple, une fenêtre graphique est un objet
constitué de la fenêtre elle-même et d'autres objets tels que des
boutons, des menus et autres gadgets graphiques. Le constructeur
d'un tel objet se chargera donc de créer tous les autres objets
dont il a besoin.
Imaginons que l'on veuille compléter la classe Date avec
une chaîne de caractères qui précise un fait marquant associé à un
objet de la classe Date. L'initialisation des variables
d'instances se fait dans l'ordre suivant :
- Initialisation des valeur par défaut en fonction des types des
variables d'instances
- Initialisation des valeurs explicitement fournies lors de la
définition de la classe
- Appel au constructeur de la classe.
Contrairement aux langage C++ , nous allons pouvoir,
dans beaucoup de cas, ne plus nous soucier de la restitution de
l'espace mémoire consommée. Java dispose d'un système de
récupération de mémoire automatique. Java estime que
l'espace occupé par objet peut être restitué au système quand il n'y a
plus aucune référence vers cet objet.
Par défaut, le récupérateur de mémoire fonctionne en arrière
plan pendant l'exécution d'un programme Java . Il est possible de
supprimer cette récupération en donnant l'option -noasyngc
sur la ligne de commande du lancement de la machine virtuelle. La
récupération de mémoire peut alors être invoquée explicitement pas le
programmeur à des moments bien précis avec la méthode
System.gc().
Avant l'appel effective à la
récupération d'un objet, la machine virtuelle Java fait appel à la
méthode finalize. A quoi donc peut servir cette méthode
puisque la récupération de mémoire se charge de tout ? La raison en
est simple : Java peut s'occuper de la récupération des objets
Java et rien d'autre.
Par exemple, un programme qui utilise des ressources systèmes (les
descripteurs de fichiers, les sockets , etc.), c'est lors de
l'invocation de la méthode finalize que le programmeur se chargera de
libérer ses ressources ; Java ne pourra pas le faire tout seul. Une
classe qui contient la méthode finalize devra avoir le
squelette suivant :
protected void finalize() throw Throwable {
super.finalize() ;
// Code propre aux objets de cette classe.
...
}
|
Prenez ce bout de code tel quel, même s'il y a beaucoup de choses
mystérieuses. Après la lecture des chapitres sur l'héritage et
les exceptions, tout ceci devrait s'éclaircir.
Les objets des classes que nous avons
définis avaient leur propre jeu de données privées et
publiques . Par exemple, les objets de la classe Date
possèdent chacun leur propre champ jour, mois et
année.
Il existe des cas où il est souhaitable d'avoir une même donnée qui
soit commune à tous les objets d'une classe. Un champ d'une classe
est dit static lorsqu'il n'y a qu'un exemplaire de ce champ
pour l'ensemble des objets de cette classe. Ce champ existe même s'il
n'existe aucun objet de cette classe. Les champs static sont
parfois appelés variables de classe commence à exister à partir
du moment ou une classe est initialisée.
class Date {
private int mois ;
private int jour ;
private int année ;
public static int nbDate = 0 ;
public Date(int m, int j, int a) {
mois = m ; jour = j ; année = a ;
nbDate++ ;
}
...
public static void main(String args[]) {
Date noel97 = new Date(25, 12, 97) ;
Date dateDeNaissance = new Date(15, 9, 57) ;
noel97.imprimer() ;
dateDeNaissance.imprimer() ;
System.out.println(noel97.nbDate) ;
System.out.println(dateDeNaissance.nbDate) ;
}
}
|
Dans cet exemple, Les champs jour, mois et
année des objets noel97 et
dateDeNaissance sont des variables d'instance ;
autrement dit, chacun de ces objets possède leur propre instance
de ces champs. La modification d'un de ces champs pour un objet
donné n'influe pas sur ce même champ de l'autre objet. Par
contre, le champ nbDate est déclaré static,
c'est donc une variable de classe . Les deux objets
noel97 et dateDeNaissance partagent alors la
même structure de donnée. Si l'on modifie la valeur de ce champ à
partir d'un objet, cette modification est valide pour l'autre
objet. C'est ainsi que dans cet exemple, on peut compter le
nombre d'objets de type Date que l'on crée en
incrémentant la valeur du champ nbDate dans le
constructeur de la classe Date. On obtient donc le
résultat suivant :
25/12/97 noel97.imprimer()
15/9/57 dateDeNaissance.imprimer()
2 System.out.println(noel97.nbDate) ;
2 System.out.println(dateDeNaissance.nbDate) ;
|
Les champs static sont initialisés une fois lors de
l'initialisation de la classe qui les contient. Une erreur de
compilation se produit lorsque
- une variable de classe est initialisée par référence à une
variable de classe définie plus loin (textuellement) dans la
définition de la classe.
class X {
static int x = y + 1 ; // Erreur ! y est déclaré après x
static int y = 0 ; // O.K.
static int z = z+1 ; // Erreur !
}
|
- une variable de classe est initialisée par référence à une
variable d'instance de la classe.
class X {
public int x = 120 ;
static int y = x + 10 ; // x est une variable d'instance
}
|
Les champs non static (variables d'instance) sont initialisés
lors de la création des objets (des instances) de la classe.
Contrairement aux champs static, chaque création d'objets
provoque l'initialisation des variables d'instances de cet objet. Une
erreur de compilation se produit lorsque une variable d'instance est
initialisée par référence à une variable d'instance définie plus loin
(textuellement) dans la définition de la classe. On pourra utiliser
les valeurs des variables de classe pour initialiser les variables
d'instance puisque la création et l'initialisation de la classe se
fait bien avant la création des objets de la classe.
class X {
int x = y + 1 ; // Erreur !
int y = 0 ; // O.K. !
int z = z+1 ; // Erreur !
}
|
Le mot clé this désigne
l'objet sur lequel la méthode est invoquée. Par exemple, la méthode
affecter peut se réécrire de la manière suivante :
public void affecter(int m, int j, int a) {
this.mois = m ; this.jour = j ; this.annee = a ;
}
|
L'intérêt du mot clé this n'est évident pas dans ce cas
là ; par contre si l'on voulait créer une liste de toutes les
objets de type Date crées, on ne pourra se passer de ce
mot clé this pour créer le chaînage.
class Date {
private int mois ;
private int jour ;
private int année ;
private Date suivant ;
public static Date listeDates = null ;
public Date(int m, int j, int a) {
mois = m ; jour = j ; année = a ;
suivant = listeDates ;
listDates = this ;
}
...
}
class Test {
public static void main(String args[]) {
Date noel97 = new Date(25, 12, 97) ;
Date dateDeNaissance = new Date(15, 9, 57) ;
for (Date d = Date.listeDates ; d != null ; d = d.suivant)
d.imprimer() ;
}
}
|
Un champ d'une classe peut être qualifié de final. Ce
qualifier indique au compilateur que ce champ ne peut être modifié et
gardera tout au long de son existence une valeur constante. Le
compilateur produira donc une erreur lorsqu'il y aura une tentative de
modification de la valeur de ce champ. L'intérêt de ce qualifier est
triple :
- C'est une aide à la programmation. En précisant que la valeur
de cette donnée ne peut changer, on se prémunit de certaines erreurs
de programmation.
- C'est une aide pour le compilateur. En effet, sachant que le
champ conservera une valeur constante tout au long du programme, le
compilateur peut effectuer toutes les optimisations pour produire un
code efficace.
- Les champs final sont en fait des constantes et peuvent
apparaître partout où une constante est attendue. *********
Si l'on ne peut modifier la valeur de champ, comment lui donner une
valeur initiale ? Et bien, en l'initialisant ! En effet, seule
l'initialisation de ce champ est permise ; s'il s'agit d'une donnée
primitive c'est une initialisation classique et s'il s'agit d'une
référence alors l'initialisation lui affectera une référence un objet.
Une référence qualifiée de final n'interdit pas la
modification de l'objet référencé ; l'objet pourra être modifié mais
la référence désignera toujours le même objet.
Tous les champs peuvent être qualifiés de final, que ce soit
des variables de classes ou des variables d'instance.
Lors des appels aux méthodes,
tous les paramètres sont passés par valeur . Le concept de
passage par adresse n'existe pas. Rappelons que les seuls
types possibles de paramètres sont les types primitifs et les
références. Autrement dit,
- les types primitifs (les entiers, les booléens et les flottants)
sont passées toujours par valeur. C'est la valeur du paramètre
(i.e. la copie d'une constante ou du contenu d'une variable) qui est
passée en paramètre à la méthode invoquée. Une méthode ne peut donc
jamais modifier la valeur d'une variable de type primitif du code
appelant.
- les références également sont passés par valeur. Ce qui est
passé ici en paramètre, c'est la valeur de la référence et jamais
l'objet lui-même. Une méthode peut donc modifier cette copie de la
valeur de la référence sans que cela modifie la valeur de la
référence du code appelant. Par contre, si la méthode modifie un
champ de l'objet référencé par cette valeur, c'est l'objet (qui lui
n'est peut être passé par valeur) qui est modifié pour tout
référence vers cet objet. Le code appelant se voit donc l'objet
référencé modifié.
Contrairement aux langage C , un même identificateur peut être utilisé
par désigner deux méthodes à condition que leur signature soit
différente. On appelle signature d'une méthode, la donnée de
son nom, du nombre de ses paramètres formels et de leur type.
int une_méthode(int i) { ... } // Erreur ! Le retour de la méthode
float une_méthode(int i) { ... } // ne fait pas partie de la signature
int une_méthode(int i) { ... } // O.K. !
float une_méthode(float f) { ... } // Ces deux méthodes ont des signatures distinctes
int une_méthode(int i) { ... } // O.K. !
int une_méthode(int i, int j) { ... } // Ces deux méthodes ont des signatures distinctes
|
Les variables locales sont allouées lors de l'invocation de la
méthode et sont détruites à la fin de celle-ci. Ces variables ne sont
visibles qu'à l'intérieur de la méthode.
Les variables locales doivent avoir été affectées avec leur
utilisation ; dans le cas contraire, une erreur de compilation est
engendrée. Cette valeur peut être donnée par initialisation (à la
définition de la variable) ou par affectation. Ces exigences, loin
d'être des contraintes, sont des aides précieuses pour le programmeur
!
void une_méthode() {
int i ; j , k ;
j = i ; // Erreur de compilation !
if (...) {
k = 1 ;
}
j = k ; // Erreur de compilation !
}
|
Lorsqu'une méthode est invoquée par différents threads
(voir 20), chaque thread possède ses propres variables
locales et paramètres.
Un objet référencé par une variable locale, peut continuer à
exister après la fin de la méthode, même si cet objet a été crée
dans cette méthode. Cet objet sera restitué au système que
lorsqu'il n'y a plus aucune référence vers cet objet.
Jusqu'à présent, les méthodes que nous avons vues s'appliquent
toujours sur un objet ou plus exactement sur une référence vers un
objet. Les méthodes qu'on qualifie de static sont celles qui
n'ont pas besoin d'un objet pour être invoquée. Ces méthodes se
rapprochent des fonctions classiques du langage C et sont appelés
méthodes de classe .
Comme toutes les méthodes, une méthode static est toujours
membre d'une classe ; elle est invoquée en lui associant, non pas un
objet, mais la classe à laquelle elle appartient. Par exemple, la
méthode sqrt qui calcule la racine carrée d'un nombre,
appartient à la classe Math. Pour invoquer cette méthode, on
utilisera la syntaxe suivante :
Math.sqrt(x) ; // Math désigne non pas un objet, mais une classe
|
Une méthode static, puisqu'elle ne s'applique pas sur un
objet, ne peut accéder aux variables d'instances (sauf de celles
passées en paramètre). De même, le mot clé this n'a pas
de sens dans une méthode static.
class Date {
private int mois ;
private int jour ;
private int année ;
private Date suivant ;
private static Date listeDates = null ;
public Date(int m, int j, int a) {
mois = m ; jour = j ; année = a ;
suivant = listeDates ;
listDates = this ;
}
...
public static void listerDates() {
for (Date d = Date.listeDates ; d != null ; d = d.suivant)
d.imprimer() ;
}
}
class Test {
public static void main(String args[]) {
Date noel97 = new Date(25, 12, 97) ;
Date dateDeNaissance = new Date(15, 9, 57) ;
Date.listerDates() ;
}
}
|
Les variables statiques peuvent être initialisées lors de leur
déclaration. Il est parfois utile de disposer, non pas de simples
initialisations, mais d'un ensemble d'instructions plus complexes pour
réaliser l'initialisation des champs statiques. On peut faire le
parallèle avec les variables d'instances. Celles-ci disposent du
constructeur pour effectuer des initialisations complexes. De même,
les blocs d'initialisation statique permettent d'effectuer des
initialisations complexes sur les champs statiques.
Contrairement aux constructeurs, les blocs d'initialisation statique
n'ont pas une syntaxe proche d'une méthode. En effet, ces
initialisations ont lieu au moment du chargement d'une classe
et c'est le système qui se charge d'invoquer ces initialisations
automatiquement au chargement. Il n'est donc pas question de passer
des paramètres à ces initialisations. De plus, comme pour les
constructeurs, les initialisations ne retournent aucune valeur. Bref,
les blocs d'initialisation peuvent être comparées à des
méthodes sans paramètres et sans valeur de retour ; il n'est donc pas
utile de les nommer. C'est ainsi que la syntaxe des initialisations
statiques ne ressemble en rien à des méthodes ; il ne s'agit que de
blocs d'instructions préfixés par le mot clé static.
Comme nous le verrons plus en détails dans le
chapitre 12, il n'est pas possible de transformer les
types primitifs en objets par une opération de changement de type
(cast ). Par contre, Java définit pour des classes spéciales
pour un certain nombre de types primitifs (Integer,
Float, Boolean, etc.. Par exemple, on créera une
instance de la classe Integer ayant pour valeur 10 de la
manière suivante :
Integer instanceInteger = new Integer(10);
|
Inversement la classe Integer dispose de méthode qui
permettent d'obtenir la valeur du champ entier d'une instance de
cette classe.
int i = instanceInteger.intValue(); // retourne 10
|
Une description détaillée de ces classes est donnée
en 17.2.
Next: 7 Héritage
Up: Java: Le langage
Previous: 5 Les structures de
Touraivane
6/12/1998