Page d'accueilFindIt !ContactLes notions de baseObjets, tableaux et chaînes

Le langage JavaTM

Table des matièresHierarchie des classes

CJava

 Création et utilisation des classes

Déclaration des classes et des interfaces
Déclaration des champs
Déclaration des méthodes
Création d'objets : opérateur new
Outrepasser une méthode
Destruction des objets

 

Déclaration des classes et des interfaces

C

Les seuls types que le programmeur peut définir en Java sont des classes ou des interfaces. Donc, les mots-clés struct et union n'existent pas en Java. Les énumérations enum disponibles à partir de Java 5.0, sont en fait des catégories de classes spéciales. De plus, l'équivalent des template n'existe que depuis Java 5.0 aussi.

Identifiants

Les identifiants que vous créez en Java (classes, interfaces, champs, méthodes, paramètres, variables locales,...) peuvent être n'importe quelle suite illimitée de lettres ou de chiffres Unicode, de caractères _ ou $. Seul le premier caractère ne doit pas être un chiffre. Ils doivent bien sûr être différents des mots-clés Java.
Par conséquent, il est possible d'utiliser des caractères accentuées pour une meilleure lisibilité de vos programmes.

C

Les identifiants sont codés comme en C/C++, mais en Java vous pouvez utilisez en plus toutes les lettres et tous les chiffres Unicode et le caractère $.
Comme en C, le compilateur fait la nuance entre les minuscules et les majuscules.


Vous pouvez créer des identifiants avec des lettres accentuées, ce qui n'est pas conseillé car la plupart des éditeurs de texte et des systèmes fonctionnant en ligne de commande (comme MS/DOS ou UNIX) n'utilisent pas Unicode pour les lettres accentuées (qui ne sont pas ASCII).

Les classes

La déclaration d'une classe peut prendre une des formes suivantes :

// Déclararation d'une classe simple
ModificateurDeClasse class NomDeClasse
{
  // Corps de NomDeClasse :
  // Déclaration des champs, des méthodes, des constructeurs
  // et/ou initialisations static
}
 
// Déclaration d'une classe dérivant d'une super classe
ModificateurDeClasse class NomDeClasseDerivee extends NomDeSuperClasse
{
  // Corps de NomDeClasseDerivee :
  // Déclaration des champs, des méthodes, des constructeurs
  // et/ou initialisations static
}
 
// Déclaration d'une classe implémentant une interface
ModificateurDeClasse class NomDeClasse2 implements NomInterface //, NomInterface2, ...
{
  // Corps de NomDeClasse2 :
  // Déclaration des champs, des méthodes, des constructeurs
  // et/ou initialisations static
  // et implémentation des méthodes de nomInterface 
}
 
// Déclaration d'une classe dérivant d'une super classe et implémentant une interface
ModificateurDeClasse class NomDeClasse3 extends NomDeSuperClasse 
                                 implements NomInterface //, NomInterface2, ...
{
  // Corps de NomDeClasse3 :
  // Déclaration des champs, des méthodes, des constructeurs
  // et/ou initialisations static
  // et implémentation des méthodes de nomInterface 
}

Le corps d'une classe est une suite quelconque de déclaration de champs, de méthodes, de constructeurs et/ou d'initialisations static.
A partir de Java 1.1, le corps d'une classe peut aussi déclarer des classes internes, des interfaces internes et des initialisations d'instance.
Une classe simple dérive implicitement de la classe Object (class nomDeClasse est équivalent à class nomDeClasse extends Object). Java ne permet pas l'héritage multiple (une seule classe peut suivre la clause extends), mais une classe peut implémenter plusieurs interfaces.

ModificateurDeClasse est optionnel et peut prendre une ou plusieurs des valeurs suivantes :

Symbolisation des contrôles d'accès aux classes
figure 4. Symbolisation des modificateurs d'accès aux classes

C++

Contrairement au C++, le point-virgule en fin de déclaration d'une classe est optionnel.

C++

En Java, on utilise la clause extends pour préciser la super classe d'une classe dérivée, à la place des deux points qui suivent la déclaration d'une classe en C++.

C++

L'héritage se fait systématiquement de manière public en Java. Il n'existe pas d'équivalence à la déclaration C++ : class Classe2 : private Classe1 { /* ... */ }.

C++

Une classe abstraite doit être déclarée abstract en Java et peut contenir aucune ou plusieurs méthodes abstract (équivalent des méthodes virtuelles pures du C++).
Les modificateurs d'accès de classe public et final n'ont pas d'équivalent en C++.

C++

En Java une classe et tout ce qu'elle contient devant être déclarée entièrement dans un seul fichier, il n'est pas possible comme en C++ de répartir les méthodes d'une même classe sur plusieurs fichiers.

Les interfaces

Une interface est une catégorie un peu spéciale de classe abstract, dont le corps ne contient que la déclaration de constantes et de méthodes abstract :

// Déclararation d'une interface simple
ModificateurInterface interface NomInterface
{
  // Corps de NomInterface :
  // Déclaration des constantes et des méthodes non implémentées
}
 
// Déclaration d'une interface dérivant d'une super interface
ModificateurInterface interface NomInterfaceDerivee extends NomSuperInterface 
                                                            //, NomSuperInterface2, ...
{
  // Corps de NomInterfaceDerivee :
  // Déclaration des constantes et des méthodes non implémentées
}

A partir de Java 1.1, le corps d'une interface peut aussi déclarer des classes internes et des interfaces internes.
II est impossible de créer une instance d'une interface. Une ou plusieurs interfaces peuvent suivre la clause extends.
Une classe non abstract, qui implémente une interface Interface1 doit implémenter le code de chacune des méthodes de Interface1. L'implémentation d'une méthode est l'ensemble des instructions que doit exécuter une méthode.
ModificateurInterface est optionnel et peut prendre une ou plusieurs des valeurs suivantes :

A quoi sert une interface et quelle est la différence avec une classe abstract ?
Tout d'abord, vous noterez qu'une classe ne peut hériter que d'une super classe, mais par contre peut implémenter plusieurs interfaces. Ensuite, une classe abstract peut déclarer des champs qui ne sont pas des constantes et implémenter certaines méthodes.

!

Une classe qui implémente une interface InterfaceDerivee dérivée d'une autre interface Interface1 doit implémenter les méthodes des deux interfaces InterfaceDerivee et Interface1.

C++

Java ne permet pas l'héritage multiple. Mais l'utilisation des interfaces peut compenser cet absence, car une classe peut implémenter plusieurs interfaces. Voir aussi le chapitre sur le portage.


Si vous avez quelque mal au départ à comprendre le concept d'interface et leur utilité, considérez les simplement comme des classes abstract, déclarant des constantes et des méthodes abstract. Vous en percevrez leur intérêt au fur et à mesure que vous serez amené à vous en servir.

Déclaration des champs

Le corps d'une classe est un ensemble de déclarations de champs (fields) et de méthodes implémentées ou non, déclarées dans n'importe quel ordre.

Syntaxe

Les déclarations de champs se font comme en C/C++ :

