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 |
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 :
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 :
Voici la sortie de ce programme :
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.
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 :
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 :
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 :
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 :
Mais pas celui-ci :
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.
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 :
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].
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 :
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 :
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 ?
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 :
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 :
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 :
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 :
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 :
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.
Java offre une syntaxe similaire pour initialiser les variables non static pour chaque objet. Voici un exemple :