IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 4 - Initialisation & nettoyage

pages : 1 2 3 4 5 6 

//: c04:Demotion.java
// Types de base déchus et surcharge.

public class Demotion {
  static void prt(String s) {
    System.out.println(s);
  }

  void f1(char x) { prt("f1(char)"); }
  void f1(byte x) { prt("f1(byte)"); }
  void f1(short x) { prt("f1(short)"); }
  void f1(int x) { prt("f1(int)"); }
  void f1(long x) { prt("f1(long)"); }
  void f1(float x) { prt("f1(float)"); }
  void f1(double x) { prt("f1(double)"); }

  void f2(char x) { prt("f2(char)"); }
  void f2(byte x) { prt("f2(byte)"); }
  void f2(short x) { prt("f2(short)"); }
  void f2(int x) { prt("f2(int)"); }
  void f2(long x) { prt("f2(long)"); }
  void f2(float x) { prt("f2(float)"); }

  void f3(char x) { prt("f3(char)"); }
  void f3(byte x) { prt("f3(byte)"); }
  void f3(short x) { prt("f3(short)"); }
  void f3(int x) { prt("f3(int)"); }
  void f3(long x) { prt("f3(long)"); }

  void f4(char x) { prt("f4(char)"); }
  void f4(byte x) { prt("f4(byte)"); }
  void f4(short x) { prt("f4(short)"); }
  void f4(int x) { prt("f4(int)"); }

  void f5(char x) { prt("f5(char)"); }
  void f5(byte x) { prt("f5(byte)"); }
  void f5(short x) { prt("f5(short)"); }

  void f6(char x) { prt("f6(char)"); }
  void f6(byte x) { prt("f6(byte)"); }

  void f7(char x) { prt("f7(char)"); }

  void testDouble() {
    double x = 0;
    prt("double argument:");
    f1(x);f2((float)x);f3((long)x);f4((int)x);
    f5((short)x);f6((byte)x);f7((char)x);
  }
  public static void main(String[] args) {
    Demotion p = new Demotion();
    p.testDouble();
  }
} ///:~

Ici, les méthodes prennent des types de base plus restreints. Si les paramètres sont d'un type plus grand, if faut les caster (convertir) vers le type requis en utilisant le nom du type entre parenthèses. Sinon, le compilateur donnera un message d'erreur.

Il est important de noter qu'il s'agit d'une conversion vers un type plus petit, ce qui signifie que des informations peuvent être perdues pendant la conversion. C'est d'ailleurs pour cette raison que le compilateur force une conversion explicite.

Surcharge sur la valeur de retour

Il est fréquent de se demander «Pourquoi seulement les noms de classes et la liste des paramètres des méthodes ? Pourquoi ne pas aussi distinguer entre deux méthodes en se basant sur leur type de retour ?» Par exemple, ces deux méthodes, qui ont le même nom et les mêmes arguments, peuvent facilement être distinguées l'une de l'autre :

void f() {}
int f() {}

Cela fonctionne bien lorsque le compilateur peut déterminer le sens sans équivoque depuis le contexte, comme dans int x = f( ). Par contre, on peut utiliser une méthode et ignorer sa valeur de retour. On se réfère souvent à cette action comme appeler une méthode pour ses effets de bord puisqu'on ne s'intéresse pas à la valeur de retour mais aux autres effets que cet appel de méthode génère. Donc, si on appelle la méthode comme suit :

f();

Comment Java peut-il déterminer quelle méthode f( ) doit être exécutée ? Et comment quelqu'un lisant ce code pourrait-il le savoir ? A cause de ce genre de difficultés, il est impossible d'utiliser la valeur de retour pour différencier deux méthodes Java surchargées.

Constructeurs par défaut

Comme mentionné précédemment, un constructeur par défaut (c.a.d un constructeur «no-arg» ) est un constructeur sans argument, utilisé pour créer des « objets de base ». Si une classe est créée sans constructeur, le compilateur créé automatiquement un constructeur par défaut. Par exemple :

//: c04:DefaultConstructor.java

class Bird {
  int i;
}

public class DefaultConstructor {
  public static void main(String[] args) {
    Bird nc = new Bird(); // défaut !
  }
} ///:~

