Java

Notification de cookies

Nous utilisons des cookies pour améliorer votre expérience. En poursuivant votre navigation sur ce site, vous acceptez l'utilisation de cookies. Plus d'informations

Introduction à la POO en java

La programmation orientée objet existe depuis l’arrivée du langage Simula ’67 en 1967. Cependant, elle n’est vraiment devenue un des paradigmes de la programmation qu’au milieu des années 1980.

Au contraire de la programmation structurée traditionnelle, la programmation orientée objet met dans une même et unique structure les données et les opérations qui leurs sont associées.
Java est un pur langage orienté objet, ce qui signifie que le niveau le plus externe de la structure des données est l’objet.

I. Classes:

Les classes et les objets ne sont pas la même chose. Une classe est la définition d’un type, alors qu’un objet est la déclaration d’une instance d’un type de classe. Après avoir créé une classe, vous pouvez créer autant d’objets que voulu basés sur cette classe.

Déclaration et instanciation des classes

Une classe Java peut être très simple. Voici la définition d’une classe vide :

class MaClasse {
}

lorsque cette classe n’est pas encore utile, elle est correcte dans Java.
Une classe plus utile contiendra quelques données membre et méthodes, que nous allons ajouter sous peu.
Premièrement, examinez la syntaxe d’instanciationde la classe. Pour créer une instance de cette classe, utilisez l’opérateur new avec le nom de la classe. 

Vous devez déclarer une variable d’instance pour l’objet:

MaClasse monObjet;

Mais, déclarer simplement une variable d’instance n’alloue pas de mémoire ni aucune autre des ressources nécessaires à l’objet. Cela crée une référence appelée monObjet, mais n’instancie pas l’objet. C’estle rôle de l’opérateur new.

monObjet =new MaClasse();

Remarquez que le nom de la classe est utilisé comme s’il s’agissait d’une méthode. Il ne s’agit pas d’une coïncidence, comme vous le verrez dans une section ultérieure. Après l’exécution de cette ligne de code, il est possible d’accéder aux variables et aux méthodes membre de la classe, qui n’existent pas encore, avec l’opérateur “ ".

Une fois l’objet créé, vous n’avez pas à vous préoccuper de sa destruction.  Les objets en Java sont automatiquement éliminés par le ramasse-miettes (garbage collector), autrement dit, lorsqu’une référence à l’objet n’est plus utilisée, la machine virtuelle désalloue automatiquement toutes les ressources allouées par l’opérateur new.

II. L' objet par l'exemple

1. Généralités

Nous abordons maintenant, par l'exemple, la programmation objet. 

Un objet est une entité qui contient des données qui définissent son état (on les appelle des attributs ou propriétés) et des fonctions (on les appelle des méthodes). Un objet est créé selon un modèle qu'on appelle une classe :

public class C1{
       type1 p1; // propriété p1
       type2p2; // propriété p2
              …
       type3 m3(…){ // méthode m3
              …
       }
       type4 m4(…){ // méthode m4
              …
       }
       …
}

A partir de la classe C1 précédente, on peut créer de nombreux objets O1, O2,… Tous auront les propriétés p1, p2,… et les méthodes m3, m4, … Ils auront des valeurs différentes pour leurs propriétés pi ayant ainsi chacun un état qui leur est propre.

Par analogie la déclaration

int i,j;

crée deux objets (le terme estincorrect ici) de type (classe) int. Leur seule propriété est leur valeur.

Si O1 est un objet de type C1, O1.p1 désigne la propriété p1 de O1 et O1.m1 la méthode m1 de O1.

Considérons un premier modèle d'objet : la classe personne.

2. Définition de la classe personne

La définition de la classe personne sera la suivante :

import java.io.*;
public class personne{
        //attributs
       private String prenom;
       private String nom;
       private int age;
       //méthode
       public void initialise(StringP, String N, int age){
              this.prenom=P;
              this.nom=N;
              this.age=age;       
       }
       //méthode
       public void identifie(){
              System.out.println(prenom+","+nom+","+age);
       }
}

Nous avons ici la définition d'une classe, donc un type de donnée. Lorsqu'on va créer des variables de ce type, on les appellera des objets. Une classe est donc un moule à partir duquel sont construits des objets.

