next up previous contents index
Next: 7 Héritage Up: Java: Le langage Previous: 5 Les structures de

Subsections

6 Classes et Objets

     Dans le langage C , on a l'habitude créer des objets complexes à l'aide de structures . Dans les langages orientés objets, on créera plutôt des classes . Les classes constituent le concept de base de le programmation objet. Elles permettent de définir des nouveaux types de données qui doivent se comporter ``comme'' des types pré définis et dont les détails d'implantation sont cachés aux utilisateurs de ces classes Seule l'interface fournie par son concepteur pourra être utilisée. Comme nous le verrons plus loin, contrairement aux types prédéfinis, les classes peuvent être créées de manière hiérarchique : une classe est souvent une sous-classe d'une autre classe.

   Un objet est une instance d'une certaine classe ; au lieu de parler d'une variable d'une certaine classe, on dira plutôt un objet d'une certaine classe. En Java , on ne peut accéder aux objets qu'à travers une référence vers celui-ci. Une référence est, en quelque sorte, un pointeur vers la structure de donnée ; la différence entre une référence et un pointeur est qu'il n'est pas permis de manipuler les références comme les pointeurs de C ou C++ . On ne peut connaître la valeur de la référence et on ne peut évidemment pas effectuer d'opérations arithmétiques sur les références. La seule chose permise est de changer la valeur de la référence pour pouvoir ``faire référence'' à un autre objet.

Une classe définit généralement deux choses :

6.1 Déclaration des classes

6.1.1 Champs

              Java (comme C++ ) possède trois mots clés pour l'encapsulation des données  : public, private et protected. Les données et méthodes déclarées private ne sont accessibles que par les méthodes de sa propre classe. Inversement les informations déclarées public sont accessibles par toutes les classes. Nous verrons plus loin la signification du mot clé protected.

Imaginons que l'on veuille déclarer une structure de donnée Date constituée de trois entiers codant le jour, le mois et l'année. Nous allons pour ce faire, définir une classe Date de la manière suivante :

 
class Date {
    private int mois ;
    private int jour ;
    private int  année ;
    ...
}
Les données mois, jour et année sont des données privées. Elles ne sont accessibles que par les seules méthodes de cette classe. Pour que l'on puisse modifier ces champs, il faut que l'on fournisse les méthodes permettant de manipuler ces données privées.

6.1.2 Méthodes

     Comme en C++ , les méthodes sont définies par : Comme les champs d'une classe, les méthodes doivent être qualifiées de public, private ou protected. Les méthodes private ne peuvent être invoquées que par les seules méthodes de cette classe.
 
class Date {
    private int mois ;
    private int jour ;
    private int  année ;
    ...
    public void affecter(int m, int j, int a) {
       mois = m ; jour = j ; année = a ;
    }
}
      La méthode affecter fait partie de la classe Date ; il lui est donc permis d'accéder aux champs privés mois, jour et année. Grâce à cette méthode affecter, puisque elle est déclarée public, on pourra désormais affecter les champs mois, jour et année d'un objet de type Date. Les méthodes publiques de la classe Date constituent l'interface publique de cette classe.
 
class Date {
    private int mois ;
    private int jour ;
    private int  année ;
    public void affecter(int m, int j, int a) {
        mois = m ; jour = j ; année = a ;
    }
    public int QuelJour() { return jour ; }
    public int QuelMois() { return mois ; }
    public int QuelleAnnée() { return année ; }
    public void JourSuivant() { ... }
    public void imprimer() { ... }
}
Contrairement au langage C++ , la définition effective des méthodes de la classe doit se faire dans la définition de la classe elle-même.
 
class Date {
    ...
    public void imprimer() { // imprimer la date
        System.out.println(jour + "/" + mois + "/" + année) ;
    }
}

6.1.3 Objet et méthodes associées

        Une fois la classe déclarée, pour pouvoir utiliser un objet de cette classe, il faut définir une instance (ou un objet ) de cette classe. Nous avons dit que les objets ne sont accessibles qu'à travers des références . Une définition qui se contente de définir un objet comme ``une variable ayant le type de la classe choisie '' ne fait que définir une référence vers un éventuel objet de cette classe.
 
Date d ;
La variable d représente une référence vers un objet de type Date ; Si l'on veut un objet effectif, il faut la créer explicitement avec le mot clé new et le constructeur de la classe Date.
 