La ligne

new Bird();

crée un nouvel objet et appelle le constructeur par défaut, même s'il n'était pas défini explicitement. Sans lui, il n'y aurait pas de méthode à appeler pour créer cet objet. Par contre, si au moins un constructeur est défini (avec ou sans argument), le compilateur n'en synthétisera pas un :

class Bush {
  Bush(int i) {}
  Bush(double d) {}
}

Maintenant si on écrit :

new Bush();

le compilateur donnera une erreur indiquant qu'aucun constructeur ne correspond. C'est comme si lorsqu'aucun constructeur n'est fourni, le compilateur dit «Il faut un constructeur, je vais en créer un.» Alors que s'il existe un constructeur, le compilateur dit «Il y a un constructeur donc le développeur sait se qu'il fait; s'il n'a pas défini de constructeur par défaut c'est qu'il ne désirait pas qu'il y en ait un.»

Le mot-clé this

Lorsqu'il existe deux objets a et b du même type , il est intéressant de se demander comment on peut appeler une méthode f( ) sur ces deux objets :

class Banana { void f(int i) { /* ... */ } }
Banana a = new Banana(), b = new Banana();
a.f(1);
b.f(2);

S'il y a une unique méthode f( ), comment cette méthode peut-elle savoir si elle a été appelée sur l'objet a ou b ?

Pour permettre au développeur d'écrire le code dans une syntaxe pratique et orienté objet dans laquelle on «envoie un message vers un objet,» le compilateur effectue un travail secret pour le développeur. Il y a un premier paramètre caché passé à la méthode f( ), et ce paramètre est une référence vers l'objet en train d'être manipulé. Les deux appels de méthode précédents correspondent donc à ceci :

Banana.f(a,1);
Banana.f(b,2);

Ce travail est interne et il est impossible d'écrire des expressions de ce type directement en espérant que le compilateur les acceptera, mais cela donne une idée de ce qui se passe.

Supposons maintenant que l'on est à l'intérieur d'une méthode et que l'on désire obtenir une référence sur l'objet courant. Comme cette référence est passée en tant que paramètre caché par le compilateur, il n'y a pas d'identificateur pour elle. Cette pour cette raison que le mot clé this existe. this - qui ne peut être utilisé qu'à l'intérieur d'une méthode - est une référence sur l'objet pour lequel cette méthode à été appelée. On peut utiliser cette référence comme tout autre référence vers un objet. Il n'est toutefois pas nécessaire d'utiliser this pour appeler une méthode de la classe courante depuis une autre méthode de la classe courante ; il suffit d'appeler cette méthode. La référence this est automatiquement utilisée pour l'autre méthode. On peut écrire :

class Apricot {
  void pick() { /* ... */ }
  void pit() { pick(); /* ... */ }
}

A l'intérieur de pit( ), on pourrait écrire this.pick( ) mais ce n'est pas nécessaire. Le compilateur le fait automatiquement pour le développeur. Le mot-clé this est uniquement utilisé pour les cas spéciaux dans lesquels on doit utiliser explicitement une référence sur l'objet courant. Par exemple, il est courament utilisé en association avec return quand on désire renvoyer une référence sur l'objet courant :

//: c04:Leaf.java
// Utilisation simple du mot-clé "this".

public class Leaf {
  int i = 0;
  Leaf increment() {
    i++;
    return this;
  }
  void print() {
    System.out.println("i = " + i);
  }
  public static void main(String[] args) {
    Leaf x = new Leaf();
    x.increment().increment().increment().print();
  }
} ///:~

Puisque increment( ) renvoie une référence vers l'objet courant par le biais du mot-clé this, on peut facilement appeler plusieurs opérations successivement sur le même objet.

Appeler un constructeur depuis un autre constructeur

Quand une classe possède plusieurs constructeurs, il peut être utile d'appeler un constructeur depuis un autre pour éviter de la duplication de code. C'est possible grâce au mot-clé this.

En temps normal, this signifie «cet objet» ou «l'objet courant,» et renvoie une référence sur l'objet courant. Dans un constructeur, le mot-clé this prend un sens différent quand on lui passe une liste de paramètres : il signifie un appel explicite au constructeur qui correspond à cette liste de paramètres. Cela donne un moyen très simple d'appeler d'autres constructeurs :

