UKOnline

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 et Long ;
  • Float et Double ;
  • 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.

Wrapper classes
Utilisation des classes wrappers des types primitifs.

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.

Boxing/Unboxing
Mémoire pour un programme utilisant les capacités d'auto boxing/unboxing.

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.