Date d ;
d =  new Date() ;
Comme on l'a déjà dit, une méthode est un message que l'on envoie à un objet. Ainsi, pour afficher la date contenue dans l'objet d, on lui envoie le message imprimer :
 
d.imprimer() ;
De telles méthodes sont appelées méthodes d'instance ; nous verrons plus loin qu'il existe un autre type de méthode qu'on appelle méthode de classe .

Cette méthode n'est utilisable (ailleurs que les méthodes de la classe Date) que parce qu'elle fait partie de l'interface de cette classe i.e. qu'elle fait partie des méthodes publiques. Par contre, il ne sera pas possible d'accéder aux champs d.jour, d.mois et d.annee : ce sont des données privées .

Les structures de données d'une classe donnée, sont dupliqués pour chaque objet de cette classe. Si l'on définit deux objets de type Date, ils possèdent, tous les deux, leur propre exemplaire des champs jour, mois et année. La modification d'un de ces champs pour un objet n'affecte évidemment pas la valeur du même champ pour l'autre objet. De tels champs sont appelés variables d'instance. Nous verrons plus loin que nous pourront définir des champs où toutes les instances d'une même classe partagent le même champ (champs static).

Quant aux méthodes, elles ne sont évidemment pas dupliquées pour chaque objet. Un seul exemplaire de toutes les méthodes définies dans une classe suffit. On remarquera également (pour le moment du moins) que les méthodes ne peuvent être invoquées qu'en utilisant un objet. Nous verrons plus loin que des méthodes particulières n'obéissent pas à cette restriction (méthodes static).

6.1.4 Constructeurs

   Lorsque l'on définit un objet d'une classe, il est souvent utile de pouvoir initialiser cet objet. Avec la définition de notre classe Date, il est évidemment possible, avec la méthode affecter, d'affecter les champs jour, mois et année.
 
Date d ;
d =  new Date() ;
d.affecter(10, 3, 87) ;
Mais cette façon de faire, n'est pas la plus agréable. Une meilleure façon de faire consiste à définir une méthode spécifique d'initialisation des champs qui sera automatiquement appelée lors de la création d'un objet. Cette fonction s'appelle constructeur.
 
class Date  {        ...
    public Date(int j, int m, int a) { jour = j ; mois = m ; année = a ; }
}
Notez que le constructeur se reconnaît par le fait qu'il porte le même nom que la classe et qu'il n'a pas de valeur de retour (pas même void). Voici, à présent, des exemples d'utilisation correcte et incorrecte :
 
Date noel97, dateDeNaissance ;
noel97 =  new Date(25, 12, 97) ;                 // Correct
dateDeNaissance = new Date() ;                  // Incorrect
La création de l'objet référencé par dateDeNaissance est incorrecte. En effet, le constructeur de la classe Date dont nous disposons requiert trois arguments ; il n'est donc pas possible de créer un objet de la classe Date sans donner le jour, le mois et l'année en argument. On doit contourner ce problème en fournissant soit plusieurs constructeurs (à 0, 1, 2 et 3 arguments) :
 
class Date  {
    ...
    public Date(int j, int m, int a) { jour = j ; mois = m ; année = a ; }
    public Date(int j, int m) { jour = j ; mois = m ; année = 57 ; }
    public Date(int j) { jour = j ; mois = 9 ; année = 57 ; }
    public Date() { jour = 15 ; mois = 9 ; année = 57 ; }
}
ou plus simplement par :
 
class Date  {
    ...
    public Date(int j, int m, int a) { jour = j ; mois = m ; année = a ; }
    public Date(int j, int m) { this(j, m, 75) }
    public Date(int j) this(j, 9, 57) ; }
    public Date() { this(15, 9, 57) ; }
}
Pourquoi alors, avant la définition de nos propres constructeurs, nous avons pu créer un objet de type Date sans lui passer de paramètres ? Il ne vous rester plus qu'arriver à la section concernant les constructeurs par défaut (voir 7.2.2).

Plus que pour l'initialisation des membres de la classe, le constructeur est particulièrement utile lorsque l'objet que l'on veut créer requiert d'autres structures de données alloués dynamiquement. Par exemple, une fenêtre graphique est un objet constitué de la fenêtre elle-même et d'autres objets tels que des boutons, des menus et autres gadgets graphiques. Le constructeur d'un tel objet se chargera donc de créer tous les autres objets dont il a besoin.