class Classe1
{
  // Déclaration de champs
  TypeChamp   champ1;
  TypeChamp   champ2, champ3;
  ModificateurDeChamp TypeChamp champ4;
  TypeChamp   champ5 = valeurOuExpression;  // Initialisation d'un champ
 
  // Création d'une référence sur un tableau
  TypeChamp   tableau1 [ ];   
  // Allocation d'un tableau de taille n initialisé avec les n valeurs
  TypeChamp   tableau2 [ ] = {valeur1, valeur2, /*..., */ valeurn};  
 
  // ...
}

TypeChamp est soit un type primitif, soit le nom d'une classe, soit le nom d'une interface. Dans les deux derniers cas, le champ est une référence sur un objet.
Les tableaux sont cités ici en exemple de champ et sont traités dans la partie sur les tableaux.

ModificateurDeChamp est optionnel et peut être un ou plusieurs des mots-clés suivants :

Symbolisation des contrôles d'accès aux champs
figure 5. Symbolisation des modificateurs d'accès aux champs

Le modificateur d'accès à un champ est soit public, soit protected, soit private, soit par défaut, si aucun de ces modificateurs n'est précisé, amical (friendly en anglais) : le champ est alors accessible uniquement par les autres classes du même package. Les liens de la figure précédente indique les endroits où il est possible d'utiliser un champ. Comme vous pouvez le voir, l'utilisation d'un champ se fait directement par son nom à l'intérieur d'une classe et de ses classes dérivées, sans besoin d'utiliser l'opérateur point (.).
L'initialisation d'un champ se comporte exactement comme l'affectation. Si le champ est static, alors l'initialisation est effectuée au chargement de la classe.
Les champs non initialisés prennent obligatoirement une valeur par défaut (voir le tableau sur les types primitifs). Si ce champ est une référence à un objet, la valeur par défaut est null.

C

Une classe Classe1 peut déclarer un champ qui est une référence à une classe Classe2 déclarée après Classe1 (pas besoin de déclarer les types avant de les utiliser comme en C).

C