Les membres ou champs d'une classe peuvent être des données ou des méthodes (fonctions). Ces champs peuvent avoir l'un des trois attributs suivants :

  • privé Un champ privé (private) n'est accessible que par les seules méthodes internes de la classe
  • public Un champ public est accessible par toute fonction définie ou non au sein de la classe
  • protégé Un champ protégé (protected) n'est accessible que par les seules méthodes internes de la classe ou d'un objet dérivé (voir ultérieurement le concept d'héritage).

En général, les données d'une classe sont déclarées privées alors que ses méthodes sont déclarées publiques. Cela signifie que l'utilisateur d'un objet (le programmeur) a n'aura pas accès directement aux données privées de l'objet b pourra faire appel aux méthodes publiques de l'objet et notamment à celles qui donneront accès à ses données privées.

La syntaxe de déclaration d'un objet est la suivante :

public class nomClasse{
       private donnée ou méthode privée
       public donnée ou méthode publique
       protected donnée ou méthode protégée
}

Remarques

L'ordre de déclaration des attributs private, protected et public est quelconque.

Revenons à notre classe personne déclaréecomme :

Quel est le rôle de la méthode initialise? Parce que nom, prénom etage sont des données privées de la classe personne, les instructions

personne p1;
p1.prenom="Mostafa";
p1.nom="Sedoki";
p1.age=30;

sont illégales. Il nous faut initialiser un objet de type personne via une méthode publique. C'est le rôle de la méthode initialise.

On écrira :

personne p1;
p1.initialise("Mostafa","Sedoki",30);

L'écriture p1.initialise est légale car initialise est d'accès public.

Regardons le code de la méthode initialise :

public void initialise(StringP, String N, int age){
       this.prenom=P;
       this.nom=N;
       this.age=age;
}

L'instruction this.prenom=P signifie que l'attribut prenom de l'objet courant (this) reçoit la valeur P. Le mot clé this désigne l'objet courant : celui dans lequel se trouve la méthode exécutée. 

Comment le connaît-on ? Regardons comment se fait l'initialisation de l'objet référencé par p1 dans le programme appelant :

p1.initialise("Mostafa","Sedoki",30);

C'est la méthode initialise de l'objet p1 qui est appelée. Lorsque dans cette méthode, on référence l'objet this,on référence en fait l'objet p1.La méthode initialise aurait aussi pu être écrite comme suit :

public void initialise(StringP, String N, int age){
       prenom=P;
       nom=N;
       this.age=age;
}

Lorsqu'une méthode d'un objet référence un attribut A de cet objet, l'écriture this.A est implicite. On doit l'utiliser explicitement

lorsqu'ily a conflit d'identificateurs. C'est le cas de l'instruction :

this.age=age;

où age désigne un attribut de l'objet courant ainsi que le paramètre age reçu par la méthode. Il faut alors lever l'ambiguïté en désignant l'attribut age par this.age.

3. Un programme de test

Voici un programme de test :

public class test1{
       public static void main(Stringarg[]){
              personne p1=new personne();
              p1.initialise("Mostafa","Sedoki",30);
              p1.identifie();
       }
}

On peut s'étonner que le programme test1.java n'importe pas la classe personne avec une instruction :

import personne;

Lorsque le compilateur rencontre dans le code source une référence de classe non définie dans ce même fichier source, il recherche la classe à divers endroits :

  • dans les paquetages importés par lesinstructions import
  • dans le répertoire à partir duquel lecompilateur a été lancé

Dans notre exemple, le compilateur a été lancé depuis le répertoire contenant le fichier personne.class, ce qui explique qu'il a trouvé la définition de la classe personne. Il est possible de rassembler plusieurs classes dans un même fichier source. Rassemblons ainsi les classes personne et test1 dansle fichier source test2.java. La classe test1 est renommée test2 pour tenir compte du changement du nom du fichier source :

//paquetages importés
importjava.io.*;
class personne{
       //attributs
       private String prenom; //prénom de ma personne
       private String nom; // son nom
       private int age; //son âge
       //méthode
       public void initialise(String P, String N, int age){
              this.prenom=P;
              this.nom=N;
              this.age=age;
       }//initialise
       //méthode
       public void identifie(){
              System.out.println(prenom+","+nom+","+age);
       }//identifie
}//classe
public class test2{
       public static void main(String arg[]){
              personne p1=new personne();
              p1.initialise("Mostafa","Sedoki",30);
              p1.identifie();
       }
}

On notera que la classe personne n'a plus l'attribut public. En effet, dans un fichier source java, seule une classe peut avoir l'attribut public. C'est celle qui a la fonction main. Par ailleurs, le fichier source doit porter le nom de cette dernière. 

Par la suite, on utilisera indifféremment les deux méthodes :

  • classes rassemblées dans un unique fichier source
  • une classe par fichier source

4. Une autre méthode initialise

Considérons toujours la classe personne et rajoutons-lui la méthode suivante :

public void initialise(personne P){
       prenom=P.prenom;
       nom=P.nom;
       this.age=P.age;
}

On a maintenant deux méthodes portant le nom initialise : c'est légal tant qu'elles admettent des paramètres différents. C'est le cas ici.  Le paramètre est maintenant une référence P à une personne. Les attributs de la personne P sont alors affectés à l'objet courant (this). 

On remarquera que la méthode initialise aun accès direct aux attributs de l'objet P bien que ceux-ci soient de type private. C'est toujours vrai : les méthodes d'un objet O1 d'une classe C a toujours accès aux attributs privés des autres objets de la même classe C.

Voici un test de la nouvelle classe personne :

//import personne;
importjava.io.*;
public class test1{
       public static void main(Stringarg[]){
              personne p1=newpersonne();
              p1.initialise("Mostafa","Sedoki",30);
              System.out.print("p1=");
              p1.identifie();
              personne p2=newpersonne();
              p2.initialise(p1);
              System.out.print("p2=");
              p2.identifie();
       }
}

et ses résultats :

p1=Mostafa,Sedoki,30
p2=Mostafa,Sedoki,30

5. Constructeurs de la classe personne

Un constructeur est une méthode qui porte le nom de la classe et qui est appelée lors de la création de l'objet. On s'en sert généralement pour l'initialiser. C'est une méthode qui peut accepter des arguments mais qui ne rend aucun résultat. Son prototype ou sa définition ne sont précédés d'aucun type (même pas void).

Si une classe a un constructeur acceptant n arguments argi,  la déclaration et l'initialisation d'un objet de cette classe pourra se faire sous la forme :

classe objet =new classe(arg1,arg2, ... argn);

Ou 

classe objet;
objet=new classe(arg1,arg2, ... argn);

Lorsqu'une classe a un ou plusieurs constructeurs, l'un de ces constructeurs doit être obligatoirement utilisé pour créer un objet de cette classe. Si une classe C n'a aucun constructeur, elle en a un par défaut qui est le constructeur sans paramètres : public C(). Les attributs de l'objet sont alors initialisés avec des valeurs par défaut. C'est ce qui s'est passé lorsque dans les programmes précédents, on avait écrit:

personne p1;
p1=new personne();

Créons deux constructeurs à notre classe personne :

public class personne{
       //attributs
       private String prenom;
       private String nom;
       private int age;
       //constructeurs
       public personne(String P, String N, int age){
              initialise(P,N,age);
       }
       public personne(personne P){
              initialise(P);
       }
       //méthode
       public void initialise(String P, String N, int age){
              this.prenom=P;
              this.nom=N;
              this.age=age;
       }
       public void initialise(personne P){
              this.prenom=P.prenom;
              this.nom=P.nom;
              this.age=P.age;
       }
       //méthode
       public void identifie(){
              System.out.println(prenom+","+nom+","+age);
       }       
}

Nos deux constructeurs se contentent de faire appel aux méthodes initialise correspondantes. On rappelle que lorsque dans un constructeur, on trouve la notation initialise(P) par exemple, le compilateur traduit par this.initialise(P). Dans le constructeur, la méthode initialise est donc appelée pour travailler sur l'objet référencé par this, c'est à dire l'objet courant, celui qui est en cours de construction.

Voici un programme de test :

//import personne;
import java.io.*;
public class test1{
       public static void main(Stringarg[]){
              personne p1=new personne("Mostafa","Sedoki",30);
              System.out.print("p1=");
              p1.identifie();
              personne p2=new personne(p1);
              System.out.print("p2=");
              p2.identifie();
       }
}

et les résultats obtenus :

p1=Mostafa,Sedoki,30
p2=Mostafa,Sedoki,30

6. Les références d'objets

Nous utilisons toujours la même classe personne. Le programme de test devient le suivant :

//import personne;
import java.io.*;
public class test1{
       public static void main(Stringarg[]){
              //p1
              personne p1=new personne("Mostafa","Sedoki",30);
              System.out.print("p1=");p1.identifie();
              //p2 référence le même objet que p1
              personnep2=p1;
              System.out.print("p2=");p2.identifie();
              //p3 référence un objet qui sera une copie de l'objet référencé par p1
              personne p3=new personne(p1);
              System.out.print("p3=");p3.identifie();
              //on change l'état de l'objet référencé par p1
              p1.initialise("Sara","karkour",25);
              System.out.print("p1=");p1.identifie();
              //comme p2=p1, l'objet référencé par p2 a du changer d'état
              System.out.print("p2=");p2.identifie();
              //comme p3 ne référence pas le même objet que p1, l'objet référencé par p3 n'a pas du changer
              System.out.print("p3=");p3.identifie();
       }
}

Les résultats obtenus sont les suivants :

p1=Mostafa,Sedoki,30
p2=Mostafa,Sedoki,30
p3=Mostafa,Sedoki,30
p1=Sara,karkour,25
p2=Sara,karkour,25
p3=Mostafa,Sedoki,30

Lorsqu'on déclare la variable p1 par:

personne p1=new personne("Mostafa","Sedoki",30);

p1 référence l'objet personne("Mostafa","Sedoki",30) mais n'est pas l'objet lui-même.  En C, on dirait que c'est un pointeur, c.a.d.l'adresse de l'objet créé. Si on écrit ensuite :

p1=null

Ce n'est pas l'objet personne("Mostafa","Sedoki",30) qui est modifié, c'est la référence p1 qui change de valeur. L'objet personne("Mostafa","Sedoki",30) sera "perdu" s'il n'est référencé par aucune autre variable.

Lorsqu'on écrit :

personne p2=p1;

on initialise le pointeur p2 : il "pointe" sur le même objet (il désigne le même objet) que le pointeur p1. Ainsi si on modifie l'objet "pointé" (ou référencé) par p1, on modifie celui référencé par p2.

Lorsqu'on écrit :

personne p3=new personne(p1);

il y a création d'un nouvel objet, copie de l'objet référencé par p1. Ce nouvel objet sera référencé par p3. Si on modifie l'objet"pointé" (ou référencé) par p1,on ne modifie en rien celui référencé par p3. C'est ce que montrent les résultats obtenus.

7. Les objets temporaires

Dans une expression, on peut faire appel explicitement au constructeur d'un objet : celui-ci est construit, mais nous n'y avons pas accès (pour le modifier parexemple). Cet objet temporaire est construit pour les besoins d'évaluation de l'expression puis abandonné. L'espace mémoire qu'il occupait sera automatiquement récupéré ultérieurement par un programme appelé "ramassemiettes«  dont le rôle est de récupérer l'espace mémoire occupé par des objets qui ne sont plus référencés par des données du programme.

Considérons l'exemple suivant :

//import personne;
public class test1{
       public static void main(Stringarg[]){
              new personne(new personne("Mostafa","Sedoki",30)).identifie();
       }
}

et modifions les constructeurs de la classe personne afin qu'ils affichent un message :

//constructeurs
public personne(String P, String N, int age){
       System.out.println("Constructeur personne(String, String, int)");
       initialise(P,N,age);
}
public personne(personne P){
       System.out.println("Constructeur personne(personne)");
       initialise(P);
}

Nous obtenons les résultats suivants :

Constructeur personne(String, String, int)
Constructeur personne(personne)
Mostafa,Sedoki,30

montrant la construction successive des deux objets temporaires.

8. Méthodes de lecture et d'écriture des attributs privés

Nous rajoutons à la classe personne les méthodes nécessaires pour lire ou modifier l'état des attributs des objets :

public class personne{
       private String prenom;
       private String nom;
       private int age;
       public personne(String P, String N, int age){
              this.prenom=P;
              this.nom=N;
              this.age=age;
>       }
       public personne(personne P){
              this.prenom=P.prenom;
              this.nom=P.nom;
              this.age=P.age;
       }
       public void identifie(){
              System.out.println(prenom+","+nom+","+age);
       }
       //accesseurs
       public String getPrenom(){
              returnprenom;
       }
       publicStringgetNom(){
              returnnom;
       }
       public int getAge(){
              returnage;
       }
       //modifieurs
       public void setPrenom(StringP){
              thisthis.prenom=P;
       }
       publicvoid setNom(StringN){
              thisthis.nom=N;
       }
       public void setAge(int age){
              thisthis.age=age;
       }
}

Nous testons la nouvelle classe avec le programme suivant :

//import personne;
public class test1{
       public staticvoid main(String[]arg){
              personne P=new personne("Ismail","Sedoki",24);
              System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
              P.setAge(26);
              System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
       }
}

et nous obtenons les résultats suivants :

P=(Ismail,Sedoki,24)
P=(Ismail,Sedoki,26)

9. Les méthodes et attributs de classe

Supposons qu'on veuille compter le nombre d'objets personne créées dans une application. On peut soi-même gérer un compteur mais onrisque d'oublier les objets temporaires qui sont créés ici ou là. Il semblerait plus sûr d'inclure dans les constructeurs de la classe personne, une instruction incrémentant un compteur. Le problème est de passer une référence de ce compteur afin que le constructeur puisse l'incrémenter : il faut leur passer un nouveau paramètre. On peut aussi inclure le compteur dans la définition de la classe. Comme c'est un attribut de la classe elle-même et non d'un objet particulier de cette classe, on le déclare différemment avec le mot clé static :

private static long nbPersonnes;// nombre de personnes créées

Pour le référencer, on écrit personne.nbPersonnes pour montrer que c'est un attribut de la classe personne elle-même.Ici, nous avons créé un attribut privé auquel on n'aura pas accès directement en-dehors de la classe. On crée donc une méthode publique pour donner accès à l'attribut de classe nbPersonnes. Pour rendre la valeur de nbPersonnes la méthode n'a pas besoin d'un objet particulier : en effet nbPersonnes n'est pas l'attribut d'un objet particulier, il est l'attribut de toute une classe. Aussi a-t-on besoin d'une méthode de classe déclarée elle aussi static :

public static long getNbPersonnes(){
       return nbPersonnes;
}

qui de l'extérieur sera appelée avec la syntaxe personne.getNbPersonnes(). Voici un exemple.

La classe personne devient la suivante :

public class personne{
       //attribut de classe
       private static long nbPersonnes=0;
       //attributs d'objets
       …
       //constructeurs
       public personne(String P, String N, int age){
              initialise(P,N,age);
              nbPersonnes++;
       }
       public personne(personne P){
              initialise(P);
              nbPersonnes++;
       }
       //méthode
       …
       //méthode de classe
       public static long getNbPersonnes(){
              return nbPersonnes;
       }
}//class

Avec le programme suivant :

//import personne;
public class test1{
       public static void main(Stringarg[]){
              personne p1=new personne("Mostafa","Sedoki",30);
              personne p2=new personne(p1);
              System.out.println("Nombrede personnes créées : "+personne.getNbPersonnes());
       }//main
}//test1

on obtient les résultats suivants :

Nombre de personnes créées : 3

10. Passage d'un objet à une fonction

Nous avons déjà dit que Java passait les paramètres effectifs d'une fonction par valeur : les valeurs des paramètres effectifs sont recopiées dans les paramètres formels. Une fonction ne peut donc modifier les paramètres effectifs.

Dans le cas d'un objet, il ne faut pas se laisser tromper par l'abus de langage qui est fait systématiquement en parlant d'objet au lieu de référence d'objet. Un objet n'est manipulé que via une référence (un pointeur) sur lui. Ce qui est donc transmis à une fonction, n'est pas l'objet lui-même mais une référence sur cet objet. C'est donc la valeur de la référence et non la valeur de l'objet lui-même qui est dupliquée dans le paramètre formel : il n'y a pas construction d'un nouvel objet.

Si une référence d'objet R1 est transmise à une fonction, elle sera recopiée dans le paramètre formel correspondant R2. Aussi les références R2 et R1 désignent-elles le même objet. Si la fonction modifie l'objet pointé par R2,elle modifie évidemment celui référencé par R1 puisque c'est le même.

C'est ce que montre l'exemple suivant :

//import personne;
public class test1{
       public static void main(String arg[]){
              personne p1=new personne("Mostafa","Sedoki",30);
              System.out.print("Paramètre effectif avant modification : ");
              p1.identifie();
              modifie(p1);
              System.out.print("Paramètre effectif après modification : ");
              p1.identifie();
       }//main
       private static void modifie(personne P){
              System.out.print("Paramètre formel avant modification : ");
              P.identifie();
              P.initialise("Ahmed","karkour",52);
              System.out.print("Paramètre formel après modification : ");
              P.identifie();
       }//modifie
}//class

La méthode modifie est déclarée static parce que c'est une méthode de classe : on n'a pas à la préfixer par un objet pour l'appeler. Les résultats obtenus sont les suivants :

Constructeur personne(String, String, int)

Paramètre effectif avant modification : Mostafa,Sedoki,30

Paramètre formel avant modification : Mostafa,Sedoki,30

Paramètre formel après modification : ahmed,karkour,52

Paramètre effectif après modification : ahmed,karkour,52

On voit qu'il n'y a construction que d'un objet : celui de la personne  p1 de la fonction main et que l'objet a bien été modifié par la fonction modifie.

11. Encapsuler les paramètres de sortie d'une fonction dans un objet

A cause du passage de paramètres par valeur, on ne sait pas écrire en Java une fonction qui aurait des paramètres de sortie de type int par exemple car on ne sait pas passer la référence d'un type int qui n'est pas un objet. On peut alors créer une classe encapsulant le type int :

public class entieres{
       private int valeur;
       public entieres(int valeur){
              this.valeur=valeur;
       }
       public void setValue(int valeur){
              this.valeur=valeur;
       }
       public int getValue(){
              return valeur;
       }
}

La classe précédente a un constructeur permettant d'initialiser un entier et deux méthodes permettant de lire et modifier la valeur de cet entier. On teste cette classe avec le programme suivant :

//import entieres;
public class test2{
       public static void main(String []arg){
              entieres I=new entieres(12);
              System.out.println("I="+I.getValue());
              change(I);
              System.out.println("I="+I.getValue());
       }
       private static void change(entieres entier){
              entier.setValue(15);
       }
}

et on obtient les résultats suivants :

I=12
I=15

12. Un tableau de personnes

Un objet est une donnée comme une autre et à ce titre plusieurs objets peuvent être rassemblés dans un tableau :

//import personne;
public class test1{
       public static void main(String arg[]){
              personne[]amis=newpersonne[3];
              System.out.println("----------------");
              amis[0]=newpersonne("Mostafa","Sedoki",30);
              amis[1]=newpersonne("Sara","Karkour",25);
              amis[2]=newpersonne("Ismail","Sedoki",24);
              int i;
              for(i=0;i<amis.length;i++)
                     amis[i].identifie();
       }
}

L'instruction personne[] amis=new personne[3]; crée un tableau de 3 éléments de type personne. Ces 3 éléments sont initialisés ici avec la valeur null, c.a.d. qu'ils ne référencent aucun objet. De nouveau, par abus de langage, on parle de tableau d'objets alors que ce n'est qu'un tableau de références d'objets. La création du tableau d'objets, tableau quiest un objet lui-même (présence de new) ne crée donc en soi aucun objet du type de ses éléments : il faut le faire ensuite.

On obtient les résultats suivants :

Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Mostafa,Sedoki,30
Sara, Karkour, 25
Ismail, Sedoki, 24

III. L'héritage par l'exemple
1. Généralités
Nous abordons ici la notion d'héritage. Le but de l'héritage est de"personnaliser" une classe existante pour qu'elle satisfasse à nos besoins. Supposons qu'on veuille créer une classe enseignant: un enseignant est une personne particulière. Il a des attributs qu'une autre personne n'aura pas : la matière qu'il enseigne par exemple. Mais il a aussi les attributs de toute personne : prénom, nom et âge. Un enseignant fait donc pleinement partie de la classe personne mais a des attributs supplémentaires. Plutôt que d'écrire une classe enseignant en partant de rien, on préfèrerait reprendre l'acquis de la classe personne qu'on adapterait au caractère particulier des enseignants. C'est le concept d'héritage qui nous permet cela.

Pour exprimer que la classe enseignant hérite des propriétés de la classe personne, on écrira :

publi class enseignant extends personne

personne est appelée la classe parent (ou mère) et enseignant la classe dérivée (ou fille). Un objet enseignant a toutes les qualités d'un objet personne : il a les mêmes attributs et les mêmes méthodes. Ces attributs et méthodes de la classe parent ne sont pas répétées dans la définition de la classe fille : on se contente d'indiquer les attributs et méthodes rajoutés par la classe fille :

class enseignant extends personne{
       //attributs
       private int section;
       //constructeur
       public enseignant(String P, String N, int age,int section){
              super(P,N,age);
              this.section=section;
       }
}

Nous supposons que la classe personne est définie comme suit :

public class personne{
       private String prenom;
       private String nom;
       private int age;
       public personne(String P, String N, int age){
              this.prenom=P;
              this.nom=N;
              this.age=age;
       }
       public personne(personne P){
              this.prenom=P.prenom;
              this.nom=P.nom;
              this.age=P.age;
       }
       public String identite(){
              return"personne("+prenom+","+nom+","+age+")";
       }
       //accesseurs
       public String getPrenom(){
              returnprenom;
       }
       public String getNom(){
              return nom;
       }
       public int getAge(){
              return age;
       }
       //modifieurs
       public void setPrenom(String P){
              this.prenom=P;
       }
       public void setNom(StringN){
              this.nom=N;
       }
       public void setAge(int age){
              this.age=age;
       }
}

La méthode identifie a été légèrement modifiée pour rendre une chaîne de caractères identifiant la personne et porte maintenant le nom identite. Ici la classe enseignant rajoute aux méthodes et attributs de la classe personne :

  • un attribut section qui est le n° de section auquel appartient l'enseignant dans le corps desenseignants (une section par discipline en gros)
  • un nouveau constructeur permettant d'initialiser tous les attributs d'unenseignant 

2. Construction d'un objet enseignant

Le constructeur de la classe enseignant est le suivant :

//constructeur
public enseignant(StringP, String N, int age,int section){
       super(P,N,age);
       this.section=section;
}

L'instruction super(P,N,age) est un appel au constructeur de la classe parent, ici la classe personne. On sait que ce constructeur initialise les champs prenom, nom etage de l'objet personne contenu à l'intérieur de l'objet étudiant. Cela paraît bien compliqué et on pourrait préférer écrire :

//constructeur
public enseignant(StringP, String N, int age,int section){
       this.prenom=P;
       this.nom=N
       this.age=age
       this.section=section;
}

C'est impossible. La classe personne a déclaré privés (private) ses trois champs prenom,nom et age. Seuls des objets de la même classe ont un accès direct à ces champs. Tous les autres objets, y compris des objets fils comme ici, doivent passer par des méthodes publiques pour y avoir accès. Cela aurait été différent si la classe personne avait déclaré protégés (protected) les trois champs : elle autorisait alors des classes dérivées à avoir un accès direct aux trois champs. Dans notre exemple, utiliser le constructeur de la classe parent était donc la bonne solution et c'est la méthode habituelle : lors de la construction d'un objet fils, on appelle d'abord le constructeur de l'objet parent puis on complète les initialisations propres cette fois à l'objet fils (section dans notre exemple).

Tentons un premier programme :

//import personne;
//import enseignant;
public class test1{
       public static void main(String arg[]){
              System.out.println(new enseignant("Mostafa","Sedoki",30,27).identite());
       }
}

Ce programme ce contente de créer un objet enseignant (new)et de l'identifier. La classe enseignant n'a pas de méthode identité mais sa classe parent en a une qui de plus est publique : elle devient par héritage une méthode publique de la classe enseignant.

3. Surcharge d'une méthode

Dans l'exemple précédent, nous avons eu l'identité de la partie personne de l'enseignant mais il manque certaines informations propres à la classe enseignant (la section). On est donc amené à écrire une méthode permettant d'identifier l'enseignant :

class enseignant extends personne{
       int section;
       public enseignant(StringP, String N, int age,int section){
              super(P,N,age);
              this.section=section;
       }
       public String identite(){
              return"enseignant("+super.identite()+","+section+")";
       }
 }

La méthode identite de la classe enseignant s'appuie sur la méthode identitede sa classe mère (super.identite)pour afficher sa partie "personne" puis complète avec le champ section qui est propre à la classe enseignant.

La classe enseignant dispose maintenant deux méthodes identite:

  • celle héritée de la classe parent personne
  • la sienne propre

Si E est un ojet enseignant, E.identite désigne la méthode identite de la classe enseignant. On dit que la méthode identite de la classe mère est "surchargée" par la méthode identite de la classe fille. De façon générale, si O est un objet et M une méthode, pour exécuter la méthode O.M, le système cherche une méthode M dans l'ordre suivant :

  • dans la classe de l'objet O
  • dans sa classe mère s'il en a une
  • dans la classe mère de sa classe mère si elle existe
  • etc…

L'héritage permet donc de surcharger dans la classe fille des méthodes de même nom dans la classe mère. C'est ce qui permet d'adapter la classe fille à ses propres besoins. Associée au polymorphisme que nous allons voir un peu plus loin, la surcharge de méthodes est le principal intérêt de l'héritage.

Considérons le même exemple que précédemment :

//import personne;
//import enseignant;
public class test1{
       public static void main(String arg[]){
              System.out.println(new enseignant("Mostafa","Sedoki",30,27).identite());
       }
}

Les résultats obtenus sont cette fois les suivants :

enseignant(personne(Mostafa,Sedoki,30),27)

4. Le polymorphisme

Considérons une lignée de classes : C0   C1   C2   …  Cn

où Ci  Cj indique que la classe Cj est dérivée de la classe Ci. Cela entraîne que la classe Cj a toutes les caractéristiques de la classe Ci plus d'autres. Soient des objets Oi de type Ci. Il est légal d'écrire :

Oi=Oj avecj>i

En effet, par héritage, la classe Cj a toutes les caractéristiques de la classe Ci plus d'autres. Donc un objet Oj de type Cj contient en lui un objet de type Ci. 

L'opération Oi=Oj fait que Oi est une référence à l'objet de type Ci contenu dans l'objet Oj.

Le fait qu'une variable Oi de classe Ci puisse en fait référencer non seulement un objet de la classe Ci mais en fait tout objet dérivé de la classe Ci est appelé polymorphisme: la faculté pour une variable de référencer différents types d'objets.

Prenons un exemple et considérons la fonction suivante indépendante de toute classe :

public static void affiche(Object obj){
       ….
}

La classe Object est la "mère" de toutes les classes Java.  Ainsi lorsqu'on écrit :

public class personne

on écrit implicitement :

public class personne extends Object

Ainsi tout objet Java contient en son sein une partie de type Object. Ainsi on pourra écrire :

enseignant e;
affiche(e);

Le paramètre formel de type Object de la fonction affiche va recevoir une valeur de type enseignant. Comme enseignant dérive de Object, c'est légal.

5. Surcharge et polymorphisme

Complétons notre fonction affiche :

public static void affiche(Object obj){
       System.out.println(obj.toString());
}

La méthode obj.toString() rend une chaîne de caractères identifiant l'objet obj sous la forme nom_de_la_classe@adresse_de_l'objet. Que se passe-t-il dans le cas de notre exemple précédent :

enseignant e=new enseignant(...);
affiche(e);

Le système devra exécuter l’instruction System.out.println(e.toString()) où e est un objet enseignant. Il va chercher une méthode toString dans la hiérarchie des classes menant à la classe enseignant en commençant par la dernière :

  • dans la classe enseignant, il ne trouve pas de méthode toString()
  • dans la classe mère personne, il ne trouve pas de méthode toString()
  • dans la classe mère Object, il trouve la méthode toString() et l'exécute

C'est ce que montre le programme suivant :

//import personne;
//import enseignant;
public class test1{
       public static void main(String arg[]){
              enseignante=new enseignant("hamdoun","morji",31,61);
              affiche(e);
              personne p=new personne("Mostafa","Sedoki",30);
              affiche(p);
       }
       public static void affiche(Objectobj){
              System.out.println(obj.toString());
       }
}

Les résultats obtenus sont les suivants :

enseignant@1ee789
personne@1ee770

C'est à dire nom_de_la_classe@adresse_de_l'objet. Comme ce n'est pas très explicite,on est tenté de définir une méthode toString pour les classes personne et etudiant qui surchargeraient la méthode toString de la classe mère Object. Plutôt que d'écrire des méthodes qui seraient proches des méthodes identite déjà existantes dans les classes personne et enseignant, contentons-nous de renommer toString ces méthodes identite :

public class personne{
       ...
       public String toString(){
              return "personne("+prenom+","+nom+","+age+")";
       }
       ...
}
class enseignant extends personne{
       int section;
       …
       public String toString(){
              return "enseignant("+super.toString()+","+section+")";
       }
}

Avec le même programme de test qu'au paravant, les résultats obtenus sont les suivants :

enseignant(personne(hamdoun,morji,31),61)
personne(Mostafa,Sedoki,30)

VI. Classes internes

Une classe peut contenir la définition d'une autre classe. Considérons l'exemple suivant :

//classes importées
import java.io.*;
public class test1{
       //classe interne
       private class article{
              //on définit la structure
              private String code;
              private String nom;
              private double prix;
              private int stockActuel;
              private int stockMinimum;
              //constructeur
              public article(String code, String nom, double prix, int stockActuel,int stockMinimum){
                     //initialisation des attributs
                     this.code=code;
                     this.nom=nom;
                     this.prix=prix;
                     this.stockActuel=stockActuel;
                     this.stockMinimum=stockMinimum;
              }//constructeur
              //toString
              public String toString(){
                     return"article("+code+","+nom+","+prix+","+stockActuel+","+stockMinimum+")";
              }//toString
       }//classe article
       //données locales
       private article art=null;
       //constructeur
       public test1(String code, String nom, double prix, int stockActuel, int stockMinimum){
              //définition attribut
              art=newarticle(code, nom, prix, stockActuel,stockMinimum);
       }//test1
       //accesseur
       public article getArticle(){
              return art;
       }//getArticle
       public static voidmain(String arg[]){
              //création d'une instance test1
              test1t1=new test1("a100","velo",1000,10,5);
              //affichage test1.art
              System.out.println("art="+t1.getArticle());
       }//main
}//fin class

La classe test1 contient la définition d'une autre classe, la classe article. On dit que article est une classe interne à la classe test1. Cela peut être utile lorsque la classe interne n'a d'utilité que dans la classe qui la contient. Lors de la compilation du source test1.java ci dessus, on obtient deux fichiers .class :

Un fichier test1$article.class a été généré pour la classe article interne à la classe test1.class. Si on exécute le programme ci-dessus,on obtient les résultats suivants : art=article(a100,velo,1000.0,10,5)

V. Les interfaces

Une interface est un ensemble de prototypes de méthodes ou de propriétés qui forme un contrat. Une classe qui décide d'implémenter une interface s'engage à fournir une implémentation de toutes les méthodes définies dans l'interface. C'est le compilateur qui vérifie cette implémentation.

Voici par exemple la définition de l'interface java.util.Enumeration:

Method Summary
boolean hasMoreElements() Testsif this enumeration contains moreelements.
Object nextElement() Returns the next element of this enumeration if this enumeration object hasat least one more element to provide.

Toute classe implémentant cette interface sera déclarée comme

public class C implements Enumeration{
       ...
       boolean hasMoreElements(){....}
       ObjectnextElement(){...}
}

Les méthodes hasMoreElements() et nextElement() devront être définies dans la classe C.

Considérons le code suivant définissant une classe élève définissant le nom d'un élève et sa note dans une matière :

//une classe élève
public class élève{
       //des attributs publics
       public String nom;
       public double note;
       //constructeur
       public élève(StringNOM, doubleNOTE){
              nom=NOM;
              note=NOTE;
       }//constructeur
}//élève

Nous définissons une classe notes rassemblant les notes de tous les élèves dans une matière :

//classes importées
//import élève
//classe notes
public class notes{
       //attributs
       protected String matière;
       protected élève[]élèves;
       //constructeur
       public notes(String MATIERE, élève[] ELEVES){
              //mémorisation élèves & matière
              matière=MATIERE;
              élèves=ELEVES;
       }//notes
       //toString
       public String toString(){
              String valeur="matière="+matière +", notes=(";
              int i;
              //on concatène toutes les notes
              for(i=0;i<élèves.length-1;i++){
                     valeur+="["+élèves[i].nom+","+élèves[i].note+"],";
              }
             //dernière note
              if(élèves.length!=0) {
                     valeur+="["+élèves[i].nom+","+élèves[i].note+"]";
              }
              valeur+=")";
              //fin
              return valeur;
       }//toString
}//classe

Les attributs matière et élèves sont déclarés protected pour être accessibles d'une classe dérivée. Nous décidons de dériver la classe notes dans une classe notesStats qui aurait deux attributs supplémentaires, la moyenne et l'écart-type des notes :

 La classe notesStats dérive de la classe notes et implémente l'interface Istats suivante :

public class notesStats extends notes implements Istats{
       //attributs
       private double _moyenne;
       private double _écartType;
}
//une interface
public interface Istats{
       double moyenne();
       double écartType();
} //Cela signifie que la classe notesStats doit avoir deux méthodes appelées moyenne et écartType avec la signature indiquée dans l'interface Istats. 

La classe notesStats est la suivante :

//classes importées
//import notes;
//import Istats;
//import élève;
public class notesStats extends notes implements Istats{
       //attributs
       privatedouble _moyenne;
       privatedouble _écartType;
       //constructeur
       public notesStats(String MATIERE, élève[] ELEVES){
              //construction de la classe parente
              super(MATIERE,ELEVES);
              //calcul moyenne des notes
              doublesomme=0;
              for(inti=0;i<élèves.length;i++){
                     somme+=élèves[i].note;
              }
              if(élèves.length!=0) _moyenne=somme/élèves.length;
              else _moyenne=-1;
              //écart-type
              double carrés=0;
              for(inti=0;i<élèves.length;i++){
                     carrés+=Math.pow((élèves[i].note-_moyenne),2);
              }//for
              if(élèves.length!=0) _écartType=Math.sqrt(carrés/élèves.length);
              else _écartType=-1;
       }//constructeur
       //ToString
       public String toString(){
              return super.toString()+",moyenne="+_moyenne+",écart-type="+_écartType;
       }//ToString
       //méthodes de l'interface Istats
       public double moyenne(){
              //rend la moyenne des notes
              return_moyenne;
       }//moyenne
       public double écartType(){
              //rend l'écart-type
              return_écartType;
       }//écartType
}//classe

La moyenne _moyenne et l'écart-type _ecartType sont calculés dès la construction de l'objet. Aussi les méthodes moyenne et écartType n'ont-elles qu'à rendre la valeur des attributs _moyenne et _ecartType. Les deux méthodes rendent -1 si le tableau des élèves est vide.

La classe de test suivante :

//classes importées
//import élève;
//import Istats;
//import notes;
//import notesStats;
//classe de test
public class test{
       public static void main(String []args){
              //qq sélèves & notes
              élève[]ELEVES=new élève[]{ new élève("Ismail",14),new élève("Mostafa",16),new élève("Dounia",18)};
              //qu'on enregistre dans un objet notes
              notes anglais=new notes("anglais",ELEVES);
              //et qu'on affiche
              System.out.println(""+anglais);
              //idem avec moyenne et écart-type
              anglais=new notesStats("anglais",ELEVES);
              System.out.println(""+anglais);
       }//main
}//classe

donne les résultats :

matière=anglais, notes=([Ismail,14.0],[Mostafa,16.0],[Dounia,18.0])
matière=anglais,notes=([Ismail,14.0],[Mostafa,16.0],[Dounia,18.0]),moyenne=16.0, écarttype=1.632993161855452

La classe notesStats aurait très bien pu implémenter les méthodes moyenne et écartType pour elle-même sans indiquer qu'elle implémentait l'interface Istats. Quel est donc l'intérêt des interfaces ?  C'est le suivant : une fonction peut admettre pour paramètre une donnée ayant le type d'une interface I. Tout objet d'une classe C implémentant l'interface I pourra alors être paramètre de cette fonction. Considérons l'interface suivante :

//une interface Iexemple
public interface Iexemple{
       int ajouter(inti,intj);
       int soustraire(inti,intj);
}//interface

L'interface Iexemple définit deux méthodes ajouter et soustraire. La classe classe1  implémentent cette interface.

//classes importées
//import Iexemple;
public class classe1 implements Iexemple{
       public int ajouter(inta,intb){
              return a+b+10;
       }
       public int soustraire(inta,intb){
              return a-b+20;
       }
}//classe


Partager ce cours avec tes amis :

Rédigé par M. ESSADDOUKI

Learning a new programming language is an easy thing, but the most difficult thing is how to design efficient algorithms for real-world problems, so don't be a programmer, be a problems solver.

Cours Similaires :