Imaginons que l'on veuille compléter la classe Date avec une chaîne de caractères qui précise un fait marquant associé à un objet de la classe Date. L'initialisation des variables d'instances se fait dans l'ordre suivant :  

6.1.5 Destructeur

       Contrairement aux langage C++ , nous allons pouvoir, dans beaucoup de cas, ne plus nous soucier de la restitution de l'espace mémoire consommée. Java dispose d'un système de récupération de mémoire automatique. Java estime que l'espace occupé par objet peut être restitué au système quand il n'y a plus aucune référence vers cet objet.

Par défaut, le récupérateur de mémoire fonctionne en arrière plan pendant l'exécution d'un programme Java . Il est possible de supprimer cette récupération en donnant l'option -noasyngc sur la ligne de commande du lancement de la machine virtuelle. La récupération de mémoire peut alors être invoquée explicitement pas le programmeur à des moments bien précis avec la méthode System.gc().

    Avant l'appel effective à la récupération d'un objet, la machine virtuelle Java fait appel à la méthode finalize. A quoi donc peut servir cette méthode puisque la récupération de mémoire se charge de tout ? La raison en est simple : Java peut s'occuper de la récupération des objets Java et rien d'autre.

Par exemple, un programme qui utilise des ressources systèmes (les descripteurs de fichiers, les sockets , etc.), c'est lors de l'invocation de la méthode finalize que le programmeur se chargera de libérer ses ressources ; Java ne pourra pas le faire tout seul. Une classe qui contient la méthode finalize devra avoir le squelette suivant :

 
protected void finalize() throw Throwable {
    super.finalize() ;
    // Code propre aux objets de cette classe.
    ...
}
Prenez ce bout de code tel quel, même s'il y a beaucoup de choses mystérieuses. Après la lecture des chapitres sur l'héritage et les exceptions, tout ceci devrait s'éclaircir.

6.2 Définitions de champs

       Les objets des classes que nous avons définis avaient leur propre jeu de données privées et publiques . Par exemple, les objets de la classe Date possèdent chacun leur propre champ jour, mois et année.

Il existe des cas où il est souhaitable d'avoir une même donnée qui soit commune à tous les objets d'une classe. Un champ d'une classe est dit static lorsqu'il n'y a qu'un exemplaire de ce champ pour l'ensemble des objets de cette classe. Ce champ existe même s'il n'existe aucun objet de cette classe. Les champs static sont parfois appelés variables de classe commence à exister à partir du moment ou une classe est initialisée.

 
class Date {
    private int mois ;
    private int jour ;
    private int  année ;
    public static int nbDate = 0 ;
    public Date(int m, int j, int a) {
        mois = m ; jour = j ; année = a ;
        nbDate++ ;
    }
    ...

    public static void main(String args[]) {
        Date noel97 = new Date(25, 12, 97) ;
        Date dateDeNaissance = new Date(15, 9, 57) ;
        noel97.imprimer() ;
        dateDeNaissance.imprimer() ;
        System.out.println(noel97.nbDate) ;
        System.out.println(dateDeNaissance.nbDate) ;
    }
}
Dans cet exemple, Les champs jour, mois et année des objets noel97 et dateDeNaissance sont des variables d'instance ; autrement dit, chacun de ces objets possède leur propre instance de ces champs. La modification d'un de ces champs pour un objet donné n'influe pas sur ce même champ de l'autre objet. Par contre, le champ nbDate est déclaré static, c'est donc une variable de classe . Les deux objets noel97 et dateDeNaissance partagent alors la même structure de donnée. Si l'on modifie la valeur de ce champ à partir d'un objet, cette modification est valide pour l'autre objet. C'est ainsi que dans cet exemple, on peut compter le nombre d'objets de type Date que l'on crée en incrémentant la valeur du champ nbDate dans le constructeur de la classe Date. On obtient donc le résultat suivant :
 
25/12/97                          noel97.imprimer()
15/9/57                           dateDeNaissance.imprimer()
2                                 System.out.println(noel97.nbDate) ;
2                                 System.out.println(dateDeNaissance.nbDate) ;

6.2.1 Initialisation des champs static

   

Les champs static sont initialisés une fois lors de l'initialisation de la classe qui les contient. Une erreur de compilation se produit lorsque

