1. Les classes Java : des nouveaux types
Delphi | Java |
Unit Biblio; interface // les déclarations des classes implementation // les implémentations des classes end. |
package Biblio;
// les déclarations et implémentation
des classes
|
En Java nous n'avons pas comme en Delphi, une partie déclaration
de la classe et une partie implémentation séparées l'une
de l'autre. La classe avec ses attributs et ses méthodes sont déclarés
et implémentés à un seul endroit.
Delphi | Java |
interface uses biblio; type Exemple = class x : real; y : integer; function F1(a,b:integer): real; procedure P2; end; implementation |
import biblio;
class Exemple void P2( )
|
1.2 Une classe est un type Java
Comme en Delphi, une classe Java peut être considérée comme un nouveau type dans le programme et donc des variables d'objets peuvent être déclarées selon ce nouveau "type".
Une déclaration de programme comprenant 3 classes en Delphi et Java :
Delphi | Java |
interface type Un = class ... end; Deux = class Appli3Classes = class
implementation procedure Appli3Classes.main;
end. |
class Appli3Classes
{ Un x; Deux y; public static void main(String [ ] arg) { Un x; Deux y; ... } } class Un { ... } class Deux
|
1.3 Toutes les classes ont le même ancêtre - héritage
Comme en Delphi toutes les classes Java dérivent automatiquement d'une seule et même classe ancêtre : la classe Object. En java le mot-clef pour indiquer la dérivation (héritage) à partir d'une autre classe est le mot extends, lorsqu'il est omis c'est donc que la classe hérite automatiquement de la classe Object :
Les deux déclarations de classe ci-dessous sont équivalentes en Delphi et en Java :
Delphi | Java |
type Exemple = class ( TObject ) ...... end; |
class Exemple extends Object { ....... } |
type Exemple = class ...... end; |
class Exemple { ....... } |
L'héritage en Java est tout fait classiquement de l'héritage simple comme en Delphi. Une classe fille qui dérive (on dit qui étend en Java) d'une seule classe mère, hérite de sa classe mère toutes ses méthodes et tous ses champs. En Java la syntaxe de l'héritage fait intervenir le mot clef extends, comme dans "class Exemple extends Object".
Une déclaration du type :
class ClasseFille extends ClasseMere {
}
signifie que la classe ClasseFille dispose de tous les attributs et les
méthodes de la classe ClasseMere.
Comparaison héritage Delphi et Java :
Delphi | Java |
type ClasseMere = class // champs de ClasseMere // méthodes de ClasseMere end; ClasseFille = class ( ClasseMere ) |
class ClasseMere { // champs de ClasseMere // méthodes de ClasseMere } class ClasseFille extends ClasseMere |
La visibilité et la protection des classes en Delphi est apportée par le module Unit où toutes les classes sont visibles dans le module en entier et dès que la unit est utilisée les classes sont visibles partout. Il n'y a pas de possibilité d'imbriquer une classe dans une autre.
En Java depuis le JDK 1.1, la situation qui était semblable à
celle de Delphi a considérablement évolué et actuellement
en Java 2, nous avons la possibilité d'imbriquer des
classes dans d'autres classes, par conséquent la visibilité
de bloc s'applique aussi aux classes.
Mots clefs pour la protection des classes et leur visibilité
:
Java | Explication |
mot clef abstract : abstract class ApplicationClasse1 { ... } |
classe abstraite non instanciable. Aucun objet ne peut être créé. |
mot clef public : public class ApplicationClasse2 { ... } |
classe visible par n'importe quel programme, elle doit avoir le même nom que le fichier de bytecode xxx.class qui la contient |
pas de mot clef : class ApplicationClasse3 { ... } |
classe visible seulement par toutes les autres classes du module où elle est définie. |
Nous remarquons donc qu'une classe dès qu'elle est déclarée est toujours visible et qu'il y a en fait deux niveaux de visibilité selon que le modificateur public est ou n'est pas présent, le mot clef abstract n'a de l'influence que pour l'héritage.
Nous étudions ci-après la visibilité des 3 classes
précédentes dans deux contextes différents.
1.5 Exemple de classe intégrée dans une autre classe
Dans le premier contexte, ces trois classes sont utilisées en étant intégrées (imbriquées) à une classe publique.
Exemple correspondant à l'imbrication de bloc suivante :
La classe :
Java | Explication |
package Biblio; public class ApplicationClasses { abstract class ApplicationClasse1 { ... } public class ApplicationClasse2 { ... } class ApplicationClasse3 { ... } } |
Ces 3 "sous-classes" sont visibles à partir de l'accès à la classe englobante "ApplicationClasses", elles peuvent donc être utilisées dans tout programme qui utilise la classe "ApplicationClasses". |
Un programme utilisant la classe :
Java | Explication |
import Biblio.ApplicationClasses;
class AppliTestClasses{ |
Le programme de gauche "class AppliTestClasses" importe (utilise) la classe précédente ApplicationClasses et ses sous-classes, en déclarant 3 variables a1, a2, a3. La notation uniforme de chemin de classe est standard. |
1.6 Exemple de classe inclue dans un package
Dans le second exemple, ces mêmes classe sont utilisées en étant inclues dans un package.
Exemple correspondant à l'imbrication de bloc suivante :
Le package :
Java | Explication |
package Biblio; abstract class ApplicationClasse1 { ... } public class ApplicationClasse2 { ... } class ApplicationClasse3 { ... } |
Ces 3 "sous-classes" font partie du package Biblio, elles sont visibles par importation séparée (comme précédemment) ou globale du package. |
Un programme utilisant le package :
Java | Explication |
import Biblio.* ;
class AppliTestClasses{ |
Le programme de gauche "class AppliTestClasses" importe le package Biblio et les classes qui le composent. Nous déclarons 3 variables a1, a2, a3. |
Remarques pratiques :
Pour pouvoir utiliser dans un programme, une classe définie dans un module (package) celle-ci doit obligatoirement avoir été déclarée dans le package, avec le modificateur public. Pour accéder à la classe Cl1 d'un package
Pack1, il est nécessaire d'importer cette classe : |
Le mot clef abstract est utilisé pour représenter une classe ou une méthode abstraite. Quel est l'intérêt de cette notion ? Avoir des modèles génériques permettant de définir ultérieurement des actions spécifiques.
Une méthode déclarée en abstract dans une classe mère :
Une méthode abstraite n'est qu'une signature de méthode sans implémentation dans la classe. |
Exemple de méthode abstraite :
class Etre_Vivant { } |
La classe Etre_Vivant est une classe mère générale
pour les êtres vivants sur la planète, chaque catégorie
d'être vivant peut être représenté par une classe
dérivée (classe fille de cette classe) :
class Serpent extends Etre_Vivant
{ } class Oiseau extends Etre_Vivant { class Homme extends Etre_Vivant { |
Tous ces êtres se déplacent d'une manière générale
donc une méthode SeDeplacer est commune à toutes les classes
dérivées, toutefois chaque espèce exécute cette
action d'une manière différente et donc on ne peut pas dire
que se déplacer est une notion concrète mais une notion abstraite
que chaque sous-classe précisera concrètement.
abstract class Etre_Vivant { abstract void SeDeplacer( ); } class Serpent extends Etre_Vivant { class Oiseau extends Etre_Vivant { class Homme extends Etre_Vivant { |
Comparaison de déclaration d'abstraction de méthode en Delphi et Java :
Delphi | Java |
type Etre_Vivant = class procedure SeDeplacer;virtual;abstract; end; Serpent = class ( Etre_Vivant ) Oiseau = class ( Etre_Vivant ) Homme = class ( Etre_Vivant )
|
abstract class Etre_Vivant { abstract void SeDeplacer( ); } class Serpent extends Etre_Vivant { class Oiseau extends Etre_Vivant { class Homme extends Etre_Vivant { |
Les classes abstraites permettent de créer des classes génériques
expliquant certains comportements sans les implémenter et
fournissant une implémentation commune de certains autres comportements
pour l'héritage de classes. Les classes abstraites sont un outil intéressant
pour le polymorphisme.
Vocabulaire et concepts :
• Une classe abstraite est une classe qui ne peut pas être instanciée.
• Une classe abstraite peut contenir des méthodes déjà implémentées.
• Une classe abstraite peut contenir des méthodes non implémentées.
• Une classe abstraite est héritable.
• On peut contsruire une hiérarchie de classes abstraites.
• Pour pouvoir construire un objet à partir d'une classe abstraite, il faut dériver une classe non abstraite en une classe implémentant toutes les méthodes non implémentées.
Une méthode déclarée dans une classe, non implémentée dans cette classe, mais juste définie par la déclaration de sa signature, est dénommée méthode abstraite.
Une méthode abstraite est une méthode à liaison
dynamique n’ayant pas d’implémentation dans la classe où elle
est déclarée. L' implémentation d'une méthode
abstraite est déléguée à une classe dérivée.
Syntaxe de l'exemple en Delphi et en Java (C# est semblable à Delphi)
:
Delphi
Java
Vehicule = class
public
procedure Demarrer; virtual; abstract;
procedure RépartirPassagers; virtual;
procedure PériodicitéMaintenance; virtual;
end;
abstract class ClasseA {
public abstract void Demarrer( );
public void RépartirPassagers( );
public void PériodicitéMaintenance( );
}
Si une classe contient au moins une méthode abstract,
elle doit impérativement être déclarée en classe
abstract elle-même.
abstract class Etre_Vivant {
abstract void SeDeplacer( );
}
Remarque
Une classe abstract ne peut pas être instanciée directement, seule une classe dérivée (sous-classe) qui redéfinit obligatoirement toutes les méthodes abstract de la classe mère peut être instanciée.
Conséquence de la remarque précédente, une classe
dérivée qui redéfinit toutes les méthodes abstract
de la classe mère sauf une (ou plus d'une) ne peut pas être
instanciée et suit la même règle que la classe mère
: elle contient au moins une méthode abstraite donc elle aussi une
classe abstraite et doit donc être déclarée en abstract.
Si vous voulez utiliser la notion de classe abstraite pour fournir un polymorphisme
à un groupe de classes, elles doivent toutes hériter de cette
classe, comme dans l'exemple ci-dessous :
• La classe Véhicule est abstraite, car la méthode Démarrer est abstraite et sert de "modèle" aux futurs classes dérivant de Véhicule, c'est dans les classes voiture, voilier et croiseur que l'on implémente le comportement précis du genre de démarrage.
• Notons au passage que dans la hiérarchie précédente, les classes vehicule Terrestre et Marin héritent de la classe Véhicule, mais n'implémentent pas la méthode abstraite Démarrer, ce sont donc par construction des classes abstraites elles aussi.
Les classes abstraites peuvent également contenir des membres déjà
implémentés. Dans cette éventualité, une
classe abstraite propose un certain nombre de fonctionnalités identiques
pour tous ses futurs descendants.(ceci n'est pas possible avec une interface).
Par exemple, la classe abstraite Véhicule n'implémente
pas la méthode abstraite Démarrer, mais fournit et implante
une méthode "RépartirPassagers" de répartition
des passagers à bord du véhicule (fonction de la forme, du
nombre de places, du personnel chargé de s'occuper de faire fonctionner
le véhicule...), elle fournit aussi et implante une méthode
"PériodicitéMaintenance" renvoyant la périodicité
de la maintenance obligatoire du véhicule (fonction du nombre de kms
ou miles parcourus, du nombre d'heures d'activités,...)
Ce qui signifie que toutes les classes voiture, voilier
et croiseur savent comment répartir leurs éventuels
passagers et quand effectuer une maintenance, chacune d'elle implémente
son propre comportement de démarrage.
Dans cet exemple, supposons que :
Les classes Vehicule, Marin et Terrestre sont abstraites
car aucune n'implémente la méthode abstraite Demarrer.
Les classes Marin et Terrestre contiennent chacune une surcharge
dynamique implémentée de la méthode virtuelle PériodicitéMaintenance
qui est déjà implémentée dans la classe Véhicule.
Les classes Voiture, Voilier et Croiseur ne sont
pas abstraites car elles implémentent les (la) méthodes abstraites
de leurs parents et elles surchargent dynamiquement (redéfinissent)
la méthode virtuelle RépartirPassagers qui est implémentée
dans la classe Véhicule.
Implantation d'un squelette Java de l'exemple
abstract class
Vehicule
{
public abstract void Demarrer ( );
public void RépartirPassagers ( )
{ … }
public void PériodicitéMaintenance ( ) { …
}
}
abstract class Terrestre
extends Vehicule
{
public void PériodicitéMaintenance ( ) { … }
}
abstract class Marin extends Vehicule
{
public void PériodicitéMaintenance ( ) { … }
}
class Voiture extends Terrestre
{
public void Demarrer ( )
{ … }
public void RépartirPassagers ( )
{ … }
}
class Voilier extends Marin
{
public void Demarrer ( )
{ … }
public void RépartirPassagers ( )
{ … }
}
class Croiseur extends Marin
{
public void Demarrer ( ) { … }
public void RépartirPassagers ( ) { … }
}
2. Les objets : des références
Les classes sont des descripteurs d'objets, les objets sont les agents
effectifs et "vivants" implantant les actions d'un programme. Les objets
dans un programme ont une vie propre :
C'est dans le segment de mémoire de la machine virtuelle Java que
s'effectue l'allocation et la désallocation d'objets. Le principe
d'allocation et de représentation des objets en Java est identique
à celui de Delphi il s'agit de la référence, qui est
une encapsulation de la notion de pointeur.
2.1 Modèle de la référence et machine Java
Rappelons que dans le modèle de la référence chaque
objet (représenté par un identificateur de variable) est caractérisé
par un couple (référence, bloc de données). Comme en Delphi, Java décompose l'instanciation
(allocation) d'un objet en deux étapes :
Delphi | Java |
type Un = class ...... end; // la déclaration :
// la création : |
class Un { ... } // la déclaration :
.... // la création : |
Après exécution du pseudo-programme précédent, les variables x et y contiennent chacune une référence (adresse mémoire) vers un bloc objet différent:
Un programme Java est fait pour être exécuté par
une machine virtuelle Java, dont nous rappellons qu'elle contient
6 éléments principaux :
Deux objets Java seront instanciés dans la machine virtuelle Java de la manière suivante :
Attitude à rapprocher pour comparaison, à celle dont Delphi
gère les objets dans une pile d'exécution de type LIFO et un
tas :
Attention à l'utilisation de l'affectation entre variables d'objets dans le modèle de représentation par référence. L'affectation x = y ne recopie pas le bloc objet de données de y dans celui de x, mais seulement la référence (l'adresse) de y dans la référence de x. Visualisons cette remarque importante :
Situation au départ, avant affectation
Situation après l'affectation " x = y "
En java, la désallocation étant automatique, le bloc de
données objet qui était référencé par
y avant l'affectation, n'est pas perdu, car le garbage collector
se charge de restituer la mémoire libérée au segment
de mémoire de la machine virtuelle Java.
2.2 Les constructeurs d'objets
Un constructeur est une méthode spéciale d'une classe
dont la seule fonction est d'instancier un objet (créer le
bloc de données). Comme en Delphi une classe Java peut posséder
plusieurs constructeurs, il est possible de pratiquer des initialisations
d'attributs dans un constructeur. Comme toutes les méthodes, un constructeur
peut avoir ou ne pas avoir de paramètres formels.
Soit une classe dénommée Un dans laquelle, comme nous l'avons
fait jusqu'à présent nous n'indiquons aucun constructeur spécifique
:
class Un { int a; } |
Automatiquement Java attribue un constructeur public à cette classe public Un ( ). C'est comme si Java avait introduit dans votre classe à votre insu , une nouvelle méthode dénommée Un.Cette méthode "cachée" n'a aucun paramètre et aucune instruction dans son corps. Ci-dessous un exemple de programme Java correct illustrant ce qui se passe :
class Un { public Un ( ) { } int a; } |
|
Exemple de constructeur avec instructions :
Java | Explication |
class Un { public Un ( ) { a = 100 } int a; } |
Le constructeur public Un sert ici à initialiser à 100 la valeur de l'attribut "int a" de chaque objet qui sera instancié. |
Exemple de constructeur avec paramètre :
Java | Explication |
class Un { public Un (int b ) { a = b; } int a; } |
Le constructeur public Un sert ici à initialiser la valeur de l'attribut "int a" de chaque objet qui sera instancié. Le paramètre int b contient cette valeur. |
Exemple avec plusieurs constructeurs :
Java | Explication |
class Un { public Un (int b ) { a = b; } public Un ( ) { a = 100; } public Un (float b ) { a = (int)b; } int a; } |
La classe Un possède 3 constructeurs servant
à initialiser chacun d'une manière différente le seul
attribut int a.
|
Comparaison Delphi - Java pour la déclaration de constructeurs
Delphi | Java |
Un = class a : integer; public constructor creer; overload; constructor creer (b:integer); overload; constructor creer (b:real); overload; end; implementation constructor Un.creer; begin
|
class Un { public Un ( ) { a = 100; } public Un (int
b ) public Un (float
b ) int a;
|
En Delphi un constructeur a un nom quelconque, tous les constructeurs
peuvent avoir des noms différents ou le même nom comme en Java.
2.3 Utilisation du constructeur d'objet automatique (par défaut)
Le constructeur d'objet par défaut de toute classe Java comme nous l'avons signalé plus haut est une méthode spéciale sans paramètre, l'appel à cette méthode spéciale afin de construire un nouvel objet répond à une syntaxe spécifique par utilisation du mot clef new..
Syntaxe
Pour un constructeur sans paramètres formels, l'instruction d'instanciation
d'un nouvel objet à partir d'un identificateur de variable déclarée
selon un type de classe, s'écrit syntaxiquement ainsi :
Exemple : (deux façons équivalentes de créer un objet x de classe Un)
Cette instruction crée dans le segment de mémoire
de la machine virtuelle Java, un nouvel objet de classe Un dont la référence (l'adresse)
est mise dans la variable x. |
Dans l'exemple ci-dessous, nous utilisons le constructeur par défaut
de la classe Un :
class Un
{ ...
}
// la déclaration :
Un x , y ;
....
// la création :
x = new Un(
);
y = new Un(
);
Un programme de 2 classes, illustrant l'affectation de références :
Java | Explication |
class AppliClassesReferences { public static void main(String [] arg) { Un x,y ; x = new Un( ); y = new Un( ); System.out.println("x.a="+x.a); System.out.println("y.a="+y.a); y = x; x.a =12; System.out.println("x.a="+x.a); System.out.println("y.a="+y.a); } } class Un { int a=10; } |
Ce programme Java contient deux classes :
class AppliClassesReferences La classe AppliClassesReferences est une classe exécutable
car elle contient la méthode main. C'est donc cette méthode
qui agira dès l'exécution du programme.
|
Détaillons les instructions | Que se passe-t-il à l'exécution ? |
Un x,y ; x = new Un( ); y = new Un( ); |
Instanciation de 2 objets différents x et y de type Un. |
System.out.println("x.a="+x.a); System.out.println("y.a="+y.a); |
Affichage de : x.a = 10 y.a = 10 |
y = x; | La référence de y est remplacée par celle de x dans la variable y (y pointe donc vers le même bloc que x). |
x.a =12; System.out.println("x.a="+x.a); System.out.println("y.a="+y.a); |
On change la valeur de l'attribut a de x, et
l'on demande d'afficher les attributs de x et de y : x.a = 12 y.a = 12 Comme y pointe vers x, y et x sont maintenant le même objet sous deux noms différents ! |
2.4 Utilisation d'un constructeur d'objet personnalisé
L'utilisation d'un constructeur personnalisé d'une classe est semblable à celle du constructeur par défaut de la classe. La seule différence se trouve lors de l'instanciation : il faut fournir des paramètres effectifs lors de l'appel au constructeur.
Syntaxe
Exemple avec plusieurs constructeurs :
une classe Java | Des objets créés |
class Un { int a ; public Un (int b ) { a = b ; } public Un ( ) {
public Un (float
b ) { |
Un obj1 = newUn( );
Un obj2 = new Un( 15 ); int k = 14; Un obj4 = new Un( 3.25f ); float r = -5.6; |
2.5 Le mot clef this pour désigner un autre constructeur
Il est possible de dénommer dans les instructions d'une méthode de classe, un futur objet qui sera instancié plus tard. Le paramètre ou (mot clef) this est implicitement présent dans chaque objet instancié et il contient la référence à l'objet actuel. Il joue exactement le même rôle que le mot clef self en Delphi.
Java | Java équivalent |
class Un { public Un ( ) { a = 100; } int a; } |
class Un { public Un ( ) { this.a = 100; } int a; } |
Dans le programme de droite le mot clef this fait référence à l'objet lui-même, ce qui dans ce cas est superflu puisque la variable int a est un champ de l'objet.
Montrons deux exemples d'utilisation pratique de this
Cas où l'objet est passé comme un paramètre dans une de ses méthodes :
Java | Explications |
class Un { public Un ( ) { a = 100; } public void methode1(Un x) { System.out.println("champ a ="+x.a); } public void methode2( int b ) { a += b; methode1(this); } int a; } |
La methode1(Un x) reçoit
un objet de type Exemple en paramètre et imprime son champ int
a.
La methode2( int b ) reçoit
un entier int b qu'elle additionne au champ int a de l'objet,
puis elle appelle la méthode1 avec comme paramètre l'objet
lui-même.
|
Comparaison Delphi - java sur cet exemple (similitude complète)
Delphi | Java |
Un = class a : integer; public constructor creer; procedure methode1( x:Un ); procedure methode2 ( b:integer ); end; implementation constructor Un.creer; begin
|
class Un { public Un ( ) { a = 100; } public void methode1(Un x) { System.out.println("champ a ="+x.a); } public void methode2( int b ) { a += b; methode1(this); } int a; }
|
Cas où le this sert à outrepasser le masquage de visibilité :
Java | Explications |
class Un { int a; public void methode1(float a) { a = this.a + 7 ; } } |
La methode1(float
a) possède un paramètre float a dont le nom masque
le nom du champ int a.
Si nous voulons malgré tout accéder au champ de l'objet, l'objet étant référencé par this, "this.a" est donc le champ int a de l'objet lui-même. |
Comparaison Delphi - java sur ce second exemple (similitude complète aussi)
Delphi | Java |
Un = class a : integer; public procedure methode( a:real ); end; implementation procedure Un.methode( a:real );begin
|
class Un { int a; public void methode(float a) { a = this.a + 7 ; } }
|
Il est aussi possible d’utiliser le mot clef this en Java dans un constructeur pour désigner l’appel à un autre constructeur avec une autre signature. En effet comme tous les constructeurs portent le même nom, il a fallu trouver un moyen d’appeler un constructeur dans un autre constructeur, c’est le rôle du mot clef this que de jouer le rôle du nom standar du constructeur de la classe.
Lorsque le mot clef this est utilisé pour désigner une
autre surcharge du constructeur en cours d’exécution, il doit obligatoirement être la première instruction
du constructeur qui s’exécute (sous peine d’obtenir un message d’erreur
à la compilation).
Exemple de classe à deux constructeurs :
Code Java
Explication
La classe ManyConstr possède 2 constructeurs :
Le premier : ManyConstr (String s)
Le second : ManyConstr (char c, String s)
Grâce à l’instruction this(s), le second constructeur appelle le premier sur la variable s, puis concatène le caractère char c au champ String ch.
La méthode main instancie un objet de classe ManyConstr avec le second constructeur et affiche le contenu du champ String ch. De l’objet ;
Résultat de l’exécution :
chaine//x
Nous examinons dans ce paragraphe comment Java utilise les variables et les méthodes à l'intérieur d'une classe. Il est possible de modifier des variables et des méthodes d'une classe ceci sera examiné plus loin.
En Java, les champs et les méthodes sont classés en deux
catégories :
3.1 Variables dans une classe en général
Rappelons qu'en Java, nous pouvons déclarer dans un bloc (for, try,...) de nouvelles variables à la condition qu'elles n'existent pas déjà dans le corps de la méthode où elles sont déclarées. Nous les dénommerons : variables locales de méthode.
Exemple de variables locales de méthode :
class Exemple { void calcul ( int x, int y ) {int a = 100; for ( int i = 1; i<10; i++ ) {char carlu; System.out.print("Entrez un caractère : "); carlu = Readln.unchar( ); int b =15; a =.... ..... } } } |
La définition int
a = 100; est locale à la méthode en général
La définition int i = 1; est locale à la boucle for. Les définitions char carlu
et int b sont locales au corps de la
boucle for. |
Java ne connaît pas la notion de variable globale au sens habituel donné à cette dénomination, dans la mesure où toute variable ne peut être définie qu'à l'intérieur d'une classe, ou d'une méthode inclue dans une classe. Donc mis à part les variables locales de méthode définies dans une méthode, Java reconnaît une autre catégorie de variables, les variables définies dans une classe mais pas à l'intérieur d'une méthode spécifique. Nous les dénommerons : attributs de classes parce que ces variables peuvent être de deux catégories.
Exemple de attributs de classe :
class AppliVariableClasse
{ float r ; void calcul ( int x,
int y ) |
Les variables float
r , long y et int x sont des attributs de classe (ici en
fait plus précisément, des variables d'instance).
Conseil : regroupez les variables de classe au début de la classe afin de mieux les gérer.
|
Les attributs de classe peuvent être soit de la catégorie
des variables de classe, soit de
la catégorie des variables d'instance.
3.2 Variables et méthodes d'instance
Java se comporte comme un langage orienté objet classique vis
à vis de ses variables et de ses méthodes. A chaque instanciation
d'un nouvel objet d'une classe donnée, la machine virtuelle Java
enregistre le p-code des méthodes de la classe dans la zone de
stockage des méthodes,elle alloue dans le segment de mémoire
autant d'emplacements mémoire pour les variables
que d'objet créés. Java dénomme cette catégorie
les variables et les méthodes d'instance.
une classeJava | Instanciation de 3 objets |
class AppliInstance
{ int x ; int y ; } |
AppliInstance
obj1 = new AppliInstance( ); AppliInstance obj2 = newAppliInstance( ); AppliInstance obj3 = newAppliInstance( ); |
Voici une image du segment de mémoire associé à ces 3 objets :
Un programme Java à 2 classes illustrant l'exemple précédent :
Programme Java exécutable |
class AppliInstance { int x = -58 ; int y = 20 ; } class Utilise { public static void main(String [ ] arg) { AppliInstance obj1 = new AppliInstance( ); AppliInstance obj2 = new AppliInstance( ); AppliInstance obj3 = new AppliInstance( ); System.out.println( "obj1.x = " + obj1.x ); } } |
3.3 Variables et méthodes de classe - static
Variable de classe
On identifie une variable ou une méthode de classe en précédant sa déclaration du mot clef static. Nous avons déjà pris la majorité de nos exemples simples avec de tels composants.
Voici deux déclarations de variables de classe :
static int x ;
static int a = 5;
Une variable de classe est accessible comme une variable d'instance(selon
sa visibilité), mais aussi sans avoir à
instancier un objet de la classe, uniquement en référençant
la variable par le nom de la classe dans la notation de chemin uniforme d'objet.
une classeJava | Instanciation de 3 objets |
class AppliInstance { static int x ; int y ; } |
AppliInstance
obj1 = new AppliInstance( ); AppliInstance obj2 = newAppliInstance( ); AppliInstance obj3 = newAppliInstance( ); |
Voici une image du segment de mémoire associé à ces 3 objets :
Exemple de variables de classe :
class ApplistaticVar
{ static int x =15 ; } class UtiliseApplistaticVar { int a ; void f( ) { a = ApplistaticVar.x ; ..... } } |
La définition "static int x =15 ;"
crée une variable de la classe ApplistaticVar,
nommée x.
L'instruction "a = ApplistaticVar.x
;" utilise la variable x comme variable de classe ApplistaticVar
sans avoir instancié un objet de cette classe. |
Nous utilisons sans le savoir depuis le début de ce cours, une variable de classe sans jamais instancier un quelconque objet de la classe. Dans l'instruction << System.out.println("Bonjour"); >>, la classe System possède une variable (un champ)de classe out qui est elle-même un objet de classe dérivant de la classe FilterOutputStream, nous n'avons jamais instancié d'objet de cette classe System.
Les champs de la classe System :
Notons que les champs err et in sont aussi des variables
de classe (précédées par le mot static).
Méthode de classe
Une méthode de classe est une méthode dont l'implémentation est la même pour tous les objets de la classe, en fait la différence avec une méthode d'instance a lieu sur la catégorie des variables sur lesquelles ces méthodes agissent.
De par leur définition les méthodes de classe ne peuvent travailler qu'avec des variables de classe, alors que les méthodes d'instances peuvent utiliser les deux catégories de variables.
Un programme correct illustrant le discours :
Java | Explications |
class Exemple {static int x ; int y ; void f1(int a) { x = a; y = a; } static void g1(int a) { x = a; } } class Utilise { public static void main(String [] arg) { Exemple obj = new Exemple( ); obj.f1(10); System.out.println("<f1(10)>obj.x="+obj.x); obj.g1(50); System.out.println("<g1(50)>obj.x="+obj.x); } } |
void f1(int a) { x = a; //accès à la variable de classe y = a ; //accès à la variable d'instance } static void g1(int a) Après exécution on obtient : <f1(10)>obj.x = 10
|
Bilan pratique et utile sur les composants de classe, en 5 remarques
1) - Les méthodes et les variables de classe sont précédées obligatoirement du
mot clef static. Elles jouent un rôle semblable à celui qui est attribué aux variables et aux sous-routines globales dans un langage impératif classique. |
Java | Explications |
class Exemple1 { int a = 5; static int b = 19; void m1( ){...} static void m2( ) {...} } |
La variable a dans int
a = 5; est une variable d'instance.
La variable b dans static int b = 19; est une variable de classe. La méthode m2 dans static void
m2( ) {...} est une méthode de classe. |
2) - Pour utiliser une variable x1 ou une méthode meth1 de la classe Classe1, il suffit de d'écrire Classe1.x1 ou bien Classe1.meth1. |
Java | Explications |
class Exemple2 { static int b = 19; static void m2( ) {...} } class UtiliseExemple { Exemple2.b = 53; Exemple2.m2( ); ... } |
Dans la classe Exemple2 b
est une variable de classe, m2 une méthode de classe.
La classe UtiliseExemple fait appel à
la méthode m2 directement avec le nom de la classe, il en est de
même avec le champ b de la classe Exemple2 . |
3) - Une variable de classe (précédée du mot clef static) est partagée par tous les objets de la même classe. |
Java | Explications |
class AppliStatic
{ static int x = -58 ; int y = 20 ; ... } class Utilise { public static void main(String [] arg) { AppliStatic obj1 = new AppliStatic( ); AppliStatic obj2 = new AppliStatic( ); AppliStatic obj3 = new AppliStatic( ); obj1.y = 100; obj1.x = 101; System.out.println("obj1.x="+obj1.x); System.out.println("obj1.y="+obj1.y); System.out.println("obj2.x="+obj2.x); System.out.println("obj2.y="+obj2.y); System.out.println("obj3.x="+obj3.x); System.out.println("obj3.y="+obj3.y); AppliStatic.x = 99; System.out.println(AppliStatic.x="+obj1.x); } } |
Dans la classe AppliStatic
x est une variable de classe, et y une variable d'instance.
La classe Utilise crée 3 objets (obj1,obj2,obj3) de classe AppliStatic. L'instruction obj1.y = 100; est un accès au champ y de l'instance obj1. Ce n'est que le champ x de cet objet qui est modifié,les champs x des objets obj2 et obj3 restent inchangés Il y a deux manières d'accéder à la variable static x, :
|
Après exécution :
obj1.x = 101
obj1.y = 100
obj2.x = 101
obj2.y = 20
obj3.x = 101
obj3.y = 20
<AppliStatic>obj1.x = 99
4) - Une méthode de classe (précédée du mot clef static) ne peut utiliser que des variables de classe (précédées du mot clef static) et jamais des variables d'instance.Une méthode d'instance peut accéder aux deux catégories de variables |
5) - Une méthode de classe (précédée du mot clef static) ne peut appeller (invoquer) que des méthodes de classe (précédées du mot clef static). |
Java | Explications |
class AppliStatic
{ static int x = -58 ; int y = 20 ; void f1(int a)
class Utilise public static void main(String
[] arg) { obj1.x = 101; |
Nous reprenons l'exemple
précédent en ajoutant à la classe AppliStatic une méthode
interne f1 : void f1(int a) { AppliStatic.x = a; y = 6 ; } Cette méthode accède à la variable de classe comme un champ d'objet. Nous rajoutons à la classe Utilise,
une méthode static (méthode de classe) notée f2:
Nous avons donc quatre manières d'accéder à la variable static x, :
|
Au paragraphe précédent, nous avons indiqué que
Java ne connaissait pas la notion de variable globale stricto sensu, mais
en fait une variable static peut jouer le rôle d'un variable globale
pour un ensemble d'objets instanciés à partir de la même
classe.
3.4 Surcharge et polymorphisme
Vocabulaire :
Le polymorphisme est la capacité d'une entité à posséder
plusieurs formes. En informatique ce vocable s'applique aux méthodes
selon leur degré d'adaptabilité, nous distinguons alors
deux dénomination :
|
A- La surcharge de méthode (polymorphisme statique)
est une fonctionnalité classique des langages très évolués
et en particulier des langages orientés objet; elle consiste dans
le fait qu'une classe peut disposer de plusieurs méthodes ayant
le même nom, mais avec des paramètres formels différents
ou éventuellement un type de retour différent. On appelle
signature de la méthode l'en-tête de la méthode
avec ses paramètres formels. Nous avons déjà utilisé
cette fonctionnalité précédement dans le paragraphe
sur les constructeurs, où la classe Un disposait de trois constructeurs
surchargés :
class Un
{
int a;
public Un ( )
{ a = 100;
}
public Un (int
b )
{ a = b;
}
public Un (float
b )
{ a = (int)b;
}
}
Mais cette surcharge est possible aussi pour
n'importe quelle méthode de la classe autre que le constructeur.
Le compilateur n'éprouve aucune difficulté lorsqu'il rencontre
un appel à l'une des versions surchargée d'une méthode,
il cherche dans la déclaration de toutes les surcharges celle dont
la signature (la déclaration des paramètres formels)
coïncide avec les paramètres effectifs de l'appel.
Programme Java exécutable | Explications |
class Un { int a; public Un (int b ) { a = b; } void f ( ) { a *=10; } void f ( int x ) { a +=10*x; } int f ( int x, char y ) { a = x+(int)y; return a; } } class AppliSurcharge { public static void main(String [ ] arg) { Un obj = new Un(15); System.out.println("<création> a ="+obj.a); obj.f( ); System.out.println("<obj.f()> a ="+obj.a); obj.f(2); System.out.println("<obj.f()> a ="+obj.a); obj.f(50,'a'); System.out.println("<obj.f()> a ="+obj.a); } } |
La méthode f de la classe Un
est surchargée trois fois :
void f ( ) void f ( int x ) int f ( int x, char
y ) La méthode f de la classe Un peut donc être appelée par un objet instancié de cette classe sous l'une quelconque des trois formes : obj.f( ); pas de paramètre => choix : void f ( ) obj.f(2); paramètre int => choix : void f ( int x ) obj.f(50,'a'); deux paramètres, un int un char => choix : int f ( int x, char y )
|
Comparaison Delphi - java sur la surcharge :
Delphi | Java |
Un = class a : integer; public constructor methode( b : integer ); procedure f;overload; procedure f(x:integer);overload; function f(x:integer;y:char):integer;overload; end; implementation constructor Un.methode( b : integer
); begin |
class Un { int a; public Un (int b ) { a = b; } void f ( ) { a *=10; } void f ( int x ) { a +=10*x; } int f ( int x, char y ) { a = x+(int)y; return a; } } class AppliSurcharge
|
B - la redéfinition de méthode (ou polymorphisme dynamique) est spécifique aux langages orientés objet. Elle est mise en oeuvre lors de l'héritage d'une classe mère vers une classe fille dans le cas d'une méthode ayant la même signature dans les deux classes. Dans ce cas les actions dûes à l'appel de la méthode, dépendent du code inhérent à chaque version de la méthode (celle de la classe mère, ou bien celle de la classe fille). Ces actions peuvent être différentes. En java aucun mot clef n'est nécessaire ni pour la surcharge ni pour la redéfinition, c'est le compilateur qui analyse la syntaxe afin de de se rendre compte en fonction des signatures s'il s'agit de redéfinition ou de surcharge. Attention il n'en va pas de même en Delphi, plus lourd mais plus explicite pour le programmeur, qui nécessite des mots clefs comme virtual, dynamic override et overload.
Dans l'exemple ci-dessous la classe ClasseFille qui hérite de la classe ClasseMere, redéfinit la méthode f de sa classe mère :
Comparaison redéfinition Delphi et Java :
Delphi | Java |
type ClasseMere = class x : integer; procedure f (a:integer);virtual;//autorisation procedure g(a,b:integer); end; ClasseFille = class ( ClasseMere ) implementation procedure ClasseMere.f (a:integer); begin... |
class ClasseMere { int x = 10; void f ( int a) class ClasseFille extends ClasseMere
|
Comme delphi, Java peut combiner la surcharge et la redéfinition sur une même méthode, c'est pourquoi nous pouvons parler de surcharge héritée :
Java |
class ClasseMere { int x = 10; void f ( int a) class ClasseFille extends ClasseMere |
C'est le compilateur Java qui fait tout le travail.
Prenons un objet obj de classe Classe1, lorsque le compilateur Java trouve
une instruction du genre "obj.method1(paramètres effectifs);",
sa démarche d'analyse est semblable à celle du compilateur
Delphi, il cherche dans l'ordre suivant :
Soit à partir de l'exemple précédent les instructions
suivantes :
ClasseFille obj = new ClasseFille( );
obj.g(-3,8);
obj.g('h');
Le compilateur Java applique la démarche d'analyse décrite, à l'instruction "obj.g(-3,8);". Ne trouvant pas dans ClasseFille de méthode ayant la bonne signature (signature = deux entiers) , le compilateur remonte dans la classe mère ClasseMere et trouve une méthode " void g ( int a, int b) " de la classe ClasseMere ayant la bonne signature (signature = deux entiers), il procède alors à l'appel de cette méthode sur les paramètres effectifs (-3,8).
Dans le cas de l'instruction obj.g('h'); , le compilateur trouve
immédiatement dans ClasseFille la méthode " void g (char
b) " ayant la bonne signature, c'est donc elle qui est appelée sur
le paramètre effectif 'h'.
Résumé pratique sur le polymorphisme en Java
La surcharge (polymorphisme statique) consiste à proposer différentes signatures de la même méthode. |
La redéfinition (polymorphisme dynamique) ne se produit que dans l'héritage d'une classe par redéfinition de la méthode mère avec une méthode fille (ayant ou n'ayant pas la même signature). |
Nous venons de voir que le compilateur s'arrête dès qu'il trouve une méthode ayant la bonne signature dans la hiérarchie des classes, il est des cas où nous voudrions accéder à une méthode de la classe mère alors que celle-ci est redéfinie dans la classe fille. C'est un problème analogue à l'utilisation du this lors du masquage d'un attribut. Il existe un mot clef qui permet d'accéder à la classe mère (classe immédiatement au dessus): le mot super.
On parle aussi de super-classe au lieu de classe mère en Java. Ce mot clef super référence la classe mère et à travers lui, il est possible d'accéder à tous les champs et à toutes les méthodes de la super-classe (classe mère). Ce mot clef est très semblable au mot clef inherited de Delphi qui joue le même rôle uniquement sur les méthodes.
Exemple :
Java |
class ClasseMere { int x = 10; void g ( int a, int b) class ClasseFille extends ClasseMere void g (char b) //surcharge
et redéfinition de g |
Le mot clef super peut en Java être utilisé seul ou avec des paramètres comme un appel au constructeur de la classe mère.
Exemple :
Java |
class ClasseMere { public ClasseMere ( ) { ... } public ClasseMere (int a ) { ... } } class ClasseFille extends ClasseMere |
3.6 Modification de visibilité
Terminons ce chapitre par les classiques modificateurs de visibilité des variables et des méthodes dans les langages orientés objets, dont Java dispose :
Modification de visibilité (modularité public-privé)
par défaut (aucun mot clef) | les variables et les méthodes d'une classe non précédées d'un mot clef sont visibles par toutes les classes inclues dans le module seulement. |
public | les variables et les méthodes d'une classe précédées du mot clef public sont visibles par toutes les classes de tous les modules. |
private | les variables et les méthodes d'une classe précédées du mot clef private ne sont visibles que dans la classe seulement. |
protected | les variables et les méthodes d'une classe précédées du mot clef protected sont visibles par toutes les classes inclues dans le module, et par les classes dérivées de cette classe. |
Ces attributs de visibilité sont identiques à ceux de Delphi.
3.7 Interfaces Java
- Les interfaces ressemblent aux classes abstraites sur un seul point : elles contiennent des membres expliquant certains comportements sans les implémenter.
- Les classes abstraites et les interfaces se différencient principalement par le fait qu'une classe peut implémenter un nombre quelconque d'interfaces, alors qu'une classe abstraite ne peut hériter que d'une seule classe abstraite ou non.
Vocabulaire et concepts :
- Une interface est un contrat, elle peut contenir des propriétés, des méthodes et des événements mais ne doit contenir aucun champ ou attribut.
- Une interface ne peut pas contenir des méthodes déjà implémentées.
- Une interface doit contenir des méthodes non implémentées.
- Une interface est héritable.
- On peut contsruire une hiérarchie d'interfaces.
- Pour pouvoir construire un objet à partir d'une interface, il faut définir une classe non abstraite implémentant toutes les méthodes de l'interface.
Une classe peut implémenter plusieurs interfaces. Dans ce cas nous avons une excellente alternative à l'héritage multiple.Lorsque l'on crée une interface, on fournit un ensemble de définitions et de comportements qui ne devraient plus être modifiés. Cette attitude de constance dans les définitions, protège les applications écrites pour utiliser cette interface.
Les variables de types interface respectent les mêmes règles de transtypage que les variables de types classe.
Les objets de type classe clA peuvent être transtypés et reférencés par des variables d'interface IntfA dans la mesure où la classe clA implémente l’interface IntfA. (cf. polymorphisme d'objet)
Si vous voulez utiliser la notion d'interface pour fournir un polymorphisme à une famille de classes, elles doivent toutes implémenter cette interface, comme dans l'exemple ci-dessous.
Exemple :
l'interface Véhicule définissant 3 méthodes (abstraites) Démarrer, RépartirPassagers de répartition des passagers à bord du véhicule (fonction de la forme, du nombre de places, du personnel chargé de s'occuper de faire fonctionner le véhicule...), et PériodicitéMaintenance renvoyant la périodicité de la maintenance obligatoire du véhicule (fonction du nombre de km ou miles parcourus, du nombre d'heures d'activités,...)
Soit l'interface Véhicule définissant ces 3 méthodes :
Soient les deux classes Véhicule terrestre et Véhicule marin, qui implémentent partiellemnt chacune l'interface Véhicule , ainsi que trois classes voiture, voilier et croiseur héritant de ces deux classes :
- Les trois méthodes de l'interface Véhicule sont abstraites et publics par définition.
- Les classes Véhicule terrestre et Véhicule marin sont abstraites, car la méthode abstraite Démarrer de l'interface Véhicule n'est pas implémentée elles reste comme "modèle" aux futurs classes. C'est dans les classes voiture, voilier et croiseur que l'on implémente le comportement précis du genre de démarrage.
Dans cette vision de la hiérarchie on a supposé que les classes abstraites Véhicule terrestre et Véhicule marin savent comment répartir leur éventuels passagers et quand effectuer une maintenance du véhicule. Les classes voiture, voilier et croiseur , n'ont plus qu'à implémenter chacune son propre comportement de démarrage.
Syntaxe de l'interface en Delphi et en Java (C# est semblable à Java) :
Delphi Java Vehicule = Interface
procedure Demarrer;
procedure RépartirPassagers;
procedure PériodicitéMaintenance;
end;Interface Vehicule {
void Demarrer( );
void RépartirPassagers( );
void PériodicitéMaintenance( );
}
Utilisation pratique des interfacesQuelques conseils prodigués par des développeurs professionnels (microsoft, Borland) :
- Les interfaces bien conçues sont plutôt petites et indépendantes les unes des autres.
- Un trop grand nombre de fonctions rend l'interface peu maniable.
- Si une modification s'avère nécessaire, une nouvelle interface doit être créée.
- La décision de créer une fonctionnalité en tant qu'interface ou en tant que classe abstraite peut parfois s'avérer difficile.
- Vous risquerez moins de faire fausse route en concevant des interfaces qu'en créant des arborescences d'héritage très fournies.
- Si vous projetez de créer plusieurs versions de votre composant, optez pour une classe abstraite.
- Si la fonctionnalité que vous créez peut être utile à de nombreux objets différents, faites appel à une interface.
- Si vous créez des fonctionnalités sous la forme de petits morceaux concis, faites appel aux interfaces.
- L'utilisation d'interfaces permet d'envisager une conception qui sépare la manière d'utiliser une classe de la manière dont elle est implémentée.
- Deux classes peuvent partager la même interface sans descendre nécessairement de la même classe de base.
Exemple de hiérarchie à partir d'une interface
Dans cet exemple :Les méthodes RépartirPassagers, PériodicitéMaintenance et Demarrer sont implantées comme des méthodes à liaison dynamique afin de laisser la possibilité pour des classes enfants de surcharger ces méthodes.
Soit l'écriture en Java de cet exemple :
Java interface IVehicule{
void Demarrer( );
void RépartirPassager( );
void PériodicitéMaintenance( );
}abstract class Terrestre implements IVehicule {
public void RépartirPassager( ){..........};
public void PériodicitéMaintenance( ){..........};
}class Voiture extends Terrestre {
public void Demarrer( ){..........};
}abstract class Marin implements IVehicule {
public void RépartirPassager( ){..........};
public void PériodicitéMaintenance( ){..........};
}
class Voilier extends Marin {
public void Demarrer( ){..........};
}
class Croiseur extends Marin {
public void Demarrer( ){..........};
}