Les concepteurs de langages cherchent les moyens de faciliter la création et la maintenance du code. Une méthode souvent retenue consiste à structurer le langage au moyen de « procédures » qui sont des blocs de code effectuant une action bien précise sur des données du programme. Il est possible de regrouper ces procédures en bibliothèques utilisables par d’autres programmeurs. Cette façon de faire est efficace mais présente l’inconvénient de séparer le code et les données et d’être sensible aux effets de bord. L’idée de base des langages objets est de regrouper données et code en une même entité l’objet. Cette réunion données-code se nomme « encapsulation ».
Quelques définitions relatives aux classes :
Classe : c’est le moule qui permet de fabriquer les objets.
Objet : c’est une réalisation concrète et utilisable de la classe. En jargon objet, on dit que c’est une « instance » de la classe.
Méthodes : ce sont des procédures qui réalisent les actions que l’on peut effectuer sur l’objet.
Message : c’est l’appel d’une méthode de l’objet.
Nous allons illustrer la description des classes par un exemple simple. Nous allons créer la classe « rectangle » qui affiche et manipule des rectangles.
La syntaxe de la déclaration d’une classe est :
class rectangle {
. . .
}
Les variables d’instance sont les données propres à chaque objet (instance de la classe). Les variables relatives à deux objets différents sont physiquement distinctes : elles occupent des cases mémoires différentes.
Pour définir un rectangle, il faut connaître la position d’un sommet (supérieur gauche par exemple) sa largeur et sa hauteur.
class rectangle {
int orx,ory,large,haut;
. . .
}
Les méthodes vont déterminer le comportement des objets rectangle.
La
syntaxe de la déclaration d’une méthode est :
Type
du résultat produit par la méthode ou void si elle ne produit pas de résultat ;
Le nom de la méthode ;
Le
type et le nom des arguments placés entre parenthèses ( ). Les parenthèses même
vides sont obligatoires.
Le
mode de passage des arguments des
méthodes est précisé dans une note .
Un
bloc d’instruction qui constitue la méthode. Sauf pour les méthodes de type
void, le bloc de code est terminé par return le_resultat.
La première méthode calcule la surface du rectangle :
int surface()
{ int surf=large*haut;
return surf;}
La seconde dessine le rectangle en noir. On utilise ici des instructions de java.awt qui est un ensemble de classes utilitaires livrées avec JAVA. L’instruction ”g.setColor(Color.black)” signifie que la couleur du pinceau pour écrire dans l’objet graphique g est la couleur Color.black.
void dessine()
{ g.setColor(Color.black);
g.drawRect(orx, ory, large, haut);}
L’objet graphique g doit être défini comme variable pour la classe par l’instruction « Graphics g ; ».
C’est une méthode particulière qui indique comment initialiser la classe.
Cette
méthode doit porter le nom de la classe.
rectangle(Graphics appG, int x, int y, int l,
int h)
{ g= appG;
orx = x; ory = y; large = l; haut = h; }
Pour chaque nouvel objet rectangle, ce constructeur initialise ses variables d’instances avec les valeurs des paramètres passées au constructeur : appG, x, y, l et h.
Dans une classe, la présence des constructeurs est facultative.
Voici le code complet de notre classe rectangle.
class
rectangle{
int orx,ory,large,haut; //variables
d'instance
Graphics
g;
rectangle (Graphics appG, int x, int y,int l, int h) //constructeur
{
g=appG;
orx=x; ory
= y; large = l; haut = h;}
int surface() //méthodes
{ int
surf=large*haut;
return surf;}
void dessine()
{
g.setColor(Clor.black);
g.drawRect(orx, ory, large,
haut);}
}
L’invocation d’une méthode se fait en donnant le nom de l’instance puis celui de la méthode en précisant les arguments (éventuels) de celle-ci.
Si la liste des arguments est vide, la présence des parenthèses est quand même nécessaire.
Exemple :
Pour dessiner un rectangle de nom rec,
le message d’appel à la méthode sera : rec.dessine(
) ;
Pour pouvoir utiliser la classe rectangle, nous allons créer une applet dont le détail du code sera explicité ultérieurement.
import
java.applet.*;
import java.awt.*;
public class testrect extends Applet
{
rectangle r1,r2; //
note 1
public init()
{
setBackground(Color.ligthGray);
r1=new rectangle(getGraphics(),10,20,100,50);
//note 2
r2=new
rectangle(getGraphics(),10,80,100,60)}; //note
3
public void paint(Graphics
g)
{ int x=r1.surface(); //note
4
g.drawString(""+x,250,100);
//note 5
r1.dessine();
//note
6
r2.dessine();}
}
Ce programme (applet) est une classe qui ne possède pas de constructeur et deux méthodes que l’on retrouve dans toute les applets. Au démarrage de l'applet, le navigateur appelle la méthode init( ) puis la méthode paint ( ) qui dessine dans la fenêtre octroyée par le navigateur à l’applet.
Cette applet sera appelée par une page html dont le code (minimal) sera :
<html>
<head>
<title>testrect</title>
</head>
<body>
<applet
code=testrect.class,
width=320, height=240 ></applet>
</body>
</html>
Le concepteur de la classe rectangle se rend compte qu’en plus des rectangles, il
devra manipuler aussi des rectangles pleins de diverses couleurs. Au lieu de
créer une nouvelle classe, il est possible en JAVA de créer un nouveau
constructeur. Il aura le même nom que le constructeur initial mais
devra différer de celui-ci soit par le nombre de ses arguments, soit par
leur type.
Voici une forme
possible de ce nouveau constructeur (qui possède l’argument supplémentaire couleur
du type Color qui est un des nombreux types de java.awt).
rectangle
(Graphics appG,int x, int y, int l, int h, Color couleur)
{ g = appG;
orx
= x; ory =y; large = l; haut = h;
plein = true;
col =couleur;}
Il faut ajouter deux variables d’instance à la classe rectangle : la variable col qui est de type Color et le booléen plein qui sera utilisé de façon interne à la classe afin de distinguer les rectangles simples des rectangles pleins. On pourra modifier la méthode dessine( ) pour prendre en compte les deux types de rectangles possibles.
public
void dessine()
{ if (plein){
g.setColor(col);
g.fillRect(orx,ory,large,haut);}
g.setColor.(Color.black);
g.drawRect(orx,ory,large,haut);}
Il est aussi possible de modifier la méthode surface pour lui faire afficher dans le rectangle la valeur de sa surface.
public
int surface()
{ int sur = large*haut;
this.dessine();
// ou dessine( )
g.drawString(""+sur,orx+10,ory+20);
return
sur;}
Dans certains cas, il peut y avoir ambiguïté sur l’instance à prendre en compte. Le mot clé this permet de préciser que la méthode doit être appliquée à l’instance en cours d’utilisation.
Les variables d’instance sont accessibles directement par toutes les méthodes de la
classe. Pour respecter l’encapsulation, il ne faut pas que les données d’un
objet puissent être modifiées directement depuis l’extérieur de celui-ci. Pour
y parvenir, on peut limiter la portée des variables de classe (et des méthodes)
au moyen de modificateurs. Dans le cas présent, les modificateurs
utilisables sont private qui limite la portée des variables à la classe
et public qui l’étend à toutes les classes.
Une
variable de classe déclarée public est accessible depuis l’extérieur de
la classe en préfixant son nom par celui de l’objet. Par défaut, variables et
méthodes sont public.
Comme exemple de l’utilisation des modificateurs, la variable col est déclarée public :
elle est accessible à partir de la classe testrect en utilisant la
syntaxe : Nom_de_l’instance.col = valeur. (noter le point entre le
nom de l’objet et la variable).
Par
contre, les autres variables étant déclarées private sont inaccessibles
depuis testrect : la ligne r1.orx
= 50 ; placée dans paint provoque une erreur de compilation
puisque « orx » a été déclarée private.
import
java.applet.*;
import java.awt.*;
class
rectangle1
{ private Graphics g;
private
int orx,ory,large,haut;
private
static float echelle=1.0f;
public Color col;
private
boolean plein;
rectangle1(Graphics
appG,int x, int y, int l, int h)//constructeur
1
{ g=appG;
orx=x;
ory=y; large=l; haut=h;
plein=false;}
rectangle1(Graphics
appG,int x, int y, int l, int h,Color couleur) //constructeur
2
{ g=appG;
orx=x;
ory=y; large=l; haut=h;
plein=true; col=couleur;}
public
void dessine()
{ if (plein){
g.setColor(col); g.fillRect(orx,ory,large,haut);}
g.setColor(Color.black);
g.drawRect(orx,ory,large,haut);}
public
void tourne()
{ int temp;
temp=large;
large=haut; haut=temp;}
public
void bouge(int dx, int dy)
{ orx += dx; ory
+= dy;}
public
void change(int l, int h)
{ large = l; this.haut
= h;}//this est facultatif
public
int surface()
{ int sur=(int)(large*haut*echelle);
this.dessine();
g.drawString(""+sur,orx+10,ory+20);
return
sur;}
}
//******************************************
public
class testrec1 extends Applet
{ rectangle1 r1,r2;
public
void init()
{ setBackground(Color.lightGray);
r1=
new rectangle1(getGraphics(),10,20,100,50);
r2= new
rectangle1(getGraphics(),10,80,100,60,Color.red);}
public
void paint(Graphics g)
{ int x = r1.surface(); g.drawString(""+x,250,100);
r1.dessine();
r2.dessine();
r1.tourne();
r1.bouge(180,100);
r1.dessine();
r2.bouge(20,30);
r2.col
= Color.blue;
r2.change(20,40);
r2.dessine();}
}
Cliquer dans le cadre de l'applet pour l'activer
De la même façon qu’il est possible de définir plusieurs constructeurs, il est
possible de définir deux méthodes ayant le même nom à la condition que leur
nombre d’arguments ou que les types des arguments diffèrent. Cette possibilité
s’appelle la surcharge des méthodes.
Exemple : Afin
de créer une homothétie, on peut surcharger la méthode :
public
void change(int l, int h)
{ large = l; haut = h;}
de la manière suivante :
public
void change(float k)
{ large = (int)(large*k).
haut = (int)(haut*k);}
Dans la classe rectangle, on pourrait définir la variable nombre_d_or. Cette
variable aura la même valeur pour toutes les instances de la classe. Il est
dont inutile de la dupliquer dans toutes les instances. Ceci est obtenu en
déclarant cette variable static . Les variables statiques,
déclarées static sont dites variables de classe.
Les
variables de classe sont initialisées une fois pour toute lors du chargement de
celle-ci.
Dans l’exemple précédent, la variable Graphics
g ayant la même valeur pour toute les instances peut être déclarée static.
Elles sont utilisables directement dans toutes les
méthodes de la classe.
Elles sont aussi utilisables
depuis l’extérieur de la classe. Leur nom doit alors être préfixé par le nom
de la classe (et pas par le nom d’une instance !)
Ce sont des méthodes destinées à agir sur la classe plutôt que sur les instances. On doit
les déclarer static. Pour accéder à une méthode statique depuis
l’extérieur de la classe, il faut préfixer le nom de la méthode par le nom
de la classe ou par le nom d'une instance de la classe.
Pour améliorer la lisibilité
du code il est conseillé de préfixer
le nom de la méthode par le nom
de la classe.
Dans la suite, nous utiliserons uniquement cette écriture.
L’exemple suivant donne une manière possible d'implémenter une classe permettant de manipuler les nombres complexes.
class
comp
{ double a,b; //variables
d'instance
final
static double PI = Math.PI; //variable
de classe
comp(double
inita,double initb) //constructeur
{a=inita; b=initb;}
static
double norme(comp x) //méthodes
de classe
{ return
Math.sqrt(x.a*x.a + x.b*x.b);}
static double
phase(comp x)
{ return Math.atan2(x.b,x.a)*180/PI;}
static
comp somme(comp x, comp y)
{ comp s=new
comp(0,0);
s.a = x.a + y.a; s.b
= x.b + y.b;
return s;}
static
comp produit(comp x, comp y)
{ comp p=new comp(0,0);
p.a
= x.a*y.a - x.b*y.b; p.b = x.b*y.a
+ y.b*x.a;
return p;}
static
comp quotient(comp x, comp y)
{ comp q=new
comp(0,0);
double n2=y.a*y.a +
y.b*y.b;
q.a = (x.a*y.a + x.b*y.b)/n2; q.b
= (x.b*y.a - y.b*x.a)/n2;
return
q;}
}
Pour utiliser cette classe, on pourra par exemple écrire :
comp a = new comp (3,5); // déclaration et initialisation
comp b =
new comp (2,-4);
comp c=
new comp (0,0);
c = comp.produit (a,b); // appel d’une méthode
Remarques :
Les méthodes
sont publiques par défaut.
Math.sqrt est un appel à la méthode statique
sqrt (racine carrée) de la classe « Math » de JAVA.
Après l’exécution du code suivant qui concerne des littéraux :
int i =
2 ;
int j =
i ;
i = 4;
la variable i vaut 4 et la variable j vaut 2. Quand on travaille sur des littéraux, toute déclaration de variable réserve pour celle-ci une zone mémoire de la taille idoine (par exemple 4 octets pour un entier de type int). Le code j = i recopie la valeur de la zone mémoire "i" dans la zone mémoire "j".
Il en va différemment avec des objets. Après l’exécution du code suivant :
rectangle
r1, r2;
r1 = new
rectangle(getGraphics(),10,20,100,50);
r2 = r1;
r1.change(60,30);
la largeur du rectangle r2 vaut aussi 60 et sa hauteur 30 car r2 et r1 correspondent en fait
au même objet. Toute modification des variables de l’un modifie les variables
de l’autre. On dit que r1 et r2 pointent sur un même objet.
Quand on déclare un objet, on récupère l’adresse
d’une case mémoire qui est en général l’adresse de la première case mémoire de la
zone réservée pour contenir toutes les données relatives à l’objet. Toute
manipulation de cet objet fait référence à cette adresse (pointeur implicite).
L'instruction r2 = r1 recopie seulement l'adresse du pointeur vers r1 dans
le pointeur vers r2. Pour créer une nouvelle instance indépendante d'un
objet, il faut utiliser son constructeur.
La comparaison de deux instances d'un objet (avec = = ) s'effectue sur les références de ces instances. Après exécution de :
Boolean ok
= false ;
rectangle r1, r2;
r1 = new
rectangle(getGraphics(),10,20,100,50);
r2 = new
rectangle(getGraphics(),10,20,100,50);
if (r1 ==
r2) ok = true;
ok reste "false" car les références de
r1 et de r2 sont différentes.
La comparaison de deux instances
d’une classe nécessite l’écriture d’une méthode spécifique dans laquelle on
compare la valeur de chacune des variables définies par le constructeur de l'objet..
C’est un processus qui permet d’ajouter des
fonctionnalités à une classe sans avoir à réécrire tout le code de cette
classe. La nouvelle classe hérite de toutes les données et méthodes de la
classe dont elle est issue. Pour signifier que la nouvelle classe dérive d’une
classe mère, on ajoute à la fin de sa déclaration le modificateur extends
suivi du nom de la classe mère.
Les classes filles n’héritent pas directement des constructeurs du
parent : On doit, avec le mot
clé super, faire appel au constructeur de la classe mère puis
initialiser les variables spécifiques à la nouvelle classe. Si la première
instruction du constructeur d’une sous-classe n’est pas super (référence
au constructeur de la classe mère), un appel super( ) est exécuté
automatiquement par le compilateur. Il faut alors qu’il existe un constructeur
sans paramètre dans la classe mère.
Par exemple pour tracer des rectangles pleins, il est
possible d’utiliser une approche différente de celle qui a déjà été examinée. On
peut par exemple créer une nouvelle classe rectplein qui va hériter de
la classe mère rectangle. Pour que la
classe rectplein puisse accéder aux données de rectangle, il faudrait
que celles-ci soient publiques ce qui est contraire au principe de
l’encapsulation. La solution est de définir les variables de classe avec le modificateur protected.
Les membres
d’une classe (données, méthodes) protégés par "protected" sont accessibles à
partir des classes dérivées
mais pas des autres classes.
La classe rectplein
hérite de toutes les méthodes de rectangle qui ne sont pas redéfinies
dans rectplein. La méthode bouge( ) est celle de rectangle ;
par contre comme la méthode dessine a été redéfinie dans la classe rectplein
c’est elle qui est utilisée pour les objets de ce nouveau type.
import
java.applet.*;
import java.awt.*;
class
rectangle
{ protected static Graphics g;
protected
int orx,ory,large,haut;
rectangle(Graphics
appG,int x, int y, int l, int h)
{ g=appG;
orx=x; ory=y; large=l; haut=h;}
public
void dessine()
{ g.setColor(Color.black);
g.drawRect(orx,ory,large,haut);}
public
void bouge (int x, int y)
{ orx +=x; ory+=y;}
}
//******************************************
class
rectplein extends rectangle
{ Color couleur; //variable
supplémentaire
rectplein
(Graphics appG,int x, int y, int l, int h, Color col)
{ super(appG,x,y,l,h);
//appel constructeur
de la classe rectangle
couleur=col;}
//initialisation
de la nouvelle variable
public
void dessine()
{ g.setColor(couleur);
g.fillRect(orx,ory,large,haut);
g.setColor(Color.black);
g.drawRect(orx,ory,large,haut);}
}
//******************************************
public
class testrect extends Applet
{ rectangle r1;
rectplein
rp1;
public
void init()
{ setBackground(Color.lightGray);
r1=
new rectangle(getGraphics(),10,20,100,50);
rp1= new
rectplein(getGraphics(),100,100,100,50,Color.red);}
public
void paint(Graphics g)
{ r1.dessine();
rp1.dessine();
//appel méthode de "rectplein"
rp1.bouge(30,60);
//appel méthode
de "rectangle"
rp1.dessine();}
}
Cliquer dans le cadre de l'applet pour l'activer
Beaucoup de langages autorisent le passage des arguments
soit par valeur soit par adresse. Dans le cas d'un passage
d'argument par valeur, on fait une copie de la valeur des paramètres dans une
pile et c'est l'adresse mémoire de cette pile qui est transmise à la méthode. Les
adresses réelles des arguments étant inconnues par la méthode, celle-ci
ne peut pas les modifier.
Dans le cas d'un passage d'argument par adresse,
on transmet à la méthode l'adresse mémoire (pointeur) de la variable argument
: les modifications apportées à la valeur de la variable par la méthode sont
immédiates et définitives.
Pour JAVA, le mode de passage est fonction de la nature des
arguments :
Les
arguments de types simples (entiers, réels, caractères ...) sont passés par
valeur et les arguments d'objets ou de tableaux sont passés par adresse.
Si
l'on souhaite qu'une méthode modifie une variable de type simple, il ne faut
pas passer celle-ci comme argument mais il faut la déclarer variable de classe.
Syntaxe pour les arguments de type tableau
En-tête
de la méthode : type (ou void) nomDeLaMethode ( typeDuTableau nomDuTableauM
[ ]) // crochets seuls.
Appel : nomDeLaMethode(nomTableauG)
// pas de crochets après le nom du tableau.
Les opérations effectuées par
la méthode sur les éléments du tableau "nomDuTableauM" s'appliquent
en fait au tableau nomTableauG qui est le seul à avoir une adresse mémoire effective.