//: c04:Flower.java
// Appel de constructeurs avec "this."

public class Flower {
  int petalCount = 0;
  String s = new String("null");
  Flower(int petals) {
    petalCount = petals;
    // Constructeur avec un unique paramètre int
    System.out.println(
      "Constructor w/ int arg only, petalCount= "
      + petalCount);
  }
  Flower(String ss) {
    // Constructeur avec un unique paramètre String
    System.out.println(
      "Constructor w/ String arg only, s=" + ss);
    s = ss;
  }
  Flower(String s, int petals) {
    this(petals);
//!    this(s); // Impossible d'en appeler deux !
    this.s = s; // Autre usage de "this"
    System.out.println("String & int args");
  }

  // Constructeur par défaut
  Flower() {
    this("hi", 47);
    System.out.println(
      "default constructor (no args)");
  }
  void print() {
//!    this(11); // Pas à l'intérieur d'une méthode normale !
    System.out.println(
      "petalCount = " + petalCount + " s = "+ s);
  }
  public static void main(String[] args) {
    Flower x = new Flower();
    x.print();
  }
} ///:~

Le constructeur Flower(String s, int petals) montre qu'on peut appeler un constructeur en utilisant this, mais pas deux. De plus, l'appel au constructeur doit absolument être la première instruction sinon le compilateur donnera un message d'erreur.

Cet exemple montre aussi un usage différent du mot-clé this. Les noms du paramètre s et du membre de données s étant les mêmes, il y a ambiguïtée. On la résoud en utilisant this.s pour se référer au membre de donnés. Cette forme est très courante en Java et utilisée fréquemment dans ce livre.

Dans la méthode print( ) on peut voir que le compilateur ne permet pas l'appel d'un constructeur depuis toute autre méthode qu'un constructeur.

La signification de static

En pensant au mot-clé this, on comprend mieux le sens de rendre une méthode statics. Cela signifie qu'il n'y a pas de this pour cette méthode. Il est impossible d'appeler une méthode non-static depuis une méthode static [28] (par contre, l'inverse est possible), et il est possible d'appeler une méthode static sur la classe elle-même, sans aucun objet. En fait, c'est principalement la raison de l'existence des méthodes static. C'est l'équivalent d'une fonction globale en C. Sauf que les fonctions globales sont interdites en Java, et ajouter une méthode static dans une classe lui permet d'accéder à d'autres méthodes static ainsi qu'aux membres static.

Certaines personnes argumentent que les méthodes static ne sont pas orientées objet puisqu'elles ont la sémantique des fonctions globales ; avec une méthode static on n'envoie pas un message vers un objet, puisqu'il n'y a pas de this. C'est probablement un argument valable, et si vous utilisez beaucoup de méthodes statiques vous devriez repenser votre stratégie. Pourtant, les méthodes statics sont utiles et il y a des cas où on en a vraiment besoin. On peut donc laisser les théoriciens décider si oui ou non il s'agit de vraie programmation orientée objet. D'ailleurs, même Smalltalk a un équivalent avec ses «méthodes de classe.»

Nettoyage : finalisation et ramasse-miettes

Les programmeurs connaissent l'importance de l'initialisation mais oublient souvent celle du nettoyage. Après tout, qui a besoin de nettoyer un int ? Cependant, avec des bibliothèques, simplement oublier un objet après son utilisation n'est pas toujours sûr. Bien entendu, Java a un ramasse-miettes pour récupérer la mémoire prise par des objets qui ne sont plus utilisés. Considérons maintenant un cas très particulier. Supposons que votre objet alloue une zone de mémoire spéciale sans utiliser new. Le ramasse-miettes ne sait récupérer que la mémoire allouée avec new, donc il ne saura pas comment récupérer la zone «speciale» de mémoire utilisée par l'objet. Pour gérer ce cas, Java fournit une méthode appelée finalize( ) qui peut être définie dans votre classe. Voici comment c'est supposé marcher. Quand le ramasse-miettes est prêt à libérer la mémoire utilisée par votre objet, il va d'abord appeler finalize( ) et ce n'est qu'à la prochaine passe du ramasse-miettes que la mémoire de l'objet est libérée. En choisissant d'utiliser finalize( ), on a la possibilité d'effectuer d'importantes tâches de nettoyage à l'exécution du ramasse-miettes.

C'est un piège de programmation parce que certains programmeurs, particulièrement les programmeurs C++, risquent au début de confondre finalize( ) avec le destructeur de C++ qui est une fonction toujours appelée quand un objet est détruit. Cependant il est important ici de faire la différence entre C++ et Java, car en C++ les objets sont toujours détruits (dans un programme sans bug), alors qu'en Java les objets ne sont pas toujours récupérés par le ramasse-miettes. Dit autrement :

Le mécanisme de ramasse-miettes n'est pas un mécanisme de destruction.

Si vous vous souvenez de cette règle de base, il n'y aura pas de problème. Cela veut dire que si une opération doit être effectuée avant la disparition d'un objet, celle-ci est à la charge du développeur. Java n'a pas de mécanisme équivalent au destructeur, il est donc nécessaire de créer une méthode ordinaire pour réaliser ce nettoyage. Par exemple, supposons qu'un objet se dessine à l'écran pendant sa création. Si son image n'est pas effacée explicitement de l'écran, il se peut qu'elle ne le soit jamais. Si l'on ajoute une fonctionnalité d'effacement dans finalize( ), alors l'image sera effacée de l'écran si l'objet est récupéré par le ramasse-miette, sinon l'image restera. Il y a donc une deuxième règle à se rappeler :

Les objets peuvent ne pas être récupérés par le ramasse-miettes.

Il se peut que la mémoire prise par un objet ne soit jamais libérée parce que le programme n'approche jamais la limite de mémoire qui lui a été attribuée. Si le programme se termine sans que le ramasse-miettes n'ait jamais libéré la mémoire prise par les objets, celle-ci sera rendue en masse (NDT : en français dans le texte) au système d'exploitation au moment où le programme s'arrête. C'est une bonne chose, car le ramasse-miettes implique un coùt supplémentaire et s'il n'est jamais appelé, c'est autant d'économisé.

A quoi sert finalize( ) ?

A ce point, on peut croire qu'il ne faudrait pas utiliser finalize( ) comme méthode générale de nettoyage. A quoi sert-elle alors ?

Une troisième règle stipule :

Le ramasse-miettes ne s'occupe que de la mémoire.

C'est à dire que la seule raison d'exister du ramasse-miettes est de récupérer la mémoire que le programme n'utilise plus. Par conséquent, toute activité associée au ramasse-miettes, la méthode finalize( ) en particulier, doit se concentrer sur la mémoire et sa libération.

Est-ce que cela veut dire que si un objet contient d'autres objets, finalize( ) doit libérer ces objets explicitement ? La réponse est... non. Le ramasse-miettes prend soin de libérer tous les objets quelle que soit la façon dont ils ont été créés. Il se trouve que l'on a uniquement besoin de finalize( ) dans des cas bien précis où un objet peut allouer de la mémoire sans créer un autre objet. Cependant vous devez vous dire que tout est objet en Java, donc comment est-ce possible ?

Il semblerait que finalize( ) ait été introduit parce qu'il est possible d'allouer de la mémoire à-la-C en utilisant un mécanisme autre que celui proposé normalement par Java. Cela arrive généralement avec des méthodes natives, qui sont une façon d'appeler du code non-Java en Java (les méthodes natives sont expliquées en Appendice B). C et C++ sont les seuls langages actuellement supportés par les méthodes natives, mais comme elles peuvent appeler des routines écrites avec d'autres langages, il est en fait possible d'appeler n'importe quoi. Dans ce code non-Java, on peut appeler des fonctions de la famille de malloc( ) en C pour allouer de la mémoire, et à moins qu'un appel à free( ) ne soit effectué cette mémoire ne sera pas libérée, provoquant une «fuite». Bien entendu, free( ) est une fonction C et C++, ce qui veut dire qu'elle doit être appelée dans une méthode native dans le finalize( ) correspondant.

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par F. Defaix et Y. Chicha ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 5 6 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java