Wrapper des types primitifs
Cette section s'intéresse à des classes permettant de représenter des données primitives avec des objets. Il y a donc des classes qui représentent des nombres entiers et flottants, des caractères et des booléens.
Classe wrapper
Dans le package java.lang
, on retrouve huit classes qui représentant les huit types de données primitives :
Byte
,Short
,Integer
etLong
;Float
etDouble
;Character
;Boolean
.
Toutes ces classes (sauf Character
) possèdent deux constructeurs : le premier prend en paramètre une valeur de type primitif et le second prend une chaine de caractères (un objet String
comme on verra plus loin dans ce chapitre). La classe Float
possède un constructeur supplémentaire qui prend un double
en paramètre. Voici par exemple les deux constructeurs de la classe Integer
, représentant un nombre entier :
Le premier constructeur est assez simple à comprendre : on construit un objet qui va représenter la donnée de type primitif passée en paramètre. Le second constructeur reçoit une chaine de caractères et va tenter de construire un objet à partir de celle-ci. Si la chaine de caractères ne représente pas une bonne donnée, une erreur d'exécution se produit. Par exemple, l'instruction suivante produira une erreur d'exécution de type NumberFormatException
:
En ce qui concerne le second constructeur de la classe Boolean
, l'objet créé représentera la valeur true
si la chaine de caractères est "true"
(insensible à la casse) et false
sinon. En ce qui concerne la classe Character
, elle n'a qu'un seul constructeur qui prend un char
en paramètre. Enfin, pour le second constructeur des classes Float
et Double
, rappelez-vous que le séparateur décimal est le point et non pas la virgule.
Voyons maintenant un exemple un peu plus complet :
La figure 23 montre la mémoire après exécution de ces quatre instructions. On peut voir qu'il y a une variable de type primitif, trois variables de type objet et trois objets.
Attention, il s'agit donc bien d'objets. Vous ne pouvez dès lors pas utiliser les opérateurs qui ne s'utilisent qu'avec les types primitifs pour effectuer des opérations comme des additions, soustractions, etc. L'instruction suivante provoquera une erreur de compilation de type « The operator + is undefined for the arguments type(s) Double, Integer » :
On peut dès lors se demander pourquoi ces objets existent. En fait, comme on le verra plus tard, on est parfois contraint à utiliser des objets. Dans ce cas, si on veut utiliser des données primitives, on doit les emballer dans un objet, d'où leur nom de classe wrapper. Mais lorsque vous avez le choix, préférez les types primitifs car ils seront plus rapide à manipuler et prendrons moins de place en mémoire.
Emballer/Déballer
Toutes les classes wrappers étant similaires, on va utiliser Xxx
dans la suite pour décrire les méthodes qui existent dans toutes les classes wrappers. On vient de voir que pour emballer une donnée primitive dans un objet, il suffit simplement de créer un objet Xxx
et d'utiliser la donnée primitive comme paramètre de création. Si on veut faire l'opération inverse, à savoir récupérer la donnée primitive stockée dans l'objet, on doit utiliser la méthode XxxValue
. Voyons tout de suite un exemple :
Comme vous pouvez le voir sur l'exemple, il est possible de déballer un objet Double
vers le type primitif int
(avec une conversion implicite bien entendu). Pour connaitre toutes les manières de déballer, il vous suffit de consulter la documentation des classes wrapper.
Convertir une chaine de caractères
Les classes wrappers proposent également des méthodes de classe, parmi lesquelles on retrouve deux méthodes qui permettent de convertir une chaine de caractères vers un type primitif.
Ces méthodes sont parseXxx
et valueOf
qui prennent toutes deux un objet String
en paramètre et renvoient un type primitif correspondant Xxx
pour parseXxx
et un objet wrapper pour valueOf
. Ces méthodes génèrent une erreur d'exécution de type NumberFormatException
si la chaine de caractères ne représente pas une donnée primitive valide.
Au lieu d'utiliser la méthode valueOf
, vous pouvez bien entendu directement construire l'objet puis utiliser la méthode XxxValue
. On pourrait donc écrire :
Remarquez par ailleurs qu'il s'agit d'un exemple où on a créé un objet sans en stocker une référence dans une variable. La seule raison d'être de l'objet de d'appeler sa méthode floatValue
afin de transformer la chaine de caractères "123"
en un float
.
Enfin, sachez qu'il n'y a pas de méthode parseChar
dans la classe Character
. De plus, sa méthode valueOf
prend un paramètre de type char
au lieu d'un String
en paramètre.
Comparaison
Une autre conséquence de l'utilisation d'objets à la place de types primitifs est que vous ne pouvez bien entendu plus les comparer avec ==
si vous souhaitez comparer leurs états, mais vous devez absolument utiliser la méthode equals
qu'on a vue à la section 4.2 :
Classes Byte
, Short
, Integer
et Long
Les classes Byte
, Short
, Integer
et Long
proposent plusieurs méthodes pour manipuler les entiers ainsi que la représentation binaire de ces entiers. On y retrouve également des constantes qui peuvent être utiles.
Comme vous vous en rappelez sans doute, on ne peut pas représenter tous les nombres existants avec les types primitifs puisqu'ils sont codés sur un certain nombre de bits. Le nombre de bits et les valeurs minimales et maximales représentables sont disponibles via les variables de classe SIZE
, MIN_VALUE
et MAX_VALUE
.
L'exécution de ce programme affiche ce qui suit à l'écran, ce qui correspond bien à ce qu'on a vu au premier chapitre :
Les int sont codés sur 32 bits Plus petit int : -2147483648 Plus grand int : 2147483647
En ce qui concerne les méthodes, citons-en quelques unes qui permettent de manipuler les bits d'un nombre : lowestOneBit
, reverseBytes
, rotateLeft
, rotateRigth
, toBinaryString
, etc.
Il est possible d'obtenir la représentation binaire, octale et hexadécimale pour les objets Integer
et Long
en utilisant les méthodes de classe toBinaryString
, toOctalString
et toHexString
qui prennent respectivement un int
et un long
en paramètre. Enfin, pour les quatre classes, sachez qu'il existe une seconde version de la méthode valueOf
qui prend deux paramètres : le premier est la chaine de caractères à convertir et le second est la base. L'exemple suivant calcule la représentation binaire d'un nombre entier, l'affiche à l'écran et reconvertit ensuite ce nombre en un Long
.
Classes Float
et Double
On retrouve aussi les constantes SIZE
, MIN_VALUE
et MAX_VALUE
dans les classes Float
et Double
. Mais on y retrouve en plus des constantes qui représentent des valeurs spéciales propres aux nombres flottants : NaN
, POSITIVE_INFINITY
et NEGATIVE_INFINITY
. Ces valeurs représentent respectivement NotANumber et les infinis positif ($+\infty$) et négatif ($-\infty$). Voici un exemple qui produit de telles valeurs :
Pour tester si un nombre vaut un des infinis ou NaN, il faut passer par les méthodes isInfinite
et isNaN
qui existent comme méthodes d'instance et de classe. Avec les entiers, diviser par zéro provoque une erreur d'exécution tandis qu'avec les réels, c'est permis et on obtient l'infini.
Puisque le numérateur est positif, l'exécution de ce programme affiche à l'écran :
Le résultat est l'infini positif
L'appel de méthode Double.isInfinite (r)
peut être remplacé par l'appel r.isInfinite()
. Notez que vous ne pouvez pas comparer un double
avec la constante Double.NaN
pour tester si sa valeur vaut NaN, vous devez passez par la méthode de classe isNaN
.
Classe Character
Au chapitre 1, on a vu que les identificateurs étaient formés de lettres et chiffres Java. Mais comment savoir ce qu'est une lettre et un chiffre Java ? La classe Character
possède deux méthodes de classe qui répondent à cette question : isJavaIdentifierStart
et isJavaIdentifierPart
. Ces méthodes prennent un caractère en paramètre et renvoie un boolean
qui indique respectivement si le caractère peut être ou non utilisé comme première lettre ou comme autre lettre d'un identificateur. Par exemple, si on veut savoir si la lettre æ peut être utilisée pour former un identificateur valide, on va faire :
Voici ce que l'exécution du programme affiche à l'écran :
Je peux utiliser æ pour former un identificateur.
La classe Character
possède une méthode digit
qui permet de convertir un caractère en un entier de type int
, une méthode isDigit
qui permet de tester si un caractère représente un chiffre et une méthode isLetter
qui teste si un caractère est une lettre. On peut tester si un caractère est une minuscule ou une majuscule avec les méthodes isLowerCase
et isUpperCase
et on peut transformer un caractère en minuscule avec toLowerCase
et en majuscule avec toUpperCase
. Enfin, la méthode isWhiteSpace
permet de tester si un caractère est un blanc (au sens de Java).
Classe Boolean
La classe Boolean
ne possède pas de méthodes supplémentaires par rapport à celles qu'on a déjà rencontrées. Il y a également deux constantes de classe qui sont des objets Boolean
représentant les valeurs true
et false
:
Auto Boxing/Unboxing
Une nouvelle fonctionnalité disponible depuis Java 5 est l'auto boxing/unboxing. Cette fonctionnalité simplifie l'emballage/déballage des données primitives dans des objets. Il s'agit essentiellement de code qui est ajouté de manière implicite par le compilateur. Commençons avec un exemple :
Que s'est-il passé ? On commence par affecter la donnée primitive 34 à une variable de type objet. Ceci n'est bien entendu pas correct mais l'auto boxing a en fait transformé l'instruction en :
L'auto boxing consiste donc à créer une variable de type objet qui va emballer la donnée primitive. Voyons maintenant la seconde instruction, on additionne une variable contenant une référence vers un objet avec un entier primitif de type int
. Encore une fois ce n'est pas correct, mais l'auto unboxing a en fait transformé l'instruction en :
L'auto unboxing va donc récupérer la donnée primitive stockée dans l'objet par un appel à une des méthodes XxxValue
. Comme on le verra plus tard, cette fonctionnalité s'avère très pratique pour rendre le code plus léger et agréable à lire, mais il faut faire attention lors de certaines situations.
La première chose à garder à l'esprit est que l'auto boxing contribue à créer de nombreux objets, ce qui va donc prendre du temps et occuper de l'espace mémoire. Prenons par exemple les instructions suivantes qui incrémentent un nombre entier de un :
On pourrait croire que la seconde instruction va changer l'état de l'objet référencé par la variable i
. Ce n'est en fait pas le cas. L'objet référencé par i
est déballé, ensuite l'opérateur d'incrémentation est appliqué, et enfin la donnée primitive est remballée. Voici donc ce qui se passe réellement :
La figure 24 montre la mémoire avant et après exécution de i++
. On voit donc bien qu'il y a deux objets dans la mémoire au final. Un de ces deux objets n'est plus référencé par aucune variable, c'est un orphelin. On verra dans le chapitre suivant qu'ils sont automatiquement traités par la machine virtuelle Java.
Un autre point qui peut porter à confusion concerne l'utilisation de ==
, !=
et equals
. La méthode equals
ne pose aucun problème, elle permet de comparer l'état de deux objets. On peut donc l'utiliser pour vérifier que deux valeurs sont les mêmes. La méthode renvoie true
si les deux objets ont le même type et les mêmes valeurs.
L'exécution de ce programme affiche donc deux fois false
à l'écran puisque, malgré que les valeurs sont les mêmes (12), les types des objets sont différents. Qu'en est-il des opérateurs ==
et !=
? Ils permettent de vérifier si les variables réfèrent vers les mêmes objets. L'exemple suivant affiche false
à l'écran puisque deux objets différents sont créés.
Pour que les opérateurs ==
et !=
puissent fonctionner, il faut les deux opérandes soient exactement du même type sans quoi une erreur de compilation de type « Incompatible operand types XXX and YYY » se produit. Voici un exemple qui produit une telle erreur :
Par contre, pour les types Boolean
, Byte
, Character
(pour les valeurs comprises entre \u0000
et \u007f
) et Integer
(pour les valeurs comprises entre -128 et 127), il faut savoir que ==
et !=
se comportent autrement. En fait, il n'existe qu'une seule copie de tous ces objets, Java s'en assure via un système de cache. L'exemple suivant affiche donc true
à l'écran :
On comprendra le fonctionnement de ce système de cache lors du chapitre suivant. Enfin, lorsqu'on compare un objet wrapper avec un type primitif, l'objet wrapper est d'abord déballé et la comparaison se fait ensuite.