6.2.2 Initialisation des champs non-static

   

Les champs non static (variables d'instance) sont initialisés lors de la création des objets (des instances) de la classe. Contrairement aux champs static, chaque création d'objets provoque l'initialisation des variables d'instances de cet objet. Une erreur de compilation se produit lorsque une variable d'instance est initialisée par référence à une variable d'instance définie plus loin (textuellement) dans la définition de la classe. On pourra utiliser les valeurs des variables de classe pour initialiser les variables d'instance puisque la création et l'initialisation de la classe se fait bien avant la création des objets de la classe.

 
class X {
     int x  = y  + 1 ;             // Erreur !
     int y = 0 ;                   // O.K. !
     int z = z+1 ;                 // Erreur !
}

6.2.3 Le mot clé this

    Le mot clé this désigne l'objet sur lequel la méthode est invoquée. Par exemple, la méthode affecter peut se réécrire de la manière suivante :
 
    public void affecter(int m, int j, int a) {
        this.mois = m ; this.jour = j ; this.annee = a ;
    }
L'intérêt du mot clé this n'est évident pas dans ce cas là ; par contre si l'on voulait créer une liste de toutes les objets de type Date crées, on ne pourra se passer de ce mot clé this pour créer le chaînage.
 
class Date {
    private int mois ;
    private int jour ;
    private int  année ;
    private Date suivant ;
    public static Date listeDates = null ;
    public Date(int m, int j, int a) {
        mois = m ; jour = j ; année = a ;
        suivant = listeDates ;
        listDates = this ;
    }
    ...
}

class Test {
    public static void main(String args[]) {
        Date noel97 = new Date(25, 12, 97) ;
        Date dateDeNaissance = new Date(15, 9, 57) ;
        for (Date d = Date.listeDates ; d != null ; d = d.suivant)
            d.imprimer() ;
    }
}

6.2.4 Champs final

Un champ d'une classe peut être qualifié de final. Ce qualifier indique au compilateur que ce champ ne peut être modifié et gardera tout au long de son existence une valeur constante. Le compilateur produira donc une erreur lorsqu'il y aura une tentative de modification de la valeur de ce champ. L'intérêt de ce qualifier est triple : Si l'on ne peut modifier la valeur de champ, comment lui donner une valeur initiale ? Et bien, en l'initialisant ! En effet, seule l'initialisation de ce champ est permise ; s'il s'agit d'une donnée primitive c'est une initialisation classique et s'il s'agit d'une référence alors l'initialisation lui affectera une référence un objet. Une référence qualifiée de final n'interdit pas la modification de l'objet référencé ; l'objet pourra être modifié mais la référence désignera toujours le même objet.

Tous les champs peuvent être qualifiés de final, que ce soit des variables de classes ou des variables d'instance.

6.3 Définitions de méthodes

6.3.1 Passage de paramètres

  Lors des appels aux méthodes, tous les paramètres sont passés par valeur . Le concept de passage par adresse n'existe pas. Rappelons que les seuls types possibles de paramètres sont les types primitifs et les références. Autrement dit,

6.3.2 Signature et polymorphisme

       

Contrairement aux langage C , un même identificateur peut être utilisé par désigner deux méthodes à condition que leur signature soit différente. On appelle signature d'une méthode, la donnée de son nom, du nombre de ses paramètres formels et de leur type.

 
int une_méthode(int i)  { ... }         // Erreur ! Le retour de la méthode
float une_méthode(int i)  { ... }       // ne fait pas partie de la signature

int une_méthode(int i)  { ... }         // O.K. !
float une_méthode(float f)  { ... }     // Ces deux méthodes ont des signatures distinctes

int une_méthode(int i)  { ... }         // O.K. !
int une_méthode(int i, int j)  { ... }  // Ces deux méthodes ont des signatures distinctes

6.3.3 Variables locales

Les variables locales sont allouées lors de l'invocation de la méthode et sont détruites à la fin de celle-ci. Ces variables ne sont visibles qu'à l'intérieur de la méthode.

Les variables locales doivent avoir été affectées avec leur utilisation ; dans le cas contraire, une erreur de compilation est engendrée. Cette valeur peut être donnée par initialisation (à la définition de la variable) ou par affectation. Ces exigences, loin d'être des contraintes, sont des aides précieuses pour le programmeur !

 
void une_méthode() {
    int i  ; j , k ;
    j = i ;                      // Erreur de compilation !
    if (...) {
         k = 1 ;
    }
    j = k ;                      // Erreur de compilation !
}
Lorsqu'une méthode est invoquée par différents threads (voir 20), chaque thread possède ses propres variables locales et paramètres.

Un objet référencé par une variable locale, peut continuer à exister après la fin de la méthode, même si cet objet a été crée dans cette méthode. Cet objet sera restitué au système que lorsqu'il n'y a plus aucune référence vers cet objet.

6.3.4 Méthodes static

Jusqu'à présent, les méthodes que nous avons vues s'appliquent toujours sur un objet ou plus exactement sur une référence vers un objet. Les méthodes qu'on qualifie de static sont celles qui n'ont pas besoin d'un objet pour être invoquée. Ces méthodes se rapprochent des fonctions classiques du langage C et sont appelés méthodes de classe .

Comme toutes les méthodes, une méthode static est toujours membre d'une classe ; elle est invoquée en lui associant, non pas un objet, mais la classe à laquelle elle appartient. Par exemple, la méthode sqrt qui calcule la racine carrée d'un nombre, appartient à la classe Math. Pour invoquer cette méthode, on utilisera la syntaxe suivante :

 
Math.sqrt(x) ;          // Math désigne non pas un objet, mais une classe
Une méthode static, puisqu'elle ne s'applique pas sur un objet, ne peut accéder aux variables d'instances (sauf de celles passées en paramètre). De même, le mot clé this n'a pas de sens dans une méthode static.
 
class Date {
    private int mois ;
    private int jour ;
    private int  année ;
    private Date suivant ;
    private static Date listeDates = null ;
    public Date(int m, int j, int a) {
        mois = m ; jour = j ; année = a ;
        suivant = listeDates ;
        listDates = this ;
    }
    ...
    public static void listerDates()  {
        for (Date d = Date.listeDates ; d != null ; d = d.suivant)
            d.imprimer() ;
    }
}

class Test {
    public static void main(String args[]) {
        Date noel97 = new Date(25, 12, 97) ;
        Date dateDeNaissance = new Date(15, 9, 57) ;
        Date.listerDates() ;
    }
}

6.4 Bloc d'initialisation static

Les variables statiques peuvent être initialisées lors de leur déclaration. Il est parfois utile de disposer, non pas de simples initialisations, mais d'un ensemble d'instructions plus complexes pour réaliser l'initialisation des champs statiques. On peut faire le parallèle avec les variables d'instances. Celles-ci disposent du constructeur pour effectuer des initialisations complexes. De même, les blocs d'initialisation statique permettent d'effectuer des initialisations complexes sur les champs statiques.

Contrairement aux constructeurs, les blocs d'initialisation statique n'ont pas une syntaxe proche d'une méthode. En effet, ces initialisations ont lieu au moment du chargement d'une classe et c'est le système qui se charge d'invoquer ces initialisations automatiquement au chargement. Il n'est donc pas question de passer des paramètres à ces initialisations. De plus, comme pour les constructeurs, les initialisations ne retournent aucune valeur. Bref, les blocs d'initialisation peuvent être comparées à des méthodes sans paramètres et sans valeur de retour ; il n'est donc pas utile de les nommer. C'est ainsi que la syntaxe des initialisations statiques ne ressemble en rien à des méthodes ; il ne s'agit que de blocs d'instructions préfixés par le mot clé static.

6.5 Conversion des types primitifs en objets et inversement

Comme nous le verrons plus en détails dans le chapitre 12, il n'est pas possible de transformer les types primitifs en objets par une opération de changement de type (cast ). Par contre, Java définit pour des classes spéciales pour un certain nombre de types primitifs (Integer, Float, Boolean, etc.. Par exemple, on créera une instance de la classe Integer ayant pour valeur 10 de la manière suivante :

 
Integer instanceInteger = new Integer(10);
Inversement la classe Integer dispose de méthode qui permettent d'obtenir la valeur du champ entier d'une instance de cette classe.
 
int i = instanceInteger.intValue();    // retourne 10
Une description détaillée de ces classes est donnée en 17.2.


next up previous contents index
Next: 7 Héritage Up: Java: Le langage Previous: 5 Les structures de
Touraivane
6/12/1998