• Aucun résultat trouvé

Nous avons dit au chapitre précédent que tout, en Java, était objet, c'est-à-dire instance de classes. En fait, ce n'est pas tout à fait vrai. Comme nous venons de le voir, les objets sont toujours créés dans une zone de mémoire dont les performances ne sont pas optimales. Java possède une classe per-mettant de créer des objets de type "nombre entier". Il s'agit de la classe Integer. Si nous voulons effectuer une même opération 1000 fois, par exem-ple afficher un compteur comptant le nombre de fois que l'opération est effectuée, nous pouvons le faire de la façon suivante :

• Créer un objet nombre entier et lui donner la valeur 0.

• Tant que la valeur du nombre entier est différente de 1000, augmenter cette valeur de 1 et l'afficher.

D’une part, utiliser pour cela un objet de type Integer (la classe Java utili-sée pour représenter les nombres entiers) serait particulièrement peu effi-cace, car il faut accéder 1 000 fois à cet objet. D'autre part, cet objet ne sert à rien d'autre qu'à compter le nombre d'itérations et à afficher sa valeur. Et encore s'agit-il là d'un exemple dans lequel la valeur de la variable est utili-sée pour l'affichage. Dans la plupart des exemples réels, ce type de variable ne sert que de compteur. Aucune des caractéristiques spécifiques d'un objet n'est utilisée ici. Il en est de même pour la plupart des calculs numériques.

Les concepteurs de Java ont donc doté ce langage d'une série d'éléments particuliers appelés primitives. Ces éléments ressemblent à des objets, mais ne sont pas des objets. Ils sont créés de façon différente, et sont également manipulés en mémoire de façon différente. Cependant, ils peuvent être en-veloppés dans des objets spécialement conçus à cet effet, et appelés enveloppeurs (wrappers).

Java dispose des primitives suivantes :

Primitive Étendue Taille

char 0 à 65 535 16 bits

byte -128 à +127 8 bits

short -32 768 à +32 767 16 bits

int - 2 147 483 648 à + 2 147 483 647 32 bits

long de - 263 à (+ 263 - 1), soit

de - 9 223 372 036 854 775 808

à + 9 223 372 036 854 775 807 64 bits

float de ± 1.4E-45 à ± 3.40282347E38 32 bits

double de ± 4.9E-324 à ± 1.7976931348623157E308 64 bits

boolean true ou false 1 bit

void - 0 bit

Java dispose également des classes suivantes pour envelopper les primiti-ves :

Classe Primitive

Character char

Byte byte

Short short

Integer int

Long long

Classe Primitive

Float float

Double double

Boolean boolean

Void void

BigInteger

-BigDecimal

-Les classes BigInteger et BigDecimal sont utilisées pour représenter res-pectivement des valeurs entières et décimales de précision quelconque. Il n'existe pas de primitives équivalentes.

Notez que le type char, servant à représenter les caractères, est un type non signé sur 16 bits, conformément au standard UNICODE. (Il n'existe pas d'autre type numérique non signé.) En revanche, contrairement à l'usage dans d'autres langages, le type boolean n'est pas un type numérique.

Note : A l’inverse de ce qui se passe avec les autres langages, la taille des différentes primitives est toujours la même en Java, quel que soit l'environ-nement. Sur tous les ordinateurs, du plus petit PC au super-calculateur, un int ou un float feront toujours 32 bits et un long ou un double feront tou-jours 64 bits. C'est une des façons d'assurer la portabilité des programmes.

Utiliser les primitives

Les primitives sont utilisées de façon très simple. Elles doivent être décla-rées, tout comme les handles d'objets, avec une syntaxe similaire, par exemple :

int i;

char c;

double valeurFlottanteEnDoublePrécision;

boolean fini;

Comme les handles d'objets, il n'est pas nécessaire de leur affecter une va-leur avant de les utiliser. Elles doivent donc être initialisées. Si vous n'ini-tialisez pas une primitive, vous obtiendrez un message d'erreur. Par exem-ple, la compilation du programme suivant :

public class primitives {

public static void main(String[] argv) { int i;

char c;

double valeurFlottanteEnDoublePrécision;

boolean fini;

System.out.println(i);

System.out.println(c);

System.out.println(valeurFlottanteEnDoublePrécision);

System.out.println(fini);

} }

produit quatre messages d'erreur :

primitives.java:7: Variable i may not have been initialized.

System.out.println(i);

^

primitives.java:8: Variable c may not have been initialized.

System.out.println(c);

^

primitives.java:9: Variable valeurFlottanteEnDoublePrécision may not have been initialized.

System.out.println(valeurFlottanteEnDoublePrécision);

^

primitives.java:10: Variable fini may not have been initialized.

System.out.println(fini);

^ 4 errors

Pour initialiser une primitive, on utilise le même opérateur que pour les objets, c'est-à-dire le signe =. Cette utilisation du signe égal est moins cho-quante que dans le cas des objets, car il s'agit là de rendre la variable égale à une valeur, par exemple :

int i;

char c;

double valeurFlottanteEnDoublePrécision;

boolean fini;

i = 12;

c = "a";

valeurFlottanteEnDoublePrécision = 23456.3456;

fini = true;

Comme dans le cas des handles d'objets, il est possible d'effectuer la décla-ration et l'initialisation sur la même ligne :

int i = 12;

char c = "a";

double valeurFlottanteEnDoublePrécision = 23456.3456;

boolean fini = true;

Il faut noter que le compilateur peut parfois déduire du code une valeur d'initialisation, mais c'est assez rare. Considérez l'exemple suivant :

class Initialisation {

public static void main(String[] args) { int b;

if (true) { b = 5;

}

System.out.println(b);

} }

Nous n'avons pas encore étudié l'instruction if, mais sachez simplement qu'elle prend un argument placé entre parenthèses. Si cet argument est vrai, le bloc suivant est exécuté. Dans le cas contraire, il est ignoré. Ici, si true est vrai, la variable b prend la valeur 5. Or, true est toujours vrai. (true signifie vrai.)

Le compilateur est suffisamment intelligent pour s'en apercevoir. Ce code est donc compilé sans erreur. En revanche, si vous modifiez le programme de la façon suivante :

class Initialisation {

public static void main(String[] args) { int b;

boolean a = true;

if (a) { b = 5;

}

System.out.println(b);

} }

le compilateur ne s'y retrouve plus et produit une erreur. Certains auteurs recommandent de toujours initialiser les variables au moment de leur dé-claration afin d'éviter les erreurs. Ce n'est pas, à notre avis, un bon conseil.

Il est évident qu'il faut initialiser les variables si leur valeur d'initialisation est connue. En revanche, si elle doit être le résultat d'un calcul, il est préfé-rable de ne pas les initialiser (à 0, par exemple, pour les valeurs numéri-ques) avant que le calcul soit effectué. En effet, si pour une raison ou une autre, vous oubliez d'initialiser une variable qui n'a pas été initialisée à 0, l'erreur sera détectée à la compilation. En revanche, si vous initialisez la variable à 0, le programme sera compilé sans erreur. Il est également tout à

fait possible qu'il ne produise pas d'erreur d'exécution. Simplement, le pro-gramme risque de donner un résultat incohérent (c'est un moindre mal) ou simplement faux. Ce type d'erreur peut parfaitement échapper à votre vigi-lance. Si, par chance, vous effectuez les vérifications qu'il faut pour vous apercevoir qu'il y a une erreur, rien ne vous indiquera d'où elle provient.

Vous pouvez passer de longues heures de débogage, parfois après que votre programme aura été distribué à des millions d'exemplaires (on peut rêver), alors qu'en ayant pris soin de ne pas initialiser la variable, l'erreur aurait été détectée immédiatement.

Valeurs par défaut des primitives

Nous venons de voir que les primitives devaient être initialisées. Cela sem-ble indiquer qu'elles n'ont pas de valeur par défaut. En fait, elles en reçoi-vent une dans certains cas. Lorsque des primitives sont utilisées comme membres d'une classe, et dans ce cas seulement, elles reçoivent une valeur par défaut au moment de leur déclaration. Pour le vérifier, vous pouvez compiler et exécuter le programme suivant :

public class primitives2 {

public static void main(String[] argv) { PrimitivesInitialiseesParDefaut pipd =

new PrimitivesInitialiseesParDefaut();

System.out.println(pipd.i);

System.out.println((int)pipd.c);

System.out.println(pipd.valeurFlottanteEnDoublePrécision);

System.out.println(pipd.fini);

} }

class PrimitivesInitialiseesParDefaut { int i;

char c;

double valeurFlottanteEnDoublePrécision;

boolean fini;

}

Vous pouvez remarquer trois choses intéressantes dans ce programme. La première est que, pour accéder à un membre d'un objet, nous utilisons le nom de cet objet, suivi d'un point et du nom du membre. Il arrive souvent qu'un objet possède des membres qui sont eux-mêmes des objets possédant des membres, etc. Il suffit alors d'ajouter à la suite les noms des différents objets en les séparant à l'aide d'un point pour accéder à un membre. Par exemple, si l'objet rectangle contient un membre appelé dimensions qui est lui-même un objet contenant les membres hauteur et largeur qui sont des primitives, on peut accéder à ces primitives en utilisant la syntaxe :

rectangle.dimensions.hauteur

En revanche, si l'objet rectangle contient directement deux membres de type primitives appelés hauteur et largeur, on pourra y accéder de la façon suivante :

rectangle.hauteur

Note : Dans ce type d'expression, les références sont évaluées de gauche à droite, c'est-à-dire que :

alpha.bêta.gamma.delta

est équivalent à :

((alpha.bêta).gamma).delta

Il est parfaitement possible d'utiliser des parenthèses pour modifier l'ordre d'évaluation des références.

La deuxième chose qu'il est intéressant de noter est l'utilisation de (int) à la sixième ligne du programme. Nous reviendrons sous peu sur cette techni-que très importante, qui sert ici à transformer la valeur de type caractère en type numérique afin de pouvoir l'afficher. (Nous avons dit que le type char était un type numérique, ce qui est vrai, mais il est affiché par

System.out.println sous la forme du caractère UNICODE correspondant à sa valeur. Le code 0 ne correspond pas à un caractère affichable. Le résultat obtenu n'est donc pas significatif. C'est pourquoi nous affichons ici le code du caractère et non le caractère lui-même.)

La troisième chose intéressante dans ce programme est que nous n'avons pas fourni de constructeur pour la classe PrimitivesInitialiseesParDefaut.

Ce n'est pas un problème, car dans ce cas, Java utilise un constructeur par défaut, qui ne fait rien.

Vous pouvez constater que notre programme est compilé sans erreur. Son exécution produit le résultat suivant :

0 0 0.0 false

Nos primitives ont donc bien été initialisées par défaut ! Toutes les primiti-ves de type numérique utilisées comme membres d'un objet sont initiali-sées à la valeur 0. Le type boolean est initialisé à la valeur false qui, rappe-lons-le, ne correspond à aucune valeur numérique.

Différences entre les objets et les primitives

Nous allons maintenant pouvoir approfondir les différences qui existent entre les objets et les primitives. Nous utiliserons pour cela un programme manipulant des primitives de type int et des objets de type Entier. Entier est un enveloppeur, c'est-à-dire une classe que nous créerons pour envelop-per une primitive dans un objet. Saisissez le programme suivant :

public class primitives3 {

public static void main(String[] argv) { System.out.println("Primitives :");

int intA = 12;

System.out.println(intA);

int intB = intA;

System.out.println(intB);

intA = 48;

System.out.println(intA);

System.out.println(intB);

System.out.println("Objets :");

Entier entierA = new Entier(12);

System.out.println(entierA);

Entier entierB = entierA;

System.out.println(entierB);

entierA.valeur = 48;

System.out.println(entierA);

System.out.println(entierB);

} }

class Entier { int valeur;

Entier(int v){

valeur = v;

}

public String toString(){

return ("" + valeur);

} }

Compilez et exécutez ce programme. Vous devez obtenir le résultat sui-vant :

Primitives : 12

12 48 12

Objets : 12 12 48 48

Examinons tout d'abord la classe Entier. Celle-ci comporte une variable de type int. Il s'agit de la primitive à envelopper. Elle comporte également un constructeur, qui prend pour paramètre une valeur de type int et initialise le champ valeur à l'aide de ce paramètre. Elle comporte enfin une méthode de type String qui ne prend pas de paramètres et retourne une représentation du champ valeur sous forme de chaîne de caractères, afin qu'il soit possible de l'afficher. La syntaxe utilisée ici est un peu particulière. Elle consiste à utiliser l'opérateur + avec une chaîne de caractères de longueur nulle d'un côté, et le champ valeur de l'autre, ce qui a pour conséquence de forcer Java à convertir valeur en chaîne de caractères et de l'ajouter à la suite de la chaîne de longueur nulle. Le résultat est donc une chaîne représentant va-leur. Nous reviendrons en détail sur cette opération dans la section consa-crée aux chaînes de caractères.

Venons-en maintenant à la procédure main de la classe primitives3. Tout d'abord, nous affichons un message indiquant que nous traitons des primi-tives :

System.out.println("Primitives :");

puis nous créons une variable de type int, que nous initialisons avec la valeur littérale 12 :

int intA = 12;

Nous affichons immédiatement sa valeur à la ligne suivante :

System.out.println(intA);

Le programme affiche donc 12.

Nous créons ensuite une nouvelle variable de type int et nous l'initialisons à l'aide de la ligne :

int intB = intA;

ce qui attribue à intB la valeur de intA. Nous affichons ensuite la valeur de intB :

System.out.println(intB);

et obtenons naturellement 12. Puis nous modifions la valeur de intA en lui attribuant une nouvelle valeur littérale, 48. Nous affichons ensuite intA et intB :

intA = 48;

System.out.println(intA);

System.out.println(intB);

Nous constatons que intA vaut bien maintenant 48 et que intB n'a pas changé et vaut toujours 12.

Ensuite, nous faisons la même chose avec des objets de type Entier. Nous affichons tout d'abord un message indiquant qu'à partir de maintenant, nous traitons des objets :

System.out.println("Objets :");

puis nous créons un objet, instance de la classe Entier :

Entier entierA = new Entier(12);

Le handle entierA est déclaré de type Entier, un nouvel objet est instancié par l'opérateur new et il est initialisé par le constructeur Entier() en

utili-sant la valeur littérale 12 pour paramètre. Le champ valeur de cet objet contient donc la valeur entière 12.

Nous le vérifions immédiatement à la ligne suivante en affichant l'objet entierA :

System.out.println(entierA);

Lorsque l'on essaie d'afficher un objet, Java exécute simplement la mé-thode toString() de cet objet et affiche le résultat. Ici, le programme affiche donc 12.

Nous créons ensuite un nouveau handle d'objet de type Entier et nous l'ini-tialisons à l'aide de la ligne :

Entier entierB = entierA;

Puis nous affichons l'objet entierB :

System.out.println(entierB);

ce qui affiche 12.

A la ligne suivante, nous modifions la valeur de entierA, puis nous affi-chons les valeurs de entierA et entierB.

entierA.valeur = 48;

System.out.println(entierA);

System.out.println(entierB);

Nous constatons alors que entierA a bien pris la valeur 48, mais que entierB a également pris cette valeur.

Au point où nous en sommes, cela ne devrait pas vous surprendre. En effet, la différence entre objet et primitive prend toute sa signification dans les lignes :

int intB = intA;

et :

Entier entierB = entierA;

La première signifie :

"Créer une nouvelle primitive de type int appelée intB et l'initialiser avec la valeur de intA."

alors que la deuxième signifie :

"Créer un nouveau handle de type Entier appelé entierB et le faire pointer vers le même objet que le handle entierA."

Dans le premier cas, nous créons une nouvelle primitive, alors que dans le second, nous ne créons pas un nouvel objet, mais seulement un nouveau

"nom" qui désigne le même objet. Puisqu'il n'y a qu'un seul objet, il ne peut avoir qu'une seule valeur. Par conséquent, si nous changeons la valeur de l'objet vers lequel pointe le handle entierA, nous changeons aussi la valeur de l'objet pointé par entierB, puisqu’il s'agit du même !

Pour simplifier, et bien que cela ne soit pas la réalité, vous pouvez considé-rer que le handle n'est qu'une étiquette pointant vers un objet qui peut avoir une valeur (c'est vrai), alors que la primitive est une valeur (en fait, c'est faux, mais cela peut être considéré comme vrai dans une certaine mesure du point de vue du programmeur Java).

A partir de maintenant, nous utiliserons de façon générique le terme de variable pour faire référence à des primitives ou à des handles d'objets.

Vous devrez bien garder à l'esprit le fait que deux variables différentes auront des valeurs différentes, alors que deux handles différents pourront éven-tuellement pointer vers le même objet.

Les valeurs littérales

Nous avons vu au chapitre précédent qu'il pouvait exister des objets anony-mes, c'est-à-dire vers lesquels ne pointe aucun handle. Existe-t-il égale-ment des primitives anonymes ? Pas exacteégale-ment, mais l'équivalent (très approximatif) : les valeurs littérales. Alors que les primitives sont créées et stockées en mémoire au moment de l'exécution du programme (leur valeur n'est donc pas connue au moment de la compilation), les valeurs littérales sont écrites en toutes lettres dans le code source du programme. Elles sont donc traduites en bytecode au moment de la compilation et stockées dans le fichier .class produit par le compilateur. Elles ne peuvent évidemment être utilisées qu'à l'endroit de leur création, puisqu'il n'existe aucun moyen d'y faire référence. (Elles présentent une différence fondamentale avec les ob-jets anonymes qui, eux, n'existent que lors de l'exécution du programme, et non lors de la compilation.)

Des valeurs littérales correspondent à tous les types de primitives. Pour les distinguer, on utilise cependant un procédé totalement différent. Le tableau ci-dessous donne la liste des syntaxes à utiliser :

Primitive Syntaxe

char 'x'

'\--' où -- représente un code spécifique :

\b arrière (backspace)

\f saut de page (form feed)

\n saut de ligne (new line)

\r retour chariot (carriage return)

\t tabulation horizontale (h tab)

\\ \ (backslash)

\' guillemet simple

\" guillemet double

\0oo caractère dont le code est oo en octal (Le 0 est facultatif.)

\uxxxx caractère dont le code Unicode est xxxx (en hexadécimal)

Primitive Syntaxe

int 5 (décimal), 05 (octal), 0x5 (hexadécimal) long 5L, 05L, 0x5L

float 5.5f ou 5f ou 5.5E5f

double 5.5 ou 5.5d ou 5.5E5 ou 5.5E5 boolean false ou true

Notes : Les valeurs flottantes ne peuvent être exprimées qu'en décimal.

Ces syntaxes ne présentent un intérêt que lorsqu'il y a une ambiguïté possi-ble. Ainsi, si vous écrivez :

long i = 55;

il n'y a aucune ambiguïté et il n'est pas utile d'ajouter le suffixe L. Par ailleurs, vous pouvez écrire :

byte i = 5;

short j = 8;

Bien que les valeurs littérales utilisées ici soient de type int, cela ne pose pas de problème au compilateur, qui effectue automatiquement la conver-sion. Nous verrons plus loin par quel moyen. (Notez, au passage, que les littéraux sont évalués lors de la compilation, de façon à éviter au maximum les erreurs lors de l'exécution.)

Vous ne pouvez cependant pas écrire :

byte i = 300;

car la valeur littérale utilisée ici est supérieure à la valeur maximale qui peut être représentée par le type byte.

Vous pouvez écrire :

float i = 5

car 5 est de type int et le compilateur est capable de le convertir automati-quement en type float. En revanche, vous ne pouvez pas écrire :

float j = 5.5

car 5.5 est de type double. Dans ce cas, Java n'effectue pas automatique-ment la conversion, pour une raison que nous verrons plus loin.