Quelques rappels
Ainsi qu'il a été dit précédemment, la déclaration d'une variable
de référence (c'est à dire correspondant à un type
non primitif) ne réalise aucune allocation d'objet correspondant
à ce type. Une variable d'instance d'un tel type a comme valeur par défaut
null.
On peut lui affecter la référence d'un objet de type «compatible»
avec le type auquel elle a été associée lors de sa définition
(typiquement le type correspondant à la variable ou une classe dérivée)
et en particulier le résultat d'une instanciation d'un objet
(compatible) réalisée avec l'opérateur
new
appliqué à un constructeur de ce type.
Les constructeurs
Le constructeur par défaut
Le langage fournit pour chaque classe un mécanisme d'instanciation
par défaut qui, outre la création de l'objet (instance)
attribue une valeur par défaut à chaque
variable d'instance, qu'elle soit d'un type primitif
(voir les valeurs par défaut) ou
de référence (sa valeur est alors
null).
Cette instanciation par défaut est réalisée explicitement pour une classe
de nom C
(si le constructeur par défaut y a été redéfini, c'est cette nouvelle
définition qui sera utilisée, comme nous le verrons dans la suite)
par l'expression
new C( )
dont la valeur est donc une référence sur la nouvelle instance créée.
Il faut noter dès maintenant que si la classe est
sous-classe directe d'une classe
SurC,
c'est-à-dire dont la définition correspond à :
class C extends SurC
le constructeur par défaut de cette sur-classe sera préalablement
appelé.
Ainsi que nous le verrons, il est possible de définir et invoquer
d'autres constructeurs que celui-ci, voire le redéfinir lui-même,
mais
- un constructeur de la sur-classe sera toujours appelé, puisqu'une
instance de
C
encapsule effectivement une instance de
SurC ;
- le constructeur par défaut (même s'il est masqué par une redéfinition)
sera appelé, ce qui garantira que toutes les variables d'instance seront effectivement initialisées.
--> cat Construct1.java
class X1 {
int a;
int b = 10;
}
class X2 extends X1{
int c;
X1 x1;
}
class Construct1 {
public static void main(String[ ] arg){
X1 x1 = new X1( ); // constructeur par défaut de X1
System.out.println(x1.a + " " + x1.b);
X2 x2 = new X2( ); // constructeur par défaut de X2
System.out.println(x2.a + " " + x2.b + " " + x2.c + " " + x2.x1);
}
}
--> javac Construct1.java
--> java Construct1
0 10
0 10 0 null
-->
|
Les constructeurs spécifiques illustrés par des exemples
Le type d'instanciation minimale fournie au travers du constructeur par défaut
ne convient bien évidemment pas nécessairement aux besoins spécifiques de
nombreuses classes
qui supposent par exemple la possibilité de paramétrer l'initialisation
des variables d'instance.
Aussi est-il possible de définir dans une classe des constructeurs
appropriés.
Un constructeur a les caractéristiques suivantes :
- il ne spécifie pas de type de valeur de retour
(même pas
void)
- il a exactement le même nom que la classe ;
Ces deux caractéristiques font de la séquence correspondante
un constructeur qui ne sera appelable qu'au travers de l'opérateur
new
pour instancier la classe correspondante.
Il est important de noter que
dès qu'un constructeur est défini
par l'utilisateur, le constructeur par défaut n'est plus utilisable
explicitement (il est néanmoins appelé implicitement avant le constructuer explicitement spécifié).
Dans le
premier exemple donné ci-après
--> cat Construct2.java
class X1 {
int a, b;
X1(int a, int b){
this.a = a; // this fait référence à l'objet en construction
this.b = b; // pour distinguer la variable a du paramètre a
}
}
class X2 extends X1{
int c;
X1 x1;
}
class Construct2 {
public static void main(String[ ] arg){
X1 x1 = new X1(10, 20); // constructeur spécifique X1(int,int)
System.out.println(x1.a + " " + x1.b);
X2 x2 = new X2(); // constructeur par défaut de X2
System.out.println(x2.a + " " + x2.b + " " + x2.c + " " + x2.x1);
}
}
--> javac Construct2.java
Construct2.java:8: cannot resolve symbol
symbol : constructor X1 ()
location: class X1
class X2 extends X1{
^
1 error
-->
|
on notera
- la définition d'un constructeur
X1(int,int) pour la classe
X1 ;
- l'utilisation du mot clé
this pour faire référence
à l'objet courant. C'est ici indispensable pour définir un contexte
d'interprétation des identificateurs
a et
b
(s'agit-il de la variable d'instance ou du paramètre du constructeur ?) ;
- l'erreur rencontrée à la compilation car lorsque le constructeur par défaut
de
X2 sera invoqué
(il n'y a pas de constructeur spécifique défini pour
cette classe), le constructeur par défaut de
X1
devra être invoqué alors qu'il y a été rendu non utilisable
par la définition du constructeur spécifique
X1(int,int) de la
classe X1.
Dans ce
second exemple, nous avons défini un constructeur
pour la classe
X2
et avons ajouté un second constructeur (de profil semblable à
celui d'un constructeur par défaut, c'est-à-dire sans paramètre)
pour la classe
X1.
Cette possibilité de donner plusieurs définitions d'une fonction,
chacune avec un profil différent est ce qu'on appelle
la
surcharge
(ici on peut
éventuellement considérer qu'on est également dans un cas
de
redéfinition
du constructeur par défaut, encore que sa définition ne soit en fait
que masquée) :
--> cat Construct3.java
class X1 {
int a, b;
X1( ){ } // constructeur vide ==> a = 0 et b = 0
X1(int a, int b){
this.a = a;
this.b = b;
}
}
class X2 extends X1{
int c;
X1 x1;
X2(int c) {this.c = c; }
}
class Construct3 {
public static void main(String[ ] arg){
X1 x1 = new X1(10, 20); // constructeur spécifique X1(int,int)
System.out.println(x1.a + " " + x1.b);
X2 x2 = new X2(30); // constructeur spécifique X2(int)
System.out.println(x2.a + " " + x2.b + " " + x2.c + " " + x2.x1);
}
}
--> javac Construct3.java
--> java Construct3
10 20
0 0 30 null // le constructeur X1() a été appelé
-->
|
Dans la
dernière variation, le constructeur de la classe
X2 qui y est défini
reçoît en paramètres trois entiers :
- les deux premiers sont destinés
- à initialiser l'instance de
X1 encapsulée dans
l'instance de X2
du fait du sous-classage. On notera que le constructeur de la classe mère
est invoqué en utilisant symboliquement l'identificateur
super ;
- à créer l'instance de
X1
correspondant à la variable
x1
- le troisième est utilisé pour initialiser la variable spécifique
c.
--> cat Construct4.java
class X1 {
int a, b;
X1(int a, int b){
this.a = a;
this.b = b;
}
}
class X2 extends X1{
int c;
X1 x1;
X2(int a, int b, int c) {
super(a,b); this.c = c; x1 = new X1(a+1,b+1); }
}
class Construct4 {
public static void main(String[ ] arg){
X2 x2 = new X2(10, 20, 30); // constructeur spécifique X2(int,int,int)
System.out.println(x2.a + " " + x2.b + " " + x2.c + " " + x2.x1);
if (x2.x1 != null)
System.out.println(x2.x1.a + " " + x2.x1.b);
}
}
--> javac Construct4.java
--> java Construct4
10 20 30 X1@ea2dfe
11 21
-->
|
On notera au passage la forme sous laquelle l'affichage de la valeur de la
variable
x1
est réalisée : il s'agit d'une forme «brute de fonderie»
(
X1@ea2dfe)
contenant la classe de l'objet référencé et sa référence :
toutes les variables de type référence ont des valeurs de cette forme
correspondant à la définition de la méthode d'instance
toString dans la classe
Object, classe dont toutes
les classes descendent plus ou moins directement.
Un affichage non générique reflétant la structure d'une classe particulière
suppose de
redéfinir effectivement la méthode
publique
toString pour cette classe concernée.
Pour la classe
X1
une redéfinition possible de la méthode
toString
serait la suivante qui visualise entre crochets les valeurs des deux
variables
a
et
b
de l'objet concerné :
class X1 {
int a, b;
.......
String toString(){
return "[" + a + "," + b +"]";
}
........
}
|
l'exécution donnant alors les résultats suivants :
--> java Construct4
10 20 30 [11,21]
11 21
-->
|