eTeks : Du C/C++ à JavaContactLes notions de baseObjets, tableaux et chaînes

Du C/C++ à JavaTM

Hiérarchie des classesTable des matières 


Création et utilisation des classes

Déclaration des classes et des interfaces
Déclaration des variables
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é struct, union et enum n'existent pas en Java. De plus, les template n'existent pas.

Identifiants

Les identifiants que vous créez en Java (classes, interfaces, variables, méthodes,...) 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. Il doit bien sûr être différent 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
ModifieurDeClasse class NomDeClasse
{
  // Corps de NomDeClasse :
  // Déclaration des variables, des méthodes, des constructeurs
  // et/ou initialisations static
}
 
// Déclaration d'une classe dérivant d'une super classe
ModifieurDeClasse class NomDeClasseDerivee extends NomDeSuperClasse
{
  // Corps de NomDeClasseDerivee :
  // Déclaration des variables, des méthodes, des constructeurs
  // et/ou initialisations static
}
 
// Déclaration d'une classe implémentant une interface
ModifieurDeClasse class NomDeClasse2 implements NomInterface //, NomInterface2, ...
{
  // Corps de NomDeClasse2 :
  // Déclaration des variables, 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
ModifieurDeClasse class NomDeClasse3 extends NomDeSuperClasse 
                                 implements NomInterface //, NomInterface2, ...
{
  // Corps de NomDeClasse3 :
  // Déclaration des variables, des méthodes, des constructeurs
  // et/ou initialisations static
  // et implémentation des méthodes de nomInterface 
}

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.
Le corps d'une classe est une suite quelconque de déclaration de variables, 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.

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

Symbolisation des contrôles d'accès aux classes
figure 4. Symbolisation des contrôles 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 modifieurs 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
ModifieurInterface 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
ModifieurInterface interface NomInterfaceDerivee extends NomSuperInterface
{
  // 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.
ModifieurInterface 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 variables et le code de 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 variables

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

Syntaxe

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

class Classe1
{
  // Déclaration de variables
  TypeVariable   variable1;
  TypeVariable   variable2, variable3;
  ModifieurDeVariable TypeVariable variable4;
  TypeVariable   variable5 = valeurOuExpression;  // Initialisation d'une variable
 
  TypeVariable  tableau1 [ ]; // Création d'une référence sur un tableau
                              // Allocation d'un tableau de taille n initialisé avec les n valeurs
  TypeVariable  tableau2 [ ] = {valeur1, valeur2, /*..., */ valeurn};  
 
  // ...
}

TypeVariable est soit un type de base, soit le nom d'une classe, soit le nom d'une interface. Dans ce dernier cas, la variable est une référence sur un objet.
Les tableaux sont cités ici en exemple de variable et sont traités dans la partie sur les tableaux.

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

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

Le contrôle d'accès à une variable est soit public, soit protected, soit private, soit par défaut, si aucun de ces modifieurs n'est précisé, amical (friendly en anglais) : la variable 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 une variable. Comme vous pouvez le voir, l'utilisation d'une variable 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'une variable se comporte exactement comme l'affectation. Si la variable est static, alors l'initialisation est effectuée au chargement de la classe.
Les variables non initialisées prennent obligatoirement une valeur par défaut (voir le tableau sur les types de base). Si cette variable est une référence à un objet, la valeur par défaut est null.

C

Une classe Classe1 peut déclarer une variable 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 modifieur 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 variables de classe ainsi que les variables d'instance.

C

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

C++

L'accès aux variables 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 variables static, vous le faites directement à la déclaration de la variable, comme par exemple static int var = 2;.

C++

Le contrôle d'accès aux variables (et aux méthodes) se fait pour chacune d'elles individuellement, et pas en bloc comme en C++ (avec par exemple public :).

C++

Les variables (et les méthodes) d'une classe Classe1 dont le contrôle 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 variables (et les méthodes) d'une classe Classe1 ont un contrôle 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 contrôle d'accès est private.

!

Le contrôle d'accès par défaut de Java est très pratique car il donne accès à toutes les variables 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 contrôles d'accès public pour accéder en dehors du package outils aux méthodes et variables dont vous avez besoin.
Donc, prenez l'habitude de préciser dès le départ les contrôles d'accès des variables 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 variables static. Ils sont exécutés dans l'ordre de déclaration et peuvent ne manipuler que les variables static déclarées avant le bloc.

class Classe1
{
  // Déclaration de variables static
  static int  variable1 = 10;
  static int  variable2;
 
  static
  {  // Bloc static
    variable2 = variable1 * 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 variables static de ces classes sont uniques pour une Machine Virtuelle, et partagées entre les différentes applets. Donc, attention aux effets de bord ! Si vous modifiez la valeur d'une variable static dans une applet, elle sera modifiée pour toutes les applets amenées à utiliser cette variable.
Ceci est à opposer au C, où les variables 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'une variable comptant le nombre d'instances créées
  static int  nombreInstances = 0;
  // Déclaration d'une variable d'instance
  int  variable1;
 
  {  // Bloc d'instance
    variable1 = 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
  TypeRetour methode1 (TypeParam1 param1Name /*,... , TypeParamN paramNName*/)
  { 
    // Corps de methode1 ()
  }
 
  ModifieurDeMethode TypeRetour methode2 (TypeParam1 param1Name /* ... */)
  { 
    // Corps de methode2 ()
  }
 
  ModifieurDeMethode TypeRetour methode3 (/* ... */) throws TypeThrowable
                                                            /*, TypeThrowable2 */
  { 
    // Corps de methode3 ()
  }
 
  // Déclaration d'une méthode abstract
  // Dans ce cas, Classe1 doit être aussi abstract abstract ModifieurDeMethode TypeRetour methode4 (/* ... */);
 
  // Déclaration d'une méthode native
  native ModifieurDeMethode TypeRetour methode4 (/* ... */);
 
  // ...
}

TypeRetour peut être :

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.

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

Le contrôle d'accès à une méthode est soit public, soit protected, soit private. Leur utilisation est la même que pour les variables.
Dans la plupart des cas, il est conseillé de ne rendre public que les méthodes et les constantes (variables final static), dont a besoin l'utilisateur d'une classe. Les autres variables sont déclarées private voir friendly ou protected et sont rendues 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 variables et de vérifier éventuellement les conditions pour modifier une variable.
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.
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++

Java ne permet pas l'utilisation des listes d'arguments variables qui existent en 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 à une variable 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 toutes les variables et les méthodes de Classe1 dans le corps de ses méthodes qu'elles soient déclarées 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 variables 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 modifieur 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 modifieur quand cela est nécessaire (pour les méthodes d'accès aux variables 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 d'un type base, 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 une variable 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 la 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 variable d'une classe peut être initialisée à une valeur par défaut à sa déclaration. Mais si vous voulez initialiser certaines de ces variables 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 variables 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
  }
 
  ModifieurDeConstruceur Classe1 (TypeParam1 param1Name /* ... */)
  { 
    // Corps du constructeur
  }
 
  ModifieurDeConstruceur 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 public par défaut sans paramètre qui ne fait rien. 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éclarer 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.

ModifieurDeConstruceur 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 la variable var1 est initialisée 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 une variable d'un objet après sa création ?
Un constructeur permet d'initialiser certaines variables 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 telle ou telle variable 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 toutes les variables d'un objet seront correctement initialisées 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.

 

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 variable;
  Classe1 (int var)
  {
    variable = var;
  }
}
 
class Classe2 extends Classe1
{
  int variable2;
  Classe2 (int var)
  {
    // Appel du constructeur de Classe1 avec la valeur 3
    super (3);
    variable = var;
  }
 
  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 variables d'instance est alloué, ces variables sont ensuite initialisées 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 variables 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 éventuellement 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 somme des */
/* deux 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 variables var1 et var2
  public Classe1 (int valeur1,
                   int valeur2)
  {
    var1 = valeur1;
    var2 = valeur2;
  }
 
  // Méthode renvoyant la somme des
  // deux variables d'un objet de 
  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 methodeOutrepassee (), 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; // Variable protected accessible
                                // par les classes dérivées
 
  // Constructeur
  Compte (int nouveauNumero, float sommeDeposee)
  {
    // Mise à jour des variables 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 (105, 201.1f);
 
    CompteDepot compte105 = new CompteDepot (101);
    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 la variable 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 : 105 Solde = 201.1
Compte : 101 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 variables d'instance qui sont allouées à la création d'une nouvelle instance des classes Compte, CompteDepot ou CompteEpargne, une variable cachée qui représente la classe du nouvel objet lui est ajoutée. A l'appel compte.calculerSolde (), consulte cette variable pour connaître la classe de l'objet désigné par compte. Une fois qu'il a cette classe il 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 contrôle 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 contrôles 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 et des types des paramètres des méthodes que vous outrepassez. 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 dira rien et finalement la méthode appelée pourra être celle de la super classe au lieu de celle que vous avez déclaré.
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.

!

Seules les méthodes sont outrepassées et pas les variables, comme le montre l'exemple suivant :

class Classe1
{
  final static int cste1 = 0;
}
 
class Classe2 extends Classe1
{
  // Déclaration d'une variable (constante) qui cache celle de Classe1
  final static int cste1 = 1;
 
  void methode1 ()
  {
    Classe2 objet2 = new Classe2 (); // création d'un objet de classe Classe2
    int var = objet2.cste1;          // var vaut 1
    Classe1 objet1 = objet2;         // cast de Classe2 vers Classe1
    var = objet1.cste1;              // 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 à cste1 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.cste1 vaille 1 puisque objet1 désigne un objet de classe Classe2. En fait, objet1.cste1 renvoie la valeur de cste1 de la classe Classe1 (pas de ligature dynamique sur les variables)... 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 cste1, 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 une variable var1 qui est une référence de classe classeAbstract. var1 peut être égale à 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 Voiture 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 de base 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 de base 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, la variable 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 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.

 


eTeks : Du C/C++ à JavaContactLes notions de baseObjets, tableaux et chaînesDébut de la page

eTeks
© 1997-2000 Emmanuel PUYBARET / eTeks - Tous droits réservés

Hiérarchie des classesTable des matières