En Java, le modificateur final est utilisé pour déclarer une constante (pas de #define, ni de const).

C

Contrairement au C/C++, Java permet d'initialiser à la déclaration les champs de classe ainsi que les champs d'instance.

C

Les champs d'instance et de classe sont tous initialisés à une valeur par défaut. En C++, il est obligatoire d'initialiser les champs de classe à part ; en Java, soit ces champs prennent une valeur par défaut, soit ils sont initialisés à leur déclaration, soit ils sont initialisés dans un bloc d'initialisation static.

C++

L'accès aux champs static se fait grâce à l'opérateur point (.) et pas l'opérateur ::, comme en C++. De plus, si vous voulez donner une valeur par défaut aux champs static, vous le faites directement à la déclaration du champ, comme par exemple static int var = 2;.

C++

Le modificateur d'accès aux champs (et aux méthodes) se fait pour chacune d'eux individuellement, et pas en bloc comme en C++ (avec par exemple public :).

C++

Les champs (et les méthodes) d'une classe Classe1 dont le modificateur d'accès est protected sont accessibles par les classes dérivées de Classe1 comme en C++, mais aussi par les classes du même package que Classe1.

C++

En Java, les champs (et les méthodes) d'une classe Classe1 ont un modificateur d'accès par défaut qui est amical (friendly), c'est à dire qu'elles sont accessibles uniquement par les autres classes du même package que Classe1. En C++, cette notion n'existe pas et par défaut le modificateur d'accès est private.

!

Le modificateur d'accès par défaut de Java est très pratique car il donne accès à tous les champs et toutes les méthodes des classes d'un même package. Mais attention, si après avoir développé certaines classes, vous pensez qu'elles peuvent vous être utiles pour d'autres programmes et qu'ainsi vous les mettez dans un nouveau package outils, il vous faudra ajouter les modificateurs d'accès public pour accéder en dehors du package outils aux méthodes et champs dont vous avez besoin.
Donc, prenez l'habitude de préciser dès le départ les modificateurs d'accès des champs et des méthodes.

Initialisations static

Le corps d'une classe peut comporter un ou plusieurs blocs d'initialisation static. Ces blocs sont exécutés au chargement d'une classe, et permettent d'exécuter des opérations sur les champs static. Ils sont exécutés dans l'ordre de déclaration et peuvent ne manipuler que les champs static déclarés avant le bloc.

class Classe1
{
  // Déclaration de champs static
  static int  champ1 = 10;
  static int  champ2;
 
  static
  {  // Bloc static
    champ2 = champ1 * 2;
  }
 
  // ...
}

!

Sur une même Machine Virtuelle Java, vous pouvez très bien exécuter différentes applets ou applications l'une après l'autre ou en même temps grâce au multi-threading. Par contre, chaque classe ClasseX n'existe qu'en un seul exemplaire pour une Machine Virtuelle, même si ClasseX est utilisée par différentes applets.
Ceci implique que les champs static de ces classes sont uniques pour une Machine Virtuelle, et partagés entre les différentes applets. Donc, attention aux effets de bord ! Si vous modifiez la valeur d'un champ static dans une applet, il sera modifié pour toutes les applets amenées à utiliser ce champ.
Ceci est à opposer au C, où les champs static sont uniques pour chaque contexte d'exécution d'un programme.

Initialisations d'instance

A partir de Java 1.1, le corps d'une classe peut comporter un ou plusieurs blocs d'initialisation d'instance. Comme pour les constructeurs, ces blocs sont exécutés à la création d'un nouvel objet dans l'ordre de déclaration.

class Classe1
{
  // Déclaration d'un champ comptant le nombre d'instances créées
  static int  nombreInstances = 0;
  // Déclaration d'un champ d'instance
  int  champ1;
 
  {  // Bloc d'instance
    champ1 = 10;
    nombreInstances++;
  }
 
  // ...
}

Déclaration des méthodes

Syntaxe

Les déclarations des méthodes en Java ont une syntaxe très proche de celles du C/C++ :

class Classe1
{
  // Déclarations de méthodes
  ModificateurDeMethode TypeRetour methode1 (TypeParam1 param1Name 
                                             /*,... , TypeParamN paramNName*/)
  { 
    // Corps de methode1 ()
  }

  // Déclaration d'une méthode sans paramètre 
  TypeRetour methode2 ()
  { 
    // Corps de methode2 ()
  }
 
  ModificateurDeMethode TypeRetour methode3 (/* ... */) throws TypeThrowable
                                                            /*, TypeThrowable2 */
  { 
    // Corps de methode3 ()
  }
 
  // Déclaration d'une méthode abstract
  // Dans ce cas, Classe1 doit être aussi abstract
  abstract ModificateurDeMethode TypeRetour methode4 (/* ... */);
 
  // Déclaration d'une méthode native
  native ModificateurDeMethode TypeRetour methode4 (/* ... */);
 
  // ...
}

TypeRetour peut être :

Les paramètres d'une méthode sont cités entre parenthèses et séparés par des virgules. Chaque paramètre est précédé par son typeTypeParami qui peut être un type primitif, une classe ou une interface.

TypeThrowable doit être une classe dérivée de la classe Throwable. Les exceptions qui sont déclenchées via l'instruction throw doivent avoir leur classe déclarée après la clause throws. Voir le chapitre traitant des exceptions.

ModificateurDeMethode est optionnel et peut être un ou plusieurs des mots-clés suivants :

Le modificateur d'accès à une méthode est soit public, soit protected, soit private. Leur utilisation est la même que pour les champs.
Dans la plupart des cas, il est conseillé de ne rendre public que les méthodes et les constantes (champs final static), dont a besoin l'utilisateur d'une classe. Les autres champs sont déclarés private voir friendly ou protected et sont rendus accessibles si besoin est, par des méthodes public permettant de les interroger et de les modifier (get... () et set... ()). Ceci permet de cacher aux utilisateurs d'une classe ses champs et de vérifier les conditions requises pour modifier la valeur d'un champ.
Ce style de programmation est largement utilisé dans la bibliothèque Java.

Pour les méthodes non abstract et non native, le corps de la méthode est un bloc, comportant une suite d'instructions définissant son implémentation.
A l'intérieur de la déclaration d'une classe Classe1, l'appel à toute méthode methode () de Classe1 ou de ses super classes, peut se faire directement sans l'opérateur point (.) ; l'utilisation de cet opérateur n'est obligatoire que pour accéder aux méthodes des autres classes, comme dans l'exemple suivant :

class Classe1
{
  static public int Factorielle (int i)
  {
    if (i == 0)
      return 1;
    else
      return i * Factorielle (i - 1);
      // Factorielle () est directement accessible à l'intérieur de Classe1
  }
}
 
class Classe2
{
  // Pour accéder à la méthode Factorielle () de Classe1
  // vous devez utilisez l'opérateur point.
  int factorielle10 = Classe1.Factorielle (10);
}

C++

Avant Java 5.0, Java ne permettait pas l'utilisation des listes d'arguments variables du C (défini avec ...). Cette absence peut être partiellement détournée grâce à l'utilisation de la surcharge des méthodes.

C++

En Java, chaque paramètre doit être déclarer avec son type et un nom. En C++, quand un paramètre est requis mais n'est pas utilisé dans une méthode, il n'est pas obligatoire de spécifier un nom, comme pour le deuxième paramètre de l'exemple void methode1 (int a, float).

C++

A l'opposé du C++, il est possible de donner le même nom à un champ et à une méthode (à éviter pour ne pas nuire à la lisibilité du programme).

C++

Contrairement au C/C++, dans une classe Classe1, vous pouvez utiliser tous les champs et les méthodes de Classe1 dans le corps de ses méthodes qu'ils soient déclarés avant ou après dans Classe1, comme dans l'exemple suivant :

class Classe1
{
  void methode1 ()
  {
    x = 1;       // x est déclaré après
    methode2 (); // methode2 () est déclarée après
  }
 
  void methode2 ()
  {
  }
 
  int x;
}

C++

Java ne permet pas de déclarer de variables ou de fonctions globales. Mais, si vous tenez absolument à garder le style de programmation procédurale du C, vous pouvez créer et utiliser des champs et des méthodes static, dans ce but.

C++

La notion de fonction "amie" du C++ (friend) n'existe pas en Java : aucune méthode ne peut être déclarée en dehors d'une classe.

C++

Contrairement au C++, toutes les méthodes d'instance non private sont virtuelles en Java. Donc le mot-clé virtual est inutile, ce qui peut éviter certains bugs difficiles à déceler en C++.

C++

Les méthodes abstract sont l'équivalent des méthodes virtuelles pures du C++ (qui se déclarent en faisant suivre la déclaration de la méthode de = 0).

C++

Java introduit le mot-clé final. Ce modificateur empêche d'outrepasser une méthode dans les classes dérivées. Cette notion est absente du C++.

C++

Toutes les méthodes Java sont déclarées et implémentées à l'intérieur de la classe dont elles dépendent. Mais, cela n'a pas pour conséquence de créer comme en C++ toutes les méthodes de Java inline !
Avec l'option d'optimisation (-O), le compilateur lui-même évalue les méthodes final qui peuvent être traitées inline (remplacement de l'appel à la méthode par le code implémentant la méthode) : Donc, il est important d'utiliser ce modificateur quand cela est nécessaire (pour les méthodes d'accès aux champs par exemple).

C++

La surcharge des opérateurs n'existe pas en Java. Seule la classe String autorise l'opérateur + pour la concaténation.

C++

Java ne permet pas de donner aux paramètres des valeurs par défaut comme en C++ (void f (int x, int y = 0, int z = 0); ). Vous êtes obligés de surcharger une méthode pour obtenir les mêmes effets, en créant une méthode avec moins de paramètres qui rappellera la méthode avec les valeurs par défaut.

C++

L'appel aux méthodes static se fait grâce à l'opérateur point (.) et pas l'opérateur ::, comme en C++.

 

!

Une méthode reçoit la valeur de chacun des paramètres qui lui sont passés, et ces paramètres se comportent comme des variables locales :

  1. Si un paramètre param est une référence sur un objet objet1, alors vous pouvez modifier le contenu de objet1 ; par contre, si vous affectez à param une référence sur un autre objet, cette modification n'aura d'effet qu'à l'intérieur du corps de la méthode. Si vous voulez mimer le passage par valeur, vous pouvez utiliser la méthode clone () de la classe Object pour créer une copie de l'objet objet1.
  2. Si un paramètre est de type primitif, la modification de sa valeur n'a de portée qu'à l'intérieur du corps de la méthode. Si vous voulez prendre en compte en dehors de la méthode la modification du paramètre, vous serez obligé de créer un objet dont la classe comporte un champ mémorisant cette valeur. Vous pourrez alors modifier la valeur comme indiqué en 1.
    (Voir aussi le chapitre traitant du portage de programmes C/C++ en Java).

Surcharge des méthodes

Une méthode methodeSurchargee (), est surchargée (overloaded) quand elle est déclarée plusieurs fois dans une même classe ou ses classes dérivées, avec le même nom mais des paramètres de types différents, ou de même type mais dans un ordre différent, comme dans l'exemple suivant :

class Classe1
{
  void methodeSurchargee (int entier)
  {
    // Corps de methodeSurchargee ()
  }
  void methodeSurchargee (float nombre)
  {
    // Corps de methodeSurchargee ()
  }
}
 
class Classe2 extends Classe1
{
  // Classe2 hérite de Classe1 donc elle déclare
  // implicitement toutes les méthodes de Classe1
 
  void methodeSurchargee (float nombre, short param)
  {
    // Corps de methodeSurchargee ()
  }
} 

!

Il est autorisé de surcharger une méthode en utilisant des paramètres de types différents pour chaque méthode. Les valeurs de retours peuvent être aussi différentes, mais il est interdit de créer deux méthodes avec les mêmes paramètres et un type de valeur de retour différent (par exemple, int methode () et float methode ()).

C++

En Java, une classe hérite de toutes les méthodes de la super classe dont elle dérive, même si elle surcharge une ou plusieurs des méthodes de sa super classe. Dans l'exemple précédent, contrairement au C++, les méthodes que l'on peut invoquer sur un objet de classe Classe2 sont les 3 méthodes methodeSurchargee (int entier), methodeSurchargee (float nombre) et methodeSurchargee (float nombre, short param). En C++, le fait de surcharger la méthode methodeSurchargee () dans Classe2, interdit d'appeler directement sur un objet de classe Classe2 les méthodes surchargées de Classe1.

Constructeur

Chaque champ d'une classe peut être initialisé à une valeur par défaut à sa déclaration. Mais si vous voulez initialiser certains de ces champs avec une valeur donnée à la création d'un nouvel objet, il vous faut déclarer une méthode spéciale appelée un constructeur.
Un constructeur est appelée automatiquement à la création d'un objet, et les instructions du corps d'un constructeur sont généralement destinées à initialiser les champs de l'objet nouvellement créé avec les valeurs récupérées en paramètre. Il a une syntaxe un peu différente de celle des méthodes :

class Classe1
{
  // Déclaration du constructeur sans paramètre remplaçant le constructeur par défaut
  public Classe1 ()
  { 
    // Corps du constructeur
  }
 
  ModificateurDeConstruceur Classe1 (TypeParam1 param1Name /* ... */)
  { 
    // Corps du constructeur
  }
 
  ModificateurDeConstruceur Classe1 (/* ... */) throws TypeThrowable 
                                                   /*, TypeThrowable2 */
  { 
    // Corps du constructeur
  }
}

Un constructeur porte le même nom que la classe où il est déclaré, et n'a pas de type de retour. A l'usage vous verrez que c'est une des méthodes qui est le plus souvent surchargée.
Toute classe qui ne déclare pas de constructeur a un constructeur par défaut sans paramètre qui ne fait rien. Ce contructeur a le même modificateur d'accès que sa classe (contructeur par défaut public si la classe est public).
Aussitôt qu'un constructeur est déclaré avec ou sans paramètre, le constructeur par défaut n'existe plus. Si vous avez déclaré dans une classe Classe1 un constructeur avec un ou plusieurs paramètres, ceci oblige à préciser les valeurs de ces paramètres à la création d'un objet de la classe Classe1.

TypeThrowable est une classe dérivée de la classe Throwable. Les exceptions qui sont déclenchées via l'instruction throw doivent avoir leur classe déclarée après la clause throws. Voir le chapitre traitant des exceptions.

ModificateurDeConstruceur est optionnel et peut être un des mots-clés suivants : public, protected ou private. Ils sont utilisés de la même manière que pour les déclarations de méthodes.

Voici par exemple, une classe Classe1 n'utilisant pas de constructeur transformée pour qu'elle utilise un constructeur :

class Classe1
{
  int var1 = 10;
  int var2;
}
class Classe1
{
  int var1;
  int var2;
 
  public Classe1 (int valeur)
  {
    // Initialisation de var1 avec valeur
    var1 = valeur;
  }
}

Le constructeur Classe1 (int valeur) sera appelé avec la valeur donnée par l'instruction de création d'un objet de classe Classe1. Ce constructeur permet ainsi de créer un objet de classe Classe1 dont le champ var1 est initialisé avec une valeur différente pour chaque nouvel objet.
Mais quel est l'intérêt d'un constructeur puisqu'il est toujours possible de modifier la valeur d'un champ d'un objet après sa création ?
Un constructeur permet d'initialiser certains champs d'un objet dès sa création, ce qui permet de garantir la cohérence d'un objet. En effet, même si vous précisez aux utilisateurs d'une classe qu'ils doivent modifier tel ou tel champ d'un nouvel objet avant d'effectuer certaines opérations sur celui-ci, rien ne garantit qu'ils le feront effectivement, ce qui peut être source de bugs.
Un constructeur permet justement d'éviter ce genre de problème car tous les champs d'un objet seront correctement initialisés au moment de sa création, ce qui garantit que les utilisateurs d'une classe pourront effectuer n'importe quelle opération sur un objet juste après sa création.
La plupart des classes de la bibliothèque Java utilisant un ou plusieurs constructeurs, vous serez souvent amener à les utiliser en créant des objets et ceci vous permettra de comprendre comment en déclarer vous-même dans vos propres classes.

 

!

Les méthodes peuvent elles aussi porter le même nom que la classe où elles sont déclarées. Comme la distinction ne se fait alors que par la présence d'un type devant le nom de la méthode, l'utilisation de cette caractéristique est déconseillée pour éviter toute confusion avec un constructeur.

class Classe1
{
  // Ceci est une méthode pas un constructeur !
  public void Classe1 (int valeur)
  {
    // ...
  }
}

Le corps d'un constructeur peut éventuellement commencé par une des deux instructions suivantes :

this (argument1 /*, argument2, ...*/); 
super (argument1 /*, argument2, ...*/);

La première instruction permet d'invoquer un autre constructeur de la même classe : il est souvent utilisé par un constructeur pour passer des valeurs pas défaut aux paramètres d'un autre constructeur.
La seconde instruction permet d'appeler un constructeur de la super classe pour lui repasser des valeurs nécessaires à l'initialisation de la partie de l'objet dépendant de la super classe, comme dans l'exemple suivant :

class Classe1
{
  int var;
  Classe1 (int val)
  {
    var = val;
  }
}
 
class Classe2 extends Classe1
{
  int var2;
  Classe2 (int val)
  {
    // Appel du constructeur de Classe1 avec la valeur 3
    super (3);
    var2 = val;
  }
 
  Classe2 ()
  {
    // Appel du premier constructeur avec la valeur 2
    this (2);
  }
}

Si aucune des instructions précédentes n'est citée, Java considère qu'il y a implicitement un appel super ();. Ceci implique que la super classe doit avoir un constructeur sans paramètre (déclaré explicitement ou fourni par Java par défaut), et que par enchaînement, la création de tout nouvel objet de classe invoquera le constructeur de sa classe et tous les constructeurs de des super classes.
Donc si comme dans l'exemple précédent vous créez une classe Classe2 qui dérive d'une classe Classe1 n'ayant pas de constructeur par défaut ou sans paramètre, vous serez obliger de déclarer au moins un constructeur dans Classe2 qui rappelle un constructeur de la classe Classe1 avec l'instruction super (...);.

A partir de Java 1.1, le corps d'une classe peut comporter aussi un ou plusieurs blocs d'initialisation d'instance, qui sont comparables au constructeur par défaut. A la création d'un objet, un objet d'une classe Classe1 est initialisé dans l'ordre suivant :

C++

Java introduit le mot-clé super : il permet de passer des valeurs d'un constructeur d'une classe au constructeur de sa super classe grâce à l'appel super (arguments). En C++, il faut donner les paramètres à passer au(x) constructeur(s) des supers classes à la suite de la déclaration d'un constructeur.

C++

Contrairement au C++, les constructeurs d'une même classe peuvent s'appeler entre eux en Java grâce à this (...). Cette fonctionnalité est très pratique pour remplacer l'absence de valeur par défaut des paramètres des constructeurs.

C++

Java ne permet de passer les objets en paramètre que par référence. Le constructeur par recopie du C++, appelé pour construire les objets passés par valeur, n'est pas utile.

Création d'objets : opérateur new

La création d'objet (on dit aussi l'instanciation d'une classe) se fait grâce à l'opérateur new suivi d'un nom de classe et des arguments envoyés au constructeur :

new Classe1 (/* argument1, argument2, ...*/)

Un nouvel objet de classe Classe1 est créé, l'espace mémoire nécessaire pour les champs d'instance est alloué, ces champs sont ensuite initialisés puis finalement le constructeur correspondant aux types des arguments est appelé.
La valeur renvoyée par l'opérateur peut être affectée à une référence de classe Classe1 ou de super classe de Classe1 (voir les casts).
Si Classe1 n'a pas encore été utilisée, l'interpréteur charge la classe, alloue l'espace mémoire nécessaire pour mémoriser les champs static de la classe et exécutent les initialisations static.

A partir de Java 1.1, il est possible de créer des objets ayant une classe anonyme.

La méthode newInstance () de la classe Class permet aussi de créer un nouvel objet. Cette méthode est équivalente à utiliser new sans argument, et donc si vous voulez utiliser newInstance () pour créer un objet de classe Classe1, Classe1 doit avoir un constructeur sans paramètre (celui fourni par défaut ou déclaré dans la classe), sinon une exception NoSuchMethodError est déclenchée. Voir aussi l'application InstantiationAvecNom.
A partir de Java 1.1, les méthodes newInstance () des classes java.lang.reflect.Constructor et java.lang.reflect.Array permettent de créer un objet de n'importe quelle classe ayant un constructeur avec ou sans paramètres.

Une exception OutOfMemoryError peut être déclenchée en cas de mémoire insuffisante pour allouer l'espace mémoire nécessaire au nouvel objet.

C

La seule manière de créer des objets en Java se fait grâce à l'opérateur new. Vous ne pouvez pas comme en C++, créer des objets sur la pile d'exécution.

C

Maintenant que vous connaissez comment créer une classe, un constructeur et un objet en Java, comparons un programme simple C avec un programme Java :

#include <stdlib.h>
/* Déclaration du type Classe1 */
typedef struct
{
  int var1;
  int var2;
}
  Classe1;
 
 
 
 
 
 
 
 
 
/* Fonction renvoyant la division */
/* entre les champs de objet      */
int division (Classe1 *objet)
{
  return objet->var1 / objet->var2;
}
 
void main ()
 
{
  /* Allocation puis initialisation */
  /* d'une instance de Classe1      */
  Classe1 *objet1 = (Classe1 *)
            calloc (1, sizeof (Classe1));
  objet1->var1 = 10;
  objet1->var2 = 20;
  int quotient = division (objet1);
}
 
 
// Déclaration de la classe Classe1
class Classe1
{
  int var1;
  int var2;
 
  // Constructeur de Classe1
  // permettant d'initialiser
  // les champs var1 et var2
  public Classe1 (int valeur1,
                   int valeur2)
  {
    var1 = valeur1;
    var2 = valeur2;
  }
 
  // Méthode renvoyant la division 
  // entre les champs de cet objet 
  public int division ()
  {
    return var1 / var2;
  }
 
  public static void main
                      (String [] args)
  {
    // Création d'une instance de
    // Classe1 directement initialisée
    Classe1 objet1
              = new Classe1 (10, 20);
 
 
    int quotient = objet1.division ();
  }
}

Outrepasser une méthode

Une méthode d'instance methodeOutrepassee () non private, est outrepassée (overridden) si elle est déclarée dans une super classe et une classe dérivée, avec le même nom, le même nombre de paramètres, et le même type pour chacun des paramètres.
Les exemples qui suivent montrent l'intérêt de ce concept :

L'application suivante décrit une gestion de comptes en banque simplifiée et correspond au graphe d'héritage décrit au chapitre précédent (sans la classe PEL). Recopiez la dans un fichier Banque.java, que vous compilez avec l'instruction javac Banque.java pour ensuite l'exécuter avec java ou Java Runner, grâce à l'instruction java Banque :

class Compte
{
  private   int   numero;
  protected float soldeInitial; // Champ protected accessible
                                // par les classes dérivées
 
  // Constructeur
  Compte (int nouveauNumero, float sommeDeposee)
  {
    // Mise à jour des champs de la classe
    numero        = nouveauNumero;
    soldeInitial  = sommeDeposee;
  }
 
  int numeroCompte ()
  {
    return numero;
  }
 
  float calculerSolde ()
  {
    return soldeInitial;
  }
}
 
// La classe CompteDepot dérive de la classe Compte
class CompteDepot extends Compte
{
  // Création d'un tableau de 1000 float pour les opérations
  private float operations [] = new float [1000];
  private int   nbreOperations; // Initialisée à 0
 
  // Constructeur
  CompteDepot (int nouveauNumero)
  {
    // Appel du constructeur de la super classe
    super (nouveauNumero, 0);
  }
 
  void ajouterOperation (float debitCredit)
  {
    // Mémorisation de l'opération et augmentation de nbreOperation
    operations [nbreOperations++] = debitCredit;
  }
 
  float calculerSolde () // outrepasse la méthode de la classe Compte
  {
    float solde = soldeInitial;
    // Somme de toutes les opérations
    for (int i = 0; i < nbreOperations; i++)
      solde += operations [i];
    return solde;
  }
}
 
// La classe CompteEpargne dérive de la classe Compte
class CompteEpargne extends Compte
{
  private float tauxInteretPourcentage;
  
  // Constructeur (tauxInteret en %)
  CompteEpargne (int nouveauNumero, float depot, float tauxInteret)
  {
    super (nouveauNumero, depot);
    tauxInteretPourcentage = tauxInteret;
  }
 
  float calculerSolde () // outrepasse la méthode de la classe Compte
  {
    return soldeInitial * (1f + tauxInteretPourcentage / 100f);
  }
}
 
// Classe d'exemple
public class Banque
{
  // Méthode lancée à l'appel de l'instruction java Banque
  public static void main (String [ ] args)
  {
    // Création de 3 comptes de classe différente
    Compte compte101 = new Compte (101, 201.1f);
 
    CompteDepot compte105 = new CompteDepot (105);
    compte105.ajouterOperation (200);
    compte105.ajouterOperation (-50.5f);
 
    CompteEpargne compte1003 = new CompteEpargne (1003, 500, 5.2f);
    
    // Appel de la méthode editerSoldeCompte () sur chacun
    // des comptes. Cette méthode prend en paramètre une
    // référence de classe Compte : Les objets désignés par
    // compte105 et compte1003 sont d'une classe qui dérive
    // de la classe Compte, c'est pourquoi ils peuvent être
    // acceptés en paramètre comme des objets de classe Compte
    editerSoldeCompte (compte101);
    editerSoldeCompte (compte105);
    editerSoldeCompte (compte1003);
  } 
 
  // Méthode éditant le numéro et le solde d'un compte
  static void editerSoldeCompte (Compte compte)
  {
    // Récupération du numéro et du solde du compte
    // La méthode calculerSolde () qui est appelée
    // est celle de la classe de l'objet désigné
    // par le champ compte
    int   numero = compte.numeroCompte ();
    float solde  = compte.calculerSolde ();
    System.out.println (  "Compte : " + numero
                        + " Solde = " + solde);
  }
}

Le résultat de ce programme donne ceci :

Compte : 101 Solde = 201.1
Compte : 105 Solde = 149.5
Compte : 1003 Solde = 526.0

La méthode editerSoldeCompte () reçoit en paramètre la variable compte. compte est une référence désignant une instance de la classe Compte, ou d'une des classes dérivées de Compte, ici CompteDepot ou CompteEpargne.
Que se passe-t-il à l'appel de la méthode calculerSolde () sur cette référence ?
La Machine Virtuelle connait à l'exécution la classe de l'objet désigné par la référence compte : en plus, des champs d'instance qui sont allouées à la création d'une nouvelle instance des classes Compte, CompteDepot ou CompteEpargne, un champ caché qui représente la classe du nouvel objet lui est ajouté. A l'appel compte.calculerSolde (), la Machine Virtuelle consulte ce champ pour connaître la classe de l'objet désigné par compte. Une fois qu'elle a cette classe elle appelle l'implémentation de la méthode calculerSolde () de cette classe, ce qui fait que la méthode calculerSolde () de la classe Compte ne sera effectivement appelée que si l'objet désigné par compte est de classe Compte.

Globalement en Java, la manière d'appeler une méthode d'instance quelle qu'elle soit, respecte ce schéma : c'est la ligature dynamique (la bonne méthode à appeler n'est pas déterminée statiquement à la compilation, mais dynamiquement à l'exécution en fonction de la classe de l'objet désigné). A première vue, son intérêt paraît pourtant limité aux méthodes outrepassées, mais souvenez-vous que toute classe qui n'est pas final peut être appelée à être dérivée un jour, et que ses méthodes seront peut-être outrepassées dans la classe dérivée. Il faut donc "préparer le terrain" pour ces méthodes...

Une méthode outrepassant une autre ne peut avoir un modificateur d'accès plus restrictif que la méthode outrepassée (pas possible d'avoir un accès protected ou private si la méthode outrepassée est public).
L'ordre de priorité des modificateurs d'accès est du plus restrictif au moins restrictif : private, friendly, protected et public.

La notion de méthode outrepassée est fondamentale et contribue pour une grande part à la puissance d'un langage objet. Elle est très souvent utilisée en Java, car il vous faut souvent outrepasser les méthodes des classes de la bibliothèque Java pour modifier leur comportement par défaut. On parle souvent aussi de polymorphisme.

C++

En Java, toutes les méthodes d'instance utilisent la ligature dynamique. Contrairement au C++, toutes les méthodes sont virtual.

!

Faites très attention à bien respecter l'orthographe du nom des méthodes que vous outrepassez et les types de leurs paramètres. Si la nouvelle méthode créée a un nom différent, elle n'outrepassera plus celle de la super classe, le compilateur ne vous signalera rien et finalement la méthode appelée pourra être celle de la super classe au lieu de celle que vous avez déclarée.
Par contre, les noms des paramètres n'ont pas d'importance, et peuvent être différents de ceux de la méthode outrepassée.
Quand une méthode private est déclarée à nouveau dans une sous-classe, elle n'est pas spécialisée.

!

Contrairement à ce qui se passe avec les méthodes outrepassées, il n'existe pas en Java de ligature dynamique sur les champs qu'ils soient static ou non, comme le montre l'exemple suivant :

class Classe1
{
  final static int CONSTANTE = 0;
}
 
class Classe2 extends Classe1
{
  // Déclaration d'un champ (constante) qui cache celle de Classe1
  final static int CONSTANTE = 1;
 
  void methode1 ()
  {
    Classe2 objet2 = new Classe2 (); // création d'un objet de classe Classe2
    int var = objet2.CONSTANTE;      // var vaut 1
    Classe1 objet1 = objet2;         // cast de Classe2 vers Classe1
    var = objet1.CONSTANTE;          // var vaut 0 pourtant objet1 est une référence
                                     // sur un objet de classe Classe2 !
  }
}

Dans cet exemple, Classe2 a besoin de donner une valeur différente à CONSTANTE pour les objets de cette classe : si on redéclare cette constante dans Classe2 avec une valeur différente, on pourrait s'attendre à ce que objet1.CONSTANTE vaille 1 puisque objet1 désigne un objet de classe Classe2. En fait, objet1.CONSTANTE renvoie la valeur de CONSTANTE de la classe Classe1 car objet1 est de type Classe1...
Si vous voulez que var vaille 1 dans les deux cas de cet exemple, vous devez créer une méthode dans chaque classe qui renvoie la valeur de CONSTANTE, comme par exemple :

class Classe1
{
  public int valeur1 ()
  {
    return 0;
  }
}
 
class Classe2 extends Classe1
{
  public int valeur1 ()  // valeur1 () outrepasse la méthode de Classe1
  {
    return 1;
  }
 
  void methode1 ()
  {
    Classe2 objet2 = new Classe2 (); // création d'un objet de classe Classe2
    int var = objet2.valeur1 ();     // var vaut 1
    Classe1 objet1 = objet2;         // cast de Classe2 vers Classe1
    var = objet1.valeur1 ();         // var vaut 1 car c'est la methode
                                     // valeur1 () qui est appelée (objet1
                                     // est une référence sur un objet de
                                     // classe Classe2)
  }
}

Utilisation de classes abstract

Les classes abstract sont une catégorie spéciale de classe : il est impossible de créer une instance d'une classe abstract classeAbstract, mais par contre il est possible de déclarer un champ var1 qui est une référence de classe classeAbstract. var1 peut être égal à null ou désigner un objet d'une des classes dérivées de la classe classeAbstract à la condition suivante : Si la classe classeAbstract déclare une ou plusieurs méthodes d'instance abstract, la classe dérivée doit outrepasser ces méthodes et donner leur implémentation. Si cette classe ne donne pas l'implémentation de toutes les méthodes abstract, elle est elle-même abstract.
Rappelez-vous qu'en fait, pour créer une instance d'une classe Classe1, il faut que toutes les méthodes de cette classe soient implémentées pour pouvoir les appeler, que ces méthodes soient déclarées dans Classe1 ou héritées des super classes de Classe1. Si une super classe déclare des méthodes abstract il faut donc que ces méthodes soient implémentées.
De même, une interface est une sorte de classe abstract dont toutes les méthodes sont implicitement abstract. C'est pourquoi toute classe qui implémente une interface, doit implémenter toutes les méthodes de l'interface pour ne pas être abstract.
L'exemple suivant vous montre l'intérêt de l'utilisation d'une classe abstract :

abstract class Vehicule
{
  abstract int nombreDeRoues ();
}
 
class Velo extends Vehicule
{
  int nombreDeRoues () // outrepasse nombreDeRoues () de la classe Vehicule
  {
    return 2;
  }
}
 
class Voiture extends Vehicule
{
  int nombreDeRoues () // outrepasse nombreDeRoues () de la classe Vehicule
  {
    return 4;
  }
}
 
class VoitureAvecRemorque extends Voiture
{
  int nombreDeRoues () // outrepasse nombreDeRoues () de la classe Voiture
  {
    // super.nombreDeRoues () fait appel à la méthode outrepassée
    return 2 + super.nombreDeRoues ();
  }
}
 
class Classe1
{
  // Création de deux objets avec l'opérateur new
  Velo     unVelo     = new Velo ();
  Voiture  uneVoiture = new Voiture ();
  // Déclaration d'une référence sur la super classe Vehicule
  // Comme la classe Vehicule est abstract, il est impossible de créer un
  // objet de cette classe, mais on peut affecter à cette référence
  // de classe Vehicule, un objet d'une classe dérivée de Vehicule
  Vehicule unVehicule;
 
  void methode ()
  {
     int a = unVelo.nombreDeRoues ();      // a est égal à 2
     int b = uneVoiture.nombreDeRoues ();  // b est égal à 4
 
     unVehicule = unVelo;                   // cast de Velo vers Vehicule
     int c = unVehicule.nombreDeRoues ();  // c est égal à 2
     unVehicule = uneVoiture;               // cast de Voiture vers Vehicule
     int d = unVehicule.nombreDeRoues ();  // d est égal à 4
  }
}

Dans cet exemple, unVehicule est une référence permettant de désigner un objet de classe Vehicule ou toute autre classe dérivée de Vehicule. Quand nombreDeRoues () est invoquée sur la référence unVehicule, l'interpréteur va consulter la classe réelle d'appartenance de l'objet référencé par unVehicule ; une fois, qu'il a déterminé cette classe, il va appeler la méthode nombreDeRoues () de cette classe.

C++

Le mot-clé super permet aussi d'invoquer la méthode methode1 () outrepassée d'une super classe Classe1, par super.methode1 (). Il correspond à la notation Classe1::methode1 () du C++.
Par contre, si Classe1 hérite elle-même d'une super classe Classe0, implémentant elle aussi methode1 (), vous ne pourrez pas appeler directement methode1 () de Classe0, comme vous le feriez en C++ grâce à Classe0::methode1 (). Mais ceci n'est pas souvent utilisé...

C

Java ne permet pas d'utiliser des pointeurs sur fonctions. Dans certains cas, l'utilisation des méthodes outrepassées est une alternative à cette absence. Voici un programme C et un programme Java mis en parallèle pour illustrer ce propos (l'exemple utilise une interface mais il est aussi possible d'utiliser une classe abstract) :

/* Déclaration d'un type pointeur sur   */
/* fonction prenant en paramètre un int */
typedef void (* methodeX) (int i);
 
 
 
 
/* Déclaration de deux fonctions du */
/* même type que methodeX ()        */
 
 
 void methodeX_1 (int i)
{  /* Corps de methodeX_1 () */ }
 
 
 
 
void methodeX_2 (int i)
{  /* Corps de methodeX_2 () */ }
 
 
 
 
 
 
/* Méthode appelant la méthode */
/* de type methodeX            */
void appelMethodeX
          (methodeX methode, int i)
{
  methode (i);
}
 
void appelMethodeXClasse1 ()
{
  /* Appel de methodeX_1 () */
  appelMethodeX (methodeX_1, 5);
}
 
 
 
 
 
void appelMethodeXClasse2 ()
{
  /* Appel de methodeX_2 () */
  appelMethodeX (methodeX_2, 10);
}
// Déclaration d'une interface déclarant
// une méthode prenant en paramètre un int
interface MethodeX
{
  void methodeX (int i);
}
 
// Déclaration de deux classes implémentant
// la méthode methodeX () de cette interface
class Classe1 implements MethodeX
{
  public void methodeX (int i)
  {  /* Corps de methodeX () */ }
}
 
class Classe2 implements MethodeX
{
  public void methodeX (int i)
  {  /* Corps de methodeX () */ }
}
 
// Déclaration d'une classe utilisant
// methodeX () de différentes classes
class ClasseUtilisantMethodeX
{
  // Méthode appelant la méthode methodeX ()
  // d'une classe implémentant MethodeX
  void appelMethodeX
           (MethodeX objet, int i)
  {
    objet.methodeX (i);
  }
 
  void appelMethodeXClasse1 ()
  {
    // Appel de methodeX () de Classe1
    // La référence désignant l'objet créé
    // avec new Classe1 () peut être
    // casté en MethodeX car
    // Classe1 implémente MethodeX
    appelMethodeX (new Classe1 (), 5);
  }
 
  void appelMethodeXClasse2 ()
  {
    // Appel de methodeX () de Classe2
    appelMethodeX (new Classe2 (), 10);
  }
}

Destruction des objets

Java gère de manière automatique pour le programmeur l'allocation dynamique de mémoire. La mémoire nécessaire à la mémorisation de tout nouvel objet est allouée dynamiquement à sa création, et la mémoire qu'il occupe est automatiquement libérée quand celui-ci n'est plus référencé par aucune variable du programme. Cette libération est réalisée grâce au Garbage Collector (littéralement Ramasseur d'Ordure) fourni avec la Machine Virtuelle Java.
Cette fonctionnalité très pratique de Java simplifie énormément la programmation, d'autant plus qu'elle implique que la notion de destructeur (méthode appelée à la destruction d'un objet en C++, très souvent utilisée pour libérer la mémoire utilisée par un objet) est beaucoup moins utile en Java.
Toutefois, Java fournit une méthode à outrepasser dans vos classes si vous avez besoin d'effectuer certains traitements spécifiques à la destruction d'un objet : void finalize (). Cette méthode est invoquée juste avant que le Garbage Collector ne récupère la mémoire occupée par l'objet. Normalement, vous ne l'utiliserez que rarement mais elle peut être utile pour libérer certaines ressources dont Java ne géreraient pas directement la destruction (contextes graphiques, ressources ou mémoire alloués par des méthodes native écrites en C ou C++).
Vous pouvez éventuellement indiquer à la Machine Virtuelle qu'une référence var1 désignant un objet n'est plus utile en la mettant à null (var1 = null;), pour que le Garbage Collector puisse détruire l'objet désigné par var1 si celui-ci n'est plus désigné par aucune référence.

C++

En java, la destruction des objets se fait automatiquement quand ils ne sont plus utilisés (référencés). L'opérateur delete du C++ servant à détruire explicitement les objets créés dynamiquement est donc inutile.

C++

En java, il n'existe pas de syntaxe prédéfinie pour les destructeurs. Vous pouvez outrepasser la méthode finalize () pour "nettoyer" vos objets mais contrairement au destructeur du C++ où le destructeur est invoqué à l'appel de delete (), finalize () est invoquée automatiquement par le Garbage Collector quand un objet n'a plus aucune référence le désignant et qu'il peut donc être détruit. Le moment précis où va intervenir le Garbage Collector n'est pas prévisible, donc s'il vous faut effectuer des opérations obligatoires quand un objet devient inutile (effacement d'un dessin à l'écran par exemple), c'est à vous de créer une méthode que vous invoquerez au moment voulue (vous pouvez l'appeler delete () si vous voulez).

C

Comme en C++, les objets Java sont alloués dynamiquement à leur création via l'opérateur new. En Java, c'est le seul moyen d'allouer de la mémoire, donc il n'existe plus de fonctions telles que malloc (), realloc () ou free (). L'opérateur sizeof () servant surtout pour évaluer la taille d'un objet à allouer n'existe plus non plus. En C, sizeof () est utile aussi pour la portabilité d'un programme, car tous les types primitifs n'ont pas la même taille suivant les systèmes (int peut avoir une taille de 16 ou 32 bits, par exemple) : en Java, tous les types primitifs ont la même taille.

Comment ça marche ?

Pour mieux comprendre comment Java manipule les objets de leur création à leur destruction, voici une figure décrivant la vie d'un objet :

La vie d'un objet Java de sa création à sa destruction
figure 7. La vie d'un objet Java de sa création à sa destruction

Ce programme très simple vous montre la nuance très importante entre une référence et un objet : un même objet peut avoir n'importe quel nombre de références le désignant, mais une référence ne peut désigner qu'un seul objet à la fois.
A tout moment, la Machine Virtuelle connaît le nombre de références (ou de variables) qui désignent chacun des objets d'un programme : quand pour un objet, ce nombre devient nul, ceci signifie que plus aucune variable du programme ne désigne cet objet. S'il n'existe plus de variable désignant cet objet, le programme n'a donc plus de moyen de le manipuler, il est logique de le considérer comme inutile et de le supprimer.

C

Pour vous montrer toute la puissance et l'intérêt du Garbage Collector, voici un programme C et un programme Java mettant en oeuvre l'utilisation de listes chaînées :

#include <stdlib.h>
/* Déclaration d'un type de liste chaînée */
typedef struct _eltListe
{
  int               nombre;
  struct _eltListe *suivant;
}
  EltListe,
 *ListeChainee;
 
/* Crée une liste d'éléments ayant      */
/* leur nombre compris entre min et max */
ListeChainee creerListe (int min, int max)
{
  ListeChainee elt = (ListeChainee)
             malloc (sizeof (EltListe));
  elt->nombre  = min;
  if (min < max)
    elt->suivant =
           creerListe (min +1, max);
  else
    elt->suivant = NULL;
  return elt;
}
 
/* Enlève un élément individuel */
ListeChainee enleverElement
                (ListeChainee liste,
                 int          nombre)
{
  ListeChainee eltCherche;
  ListeChainee eltPrecedent = NULL;
 
  /* Recherche de l'élément contenant */
  /* le nombre                        */
  for (eltCherche = liste;
       eltCherche != NULL;
       eltCherche = eltCherche->suivant)
    if (eltCherche->nombre != nombre)
      eltPrecedent = eltCherche;
    else
    {
      /* Suppression de l'element */
      /* de la liste chaînée      */
      if (eltCherche == liste)
        liste = liste->suivant;
      else
        eltPrecedent->suivant =
            eltCherche->suivant;
      free (eltCherche);
    }
  return liste;
}
 
/* Libère la mémoire prise par tous */
/* les éléments de la liste         */
void viderListe (ListeChainee liste)
{
  while (liste != NULL)
  {
    ListeChainee eltPrecedent = liste;
    liste = liste->suivant;
    free (eltPrecedent);
  }
}
 
void main ()
 
{
  ListeChainee liste =
           creerListe (1, 10);
  liste = enleverElement (liste, 8);
  viderListe (liste);
}
 
 
// Déclaration d'une classe de liste chaînée
public class ListeChainee
{
  int          nombre;
  ListeChainee suivant;
 
 
 
 
  // Construit une liste d'éléments ayant
  // leur nombre compris entre min et max  
  ListeChainee (int min, int max)
  {
 
 
    nombre = min;
    if (min < max)
      suivant =
        new ListeChainee (min + 1, max);
    else
      suivant = null;
  }
 
 
  // Enlève un élément individuel
  ListeChainee enleverElement
                      (int nombre)
 
  {
    ListeChainee eltCherche;
    ListeChainee eltPrecedent = null;
 
    // Recherche de l'élément contenant
    // le nombre (this désigne la tête
    // de liste elle-même)
    for (eltCherche = this;
         eltCherche != null;
         eltCherche = eltCherche.suivant)
      if (eltCherche.nombre != nombre)
        eltPrecedent = eltCherche;
      else
        // Suppression de la référence sur
        // de l'element recherche, l'objet
        // peut donc être supprimé
        if (eltCherche == this)
          return this.suivant;
        else
          eltPrecedent.suivant =
              eltCherche.suivant;
 
    return this;
  }
 
// void viderListe ()
// est inutile
 
 
 
 
 
 
 
 
 
 
  public static void main
                   (String [ ] args)
  {
    ListeChainee liste =
              new ListeChainee (1, 10);
    liste = liste.enleverElement (8);
    liste = null;
  }
}

L'instruction liste = null entraîne que l'unique référence sur l'objet de tête de liste est perdue, donc le Garbage Collector peut supprimer cet objet. Quand il le supprime, le champ suivant de cet objet est supprimée et il n'existe plus de référence sur l'élément suivant. Ce dernier peut être supprimé à son tour, ainsi de suite jusqu'à qu'au dernier élément de la liste. Dans cet exemple, liste = null n'est même pas obligatoire car la variable locale liste est supprimée à la sortie de la méthode main (), ce qui provoque les mêmes effets.
Une fois compris l'exemple précédent, vous pouvez essayer de créer à partir de celui-ci une classe de liste doublement chaînée (avec liens suivant et precedent).

Si vous ne voulez pas croire en la magie, il vous faudra sûrement un certain temps pour faire confiance au Garbage Collector sans arrière pensée. Ce type de gestion de la mémoire étant très pratique, le réflexe de ne plus libérer la mémoire explicitement comme en C/C++ (avec free () ou delete) s'acquière d'office, mais vous prendrez plus de temps à comprendre comment vos objets sont détruits dans telle ou telle situation.
La meilleure piste pour répondre à vos interrogations, est de vous demander par combien de variables sont référencés le ou les objets sur lesquels vous avez des doutes. Si par enchaînement, ce nombre tombe à 0, vous pouvez oublier vos doutes.

C

Comme en Java, vous ne détruisez pas explicitement les objets, toute référence est égale soit à null soit elle désigne un objet TOUJOURS valide. Vous ne pouvez pas avoir de risque de manipuler un objet qui n'existe plus comme dans le programme C suivant :

void fonction1 ()
{
  char *chaine = malloc (20);
  strcpy (chaine, "bonjour\n");
  /* ... */
  free (chaine);
  /* ... */
  printf (chaine);
}

En C, si dans un programme vous utilisez par erreur un pointeur après avoir libéré l'espace mémoire qu'il désigne, le compilateur ne vous indiquera aucune erreur. De plus, ce genre d'erreur est souvent difficile à trouver.
En Java, si vous essayez d'accéder à un objet par une référence égale à null, une exception NullPointerException est déclenchée.


Page d'accueilFindIt !ContactLes notions de baseObjets, tableaux et chaînesDébut de la page
© Copyrights 1997-2023 Emmanuel PUYBARET / eTeks
- Tous droits réservés -
Table des matièresHiérarchie des classes