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 

Initialisation de membre

Java prend en charge l'initialisation des variables avant leur utilisation. Dans le cas des variables locales à une méthode, cett garantie prend la forme d'une erreur à la compilation. Donc le code suivant :

void f() {
    int i;
    i++;
  }

générera un message d'erreur disant que la variable i peut ne pas avoir été initialisée. Bien entendu, le compilateur aurait pu donner à i une valeur par défaut, mais il est plus probable qu'il s'agit d'une erreur de programmation et une valeur par défaut aurait masqué ce problème. En forçant le programmeur à donner une valeur par défaut, il y a plus de chances de repérer un bogue.

Cependant, si une valeur primitive est un membre de données d'une classe, les choses sont un peu différentes. Comme n'importe quelle méthode peut initialiser ou utiliser cette donnée, il ne serait pas très pratique ou faisable de forcer l'utilisateur à l'initialiser correctement avant son utilisation. Cependant, il n'est pas correct de la laisser avec n'importe quoi comme valeur, Java garantit donc de donner une valeur initiale à chaque membre de données avec un type primitif. On peut voir ces valeurs ici :

//: c04:InitialValues.java
// Imprime les valeurs initiales par défaut.

class Measurement {
  boolean t;
  char c;
  byte b;
  short s;
  int i;
  long l;
  float f;
  double d;
  void print() {
    System.out.println(
      "Data type      Initial value\n" +
      "boolean        " + t + "\n" +
      "char           [" + c + "] "+ (int)c +"\n"+
      "byte           " + b + "\n" +
      "short          " + s + "\n" +
      "int            " + i + "\n" +
      "long           " + l + "\n" +
      "float          " + f + "\n" +
      "double         " + d);
  }
}

public class InitialValues {
  public static void main(String[] args) {
    Measurement d = new Measurement();
    d.print();
    /* Dans ce cas, il est également possible d'écrire :
    new Measurement().print();
    */

  }
} ///:~

Voici la sortie de ce programme :

Data type      Initial value
boolean        false
char           [ ] 0
byte           0
short          0
int            0
long           0
float          0.0
double         0.0

La valeur pour char est zéro, ce qui se traduit par un espace dans la sortie-écran.

Nous verrons plus tard que quand on définit une référence sur un objet dans une classe sans l'initialiser avec un nouvel objet, la valeur spéciale null (mot-clé Java) est donnée à cette référence.

On peut voir que même si des valeurs ne sont pas spécifiées, les données sont initialisées automatiquement. Il n'y a donc pas de risque de travailler par inattention avec des variables non-initialisées.

Spécifier une initialisation

Comment peut-on donner une valeur initiale à une variable ? Une manière directe de le faire est la simple affectation au moment de la définition de la variable dans la classe (note : il n'est pas possible de le faire en C++ bien que tous les débutants s'y essayent). Les définitions des champs de la classe Measurement sont modifiées ici pour fournir des valeurs initiales :

class Measurement {
  boolean b = true;
  char c = 'x';
  byte B = 47;
  short s = 0xff;
  int i = 999;
  long l = 1;
  float f = 3.14f;
  double d = 3.14159;
  //. . .

On peut initialiser des objets de type non-primitif de la même manière. Si Depth (NDT : « profondeur ») est une classe, on peut ajouter une variable et l'initialiser de cette façon :

class Measurement {
  Depth o = new Depth();
  boolean b = true;
  // . . .

Si o ne reçoit pas de valeur initiale et que l'on essaye de l'utiliser malgré tout, on obtient une erreur à l'exécution appelée exception (explications au chapitre 10).

Il est même possible d'appeler une méthode pour fournir une valeur d'initialisation :

class CInit {
  int i = f();
  //...
}

Bien sûr cette méthode peut avoir des arguments, mais ceux-ci ne peuvent pas être d'autres membres non encore initialisés, de la classe. Par conséquent ce code est valide :

class CInit {
  int i = f();
  int j = g(i);
  //...
}

Mais pas celui-ci :

class CInit {
  int j = g(i);
  int i = f();
  //...
}

C'est un des endroits où le compilateur se plaint avec raison du forward referencing (référence à un objet déclaré plus loin dans le code), car il s'agit d'une question d'ordre d'initialisation et non pas de la façon dont le programme est compilé.

Cette approche par rapport à l'initialisation est très simple. Elle est également limitée dans le sens où chaque objet de type Measurement aura les mêmes valeurs d'initialisation. Quelquefois c'est exactement ce dont on a besoin, mais d'autres fois un peu plus de flexibilité serait nécessaire.

Initialisation par constructeur

On peut utiliser le constructeur pour effectuer les initialisations. Cela apporte plus de flexibilité pour le programmeur car il est possible d'appeler des méthodes et effectuer des actions à l'exécution pour déterminer les valeurs initiales. Cependant il y a une chose à se rappeler : cela ne remplace pas l'initialisation automatique qui est faite avant l'exécution du constructeur. Donc par exemple :

class Counter {
  int i;
  Counter() { i = 7; }
  // . . .

Dans ce cas, i sera d'abord initialisé à 0 puis à 7. C'est ce qui ce passe pour tous les types primitifs et les références sur objet, même pour ceux qui ont été initialisés explicitement au moment de leur définition. Pour cette raison, le compilateur ne force pas l'utilisateur à initialiser les éléments dans le constructeur à un endroit donné, ni avant leur utilisation : l'initialisation est toujours garantie [30].

Ordre d'initialisation

Dans une classe, l'ordre d'initialisation est déterminé par l'ordre dans lequel les variables sont definies. Les définitions de variables peuvent être disséminées n'importe où et même entre les définitions des méthodes, mais elles sont initialisées avant tout appel à une méthode, même le constructeur. Par exemple :

//: c04:OrderOfInitialization.java
// Montre l'ordre d'initialisation.

// Quand le constructeur est appelé pour créer
// un objet Tag, un message s'affichera :
class Tag {
  Tag(int marker) {
    System.out.println("Tag(" + marker + ")");
  }
}

class Card {
  Tag t1 = new Tag(1); // Avant le constructeur
  Card() {
    // Montre que l'on est dans le constructeur :
    System.out.println("Card()");
    t3 = new Tag(33); // Réinitialisation de t3
  }
  Tag t2 = new Tag(2); // Après le constructeur
  void f() {
    System.out.println("f()");
  }
  Tag t3 = new Tag(3); // la fin
}

public class OrderOfInitialization {
  public static void main(String[] args) {
    Card t = new Card();
    t.f(); // Montre que la construction a été effectuée
  }
} ///:~

Dans la classe Card, les définitions des objets Tag sont intentionnellement dispersées pour prouver que ces objets seront tous initialisés avant toute action (y compris l'appel du constructeur). De plus, t3 est réinitialisé dans le constructeur. La sortie-écran est la suivante :

Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()

La référence sur t3 est donc initialisée deux fois, une fois avant et une fois pendant l'appel au constructeur (on jette le premier objet pour qu'il soit récupéré par le ramasse-miettes plus tard). A première vue, cela ne semble pas très efficace, mais cela garantit une initialisation correcte ; que se passerait-il si l'on surchargeait le constructeur avec un autre constructeur qui n'initialiserait pas t3 et qu'il n'y avait pas d'initialisation « par défaut » dans la définition de t3 ?

Initialisation de données statiques

Quand les données sont statiques (static) la même chose se passe ; s'il s'agit d'une donnée de type primitif et qu'elle n'est pas initialisée, la variable reçoit une valeur initiale standard. Si c'est une référence sur un objet, c'est la valeur null qui est utilisée à moins qu'un nouvel objet ne soit créé et sa référence donnée comme valeur à la variable.

Pour une initialisation à l'endroit de la définition, les mêmes règles que pour les variables non-statiques sont appliquées. Il n'y a qu'une seule version (une seule zone mémoire) pour une variable statique quel que soit le nombre d'objets créés. Mais une question se pose lorsque cette zone statique est initialisée. Un exemple va rendre cette question claire :

//: c04:StaticInitialization.java
// Préciser des valeurs initiales dans une
// définition de classe.

class Bowl {
  Bowl(int marker) {
    System.out.println("Bowl(" + marker + ")");
  }
  void f(int marker) {
    System.out.println("f(" + marker + ")");
  }
}

class Table {
  static Bowl b1 = new Bowl(1);
  Table() {
    System.out.println("Table()");
    b2.f(1);
  }
  void f2(int marker) {
    System.out.println("f2(" + marker + ")");
  }
  static Bowl b2 = new Bowl(2);
}

class Cupboard {
  Bowl b3 = new Bowl(3);
  static Bowl b4 = new Bowl(4);
  Cupboard() {
    System.out.println("Cupboard()");
    b4.f(2);
  }
  void f3(int marker) {
    System.out.println("f3(" + marker + ")");
  }
  static Bowl b5 = new Bowl(5);
}

public class StaticInitialization {
  public static void main(String[] args) {
    System.out.println(
      "Creating new Cupboard() in main");
    new Cupboard();
    System.out.println(
      "Creating new Cupboard() in main");
    new Cupboard();
    t2.f2(1);
    t3.f3(1);
  }
  static Table t2 = new Table();
  static Cupboard t3 = new Cupboard();
} ///:~

Bowl permet de visionner la création d'une classe. Table, ainsi que Cupboard, créent des membres static de Bowl partout au travers de leur définition de classe. Il est à noter que Cupboard crée un Bowl b3 non-statique avant les définitions statiques. La sortie montre ce qui se passe :

Bowl(1)
Bowl(2)
Table()
f(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f(2)
f2(1)
f3(1)

L'initialisation statique intervient seulement si c'est nécessaire. Si on ne crée jamais d'objets Table et que Table.b1 ou Table.b2 ne sont jamais référencés, les membres statiques Bowl b1 et b2 ne seront jamais créés. Cependant, ils ne sont initialisés que lorsque le premier objet Table est créé (ou le premier accès statique est effectué). Après cela, les objets statiques ne sont pas réinitialisés.

Dans l'ordre d'initialisation, les membres static viennent en premier, s'ils n'avaient pas déjà été initialisés par une précédente création d'objet, les objets non static sont traités. On peut le voir clairement dans la sortie du programme.

Il peut être utile de résumer le processus de création d'un objet. Considérons une classe appelée Dog :

  1. La première fois qu'un objet de type Dog est créé, ou la première fois qu'on utilise une méthode déclarée static ou un champ static de la classe Dog, l'interprèteur Java doit localiser Dog.class, ce qu'il fait en cherchant dans le classpath ;
  2. Au moment où Dog.class est chargée (créant un objet Class, que nous verrons plus tard), toutes les fonctions d'initialisation statiques sont exécutées. Par conséquent, l'initialisation statique n'arrive qu'une fois, au premier chargement de l'objet Class ;
  3. Lorsque l'on exécute new Dog( ) pour créer un nouvel objet de type Dog, le processus de construction commence par allouer suffisamment d'espace mémoire sur le tas pour contenir un objet Dog ;
  4. Cet espace est mis à zéro, donnant automatiquement à tous les membres de type primitif dans cet objet Dog leurs valeurs par défaut (zéro pour les nombres et l'équivalent pour les boolean et les char) et aux références la valeur null ;
  5. Toute initialisation effectuée au moment de la définition des champs est exécutée ;
  6. Les constructeurs sont exécutés. Comme nous le verrons au chapitre 6, ceci peut en fait déclencher beaucoup d'activité, surtout lorsqu'il y a de l'héritage.

Initialisation statique explicite

Java permet au programmeur de grouper toute autre initialisation statique dans une « clause de construction » static (quelquefois appelé bloc statique) dans une classe. Cela ressemble à ceci :

class Spoon {
  static int i;
  static {
    i = 47;
  }
  // . . .

On dirait une méthode, mais il s'agit simplement du mot-clé static suivi d'un corps de méthode. Ce code, comme les autres initialisations statiques, est exécuté une seule fois, à la création du premier objet de cette classe ou au premier accès à un membre déclaré static de cette classe (même si on ne crée jamais d'objet de cette classe). Par exemple :

//: c04:ExplicitStatic.java
// Initialisation statique explicite
// avec l'instruction "static".

class Cup {
  Cup(int marker) {
    System.out.println("Cup(" + marker + ")");
  }
  void f(int marker) {
    System.out.println("f(" + marker + ")");
  }
}

class Cups {
  static Cup c1;
  static Cup c2;
  static {
    c1 = new Cup(1);
    c2 = new Cup(2);
  }
  Cups() {
    System.out.println("Cups()");
  }
}

public class ExplicitStatic {
  public static void main(String[] args) {
    System.out.println("Inside main()");
    Cups.c1.f(99);  // (1)
  }
  // static Cups x = new Cups();  // (2)
  // static Cups y = new Cups();  // (2)
} ///:~

Les instructions statiques d'initialisation pour Cups sont exécutées soit quand l'accès à l'objet static c1 intervient à la ligne (1), soit si la ligne (1) est mise en commentaire et les lignes (2) ne le sont pas. Si (1) et (2) sont en commentaire, l'initialisation static pour Cups n'intervient jamais. De plus, que l'on enlève les commentaires pour les deux lignes (2) ou pour une seule n'a aucune importance : l'initialisation statique n'est effectuée qu'une seule fois.

Initialisation d'instance non statique

Java offre une syntaxe similaire pour initialiser les variables non static pour chaque objet. Voici un exemple :

//: c04:Mugs.java
// Java "Instance Initialization." (Initialisation d'instance de Java)

class Mug {
  Mug(int marker) {
    System.out.println("Mug(" + marker + ")");
  }
  void f(int marker) {
    System.out.println("f(" + marker + ")");
  }
}

public class Mugs {
  Mug c1;
  Mug c2;
  {
    c1 = new Mug(1);
    c2 = new Mug(2);
    System.out.println("c1 & c2 initialized");
  }
  Mugs() {
    System.out.println("Mugs()");
  }
  public static void main(String[] args) {
    System.out.println("Inside main()");
    Mugs x = new Mugs();
  }
} ///:~

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