|
Création et utilisation des classes |
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.
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 :
- public : Une seule classe ou interface peut être déclarée public par fichier source .java. Par convention, le fichier porte le nom de la classe déclarée public. Si d'autres classes (non public) sont déclarées dans un fichier Classe1.java, elles ne peuvent être utilisés que dans les fichiers qui appartiennent au même package que Classe1.java.
- final : Une classe déclarée final ne peut être dérivée et ne peut donc jamais suivre la clause extends. Cette clause peut être utile quand vous considérez qu'une classe ne doit pas ou n'a pas besoin d'être dérivée.
- abstract : Il est impossible de créer une instance d'une classe déclarée abstract. Cette catégorie de classe peut comporter une ou plusieurs méthodes déclarées abstract. Par contre, si une classe comporte une méthode abstract, elle doit être être déclarée abstract.
A quoi sert une classe abstract si on ne peut créer aucun objet de cette classe ? Ce type de classe est utilisé pour fournir des méthodes et des champs communs à toutes les classes qui en dérivent. L'intérêt des classes abstract est démontrée plus loin dans ce chapitre.
Une classe ne peut être déclarée abstract et final (elle ne servirait à rien puisqu'il serait impossible de créer des classes dérivées de celle-ci).
figure 4. Symbolisation des modificateurs d'accès aux classes
Contrairement au C++, le point-virgule en fin de déclaration d'une classe est optionnel.
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++.
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 { /* ... */ }.
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++.
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 :
- public : Une seule interface ou classe peut être déclarée public par fichier source. Par convention, le fichier porte le nom de l'interface déclarée public. Si d'autres interfaces (non public) sont déclarées dans un fichier Interface1.java, elles ne peuvent être utilisés que dans les fichiers qui appartiennent au même package que Interface1.java. Une interface ne peut porter le même nom qu'une classe.
- abstract : Toute interface est implicitement abstract. Ce modificateur est permis mais pas obligatoire.
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.
- Soit une classe Classe1 qui implémente toutes les méthodes d'une interface Interface1. Cette classe peut déclarer d'autres méthodes, ce qui compte c'est que chaque instance de cette classe garantit qu'au moins toutes les méthodes d'Interface1 existent et peuvent être appelées, comme dans l'exemple suivant :
interface CouleurDominante { // Déclaration d'une méthode QuelleCouleurDominante () } class Classe1 extends SuperClasse1 implements CouleurDominante { // Corps de la méthode QuelleCouleurDominante () // Autres méthodes éventuelles } class Classe2 extends SuperClasse2 implements CouleurDominante { // Corps de la méthode QuelleCouleurDominante () // Autres méthodes éventuelles } class ClasseAyantBesoinDeLaCouleurDominante { CouleurDominante objetColore; // On peut affecter à objetColore une référence // à un objet de classe Classe1 ou Classe2 // et ainsi obtenir la couleur dominante d'un objet coloré en invoquant // la méthode QuelleCouleurDominante () sur la variable objetColore }- Une interface peut être utilisée pour masquer l'implémentation d'une classe. Ce concept est utilisé dans plusieurs packages Java dont java.awt.peer : Ce package déclare un ensemble d'interfaces qui offrent les mêmes méthodes sur chaque Machine Virtuelle Java mais qui sont implémentées différemment pour que l'interface utilisateur du système (bouton, fenêtre,...) soit utilisée.
- Une interface vide (comme l'interface Cloneable) permet de créer une catégorie de classes : chaque classe implémentant ce type d'interface appartient à telle ou telle catégorie. Pour tester si la classe d'un objet appartient à une catégorie, il suffit d'utiliser l'opérateur instanceof avec le nom de l'interface.
- Une interface peut servir aussi pour déclarer un ensemble de constantes, qui seront utilisées dans des classes sans lien d'héritage entre elles, comme dans l'exemple suivant :
interface ConstantesCommunes { // Déclaration des constantes A, B et C } class Classe1 extends SuperClasse1 implements ConstantesCommunes { // Les constantes A, B et C sont utilisables dans la classe Classe1 } class Classe2 extends SuperClasse2 implements ConstantesCommunes { // Les constantes A, B et C sont utilisables dans la classe Classe2 }
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.
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.
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 :
- public, protected ou private :
- Un champ public est accessible partout où est accessible la classe Classe1 dans laquelle il est déclaré.
- Un champ protected est accessible par les autres classes du même package que Classe1, et par les classes dérivées de Classe1.
- Un champ private n'est accessible qu'à l'intérieur du corps de Classe1.
- static : Si un champ est static, il est créée en un unique exemplaire quelque soit le nombre d'instance de Classe1 : c'est un champ de classe. L'accès à la valeur de ce champ se fait grâce à l'opérateur point (.) en indiquant le nom de la classe ou une référence à un objet (Classe1.champ ou objetClasse1.champ).
A l'opposé, si un champ n'est pas static, chaque nouvelle instance objetClasse1 de Classe1 créera un champ pour objetClasse1 : c'est une champ d'instance. L'accès à la valeur de ce champ se fait grâce à l'opérateur point (.) , de la manière objetClasse1.champ.- final : Un champ final n'est pas modifiable une fois initialisé : c'est une constante qui peut prendre une valeur initiale différente d'un objet à l'autre d'une même classe. Si ce champ a toujours la même valeur d'initialisation, il vaut mieux optimiser votre code en y ajoutant static pour en faire une constante de classe.
Les champs déclarées dans le corps d'une interface sont des constantes implicitement public, final et static, et doivent être initialisés avec une expression constante.- transient : Un champ transient sert à indiquer aux méthodes gérant la persistance qu'elle ne fait pas partie de la persistance de l'objet. Cette fonctionnalité n'a été mise en oeuvre qu'à partir de Java 1.1, et sert à éviter de sauvegarder des champs ne servant qu'à des calculs intermédiaires (indices par exemple).
- volatile : Un champ volatile permet d'être sûr que deux threads (tâches) auront accès de manière ordonnée à ce champ (modificateur implémenté uniquement à partir de Java 1.1). Voir aussi le chapitre sur les threads. Un champ volatile ne peut être aussi final.
figure 5. Symbolisation des modificateurs d'accès aux champsLe 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.
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).
En Java, le modificateur final est utilisé pour déclarer une constante (pas de #define, ni de const).
Contrairement au C/C++, Java permet d'initialiser à la déclaration les champs de classe ainsi que les champs d'instance.
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.
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;.
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 :).
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.
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++; } // ... }
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 :
- soit un type primitif, une classe ou une interface : dans ce cas, la méthode doit utiliser l'instruction return suivie d'une valeur du type TypeRetour, pour renvoyer une valeur. Le type peut être une référence à un tableau en utilisant [ ] (par exemple, int [ ] methode ()).
- soit void quand la méthode ne renvoie pas de valeur.
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 :
- public, protected ou private :
- Une méthode public est accessible partout où est accessible la classe Classe1 dans laquelle elle est déclarée. Ce sont les méthodes public que l'utilisateur d'une classe peut appeler.
- Une méthode protected est accessible par les autres classes du même package que Classe1, et par les classes dérivées de Classe1.
- Une méthode private n'est accessible qu'à l'intérieur du corps de Classe1. Les méthodes private sont typiquement des routines internes de calcul pour lesquels il n'y a pas de raison de donner un accès à l'utilisateur d'une classe.
- static : Si une méthode est static, c'est une méthode de classe qui ne peut utiliser que les champs et les méthodes de Classe1, qui sont déclarées static. L'appel à cette méthode se fait grâce à l'opérateur point (.) en indiquant le nom de la classe ou une référence à un objet (Classe1.methode () ou objetClasse1.methode ()).
A l'opposé, si une méthode n'est pas static, c'est une méthode d'instance, accessible pour chaque instance objetClasse1 de Classe1 grâce à l'opérateur point (.) , de la manière objetClasse1.methode ().
Les méthodes déclarées dans le corps d'une interface ne peuvent être static.- final : Une méthode final ne peut pas être outrepassée par les classes dérivant de Classe1.
- abstract : Une méthode abstract permet de déclarer une méthode d'instance sans en donner l'implémentation, et ne peut apparaître qu'au sein d'une classe abstract. Toute classe non abstract dérivée de cette classe, doit implémenter cette méthode, en l'outrepassant.
Les méthodes déclarées dans le corps d'une interface sont implicitement abstract et public.- native : Une méthode native est implémentée dans une bibliothèque annexe propre à la plateforme de développement qui peut être développée en C ou en C++ par exemple. Ceci permet de faire appel à certaines fonctionnalités propres à la plateforme ciblée, qui ne seraient pas disponibles en Java. Mais une méthode native n'est pas portable...
- synchronized : Une méthode synchronized permet d'obtenir un verrou sur l'objet sur lequel elle est appelée (ou sur la classe si la méthode est aussi static). Ce verrou empêche qu'en cas de programmation multi-threads (multitâches), différents threads aient accès de manière simultanée à un même objet. Voir aussi la synchronisation des threads.
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); }
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.
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).
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).
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; }
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.
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.
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++.
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).
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++.
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).
La surcharge des opérateurs n'existe pas en Java. Seule la classe String autorise l'opérateur + pour la concaténation.
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.
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 :
- 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.
- 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 ()).
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 :
- Si Classe1 hérite d'une super classe Classe0, appel d'un constructeur de Classe0 soit par un appel explicite à super (...), ou implicitement si Classe0 possède une constructeur par défaut.
- Exécution d'éventuels blocs d'initialisation d'instance déclarés dans Classe1.
- Exécution du constructeur de Classe1.
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.
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.
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 (); } }
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.0La 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.
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.
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é...
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); } }
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.
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.
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).
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 :
figure 7. La vie d'un objet Java de sa création à sa destructionCe 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.
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.
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.
|