Primitives Java par rapport aux Objets

Aperçu

Dans ce tutoriel, nous montrons les avantages et les inconvénients de l’utilisation des types primitifs Java et de leurs homologues enveloppés.

Système de type Java

Java a un système de type double composé de primitives telles que les types int, booléens et de référence telles que Integer, Booléens. Chaque type primitif correspond à un type de référence.

Chaque objet contient une seule valeur du type primitif correspondant. Les classes wrapper sont immuables (de sorte que leur état ne peut pas changer une fois l’objet construit) et sont finales (de sorte que nous ne pouvons pas en hériter).

Sous le capot, Java effectue une conversion entre les types primitifs et de référence si un type réel est différent de celui déclaré:

Integer j = 1; // autoboxingint i = new Integer(1); // unboxing

Le processus de conversion d’un type primitif en un type de référence est appelé autoboxing, le processus opposé est appelé unboxing.

Avantages et inconvénients

La décision sur l’objet à utiliser est basée sur les performances de l’application que nous essayons d’atteindre, la quantité de mémoire disponible, la quantité de mémoire disponible et les valeurs par défaut que nous devons gérer.

Si nous ne faisons face à aucune de ces considérations, nous pouvons ignorer ces considérations bien qu’il soit utile de les connaître.

3.1. Empreinte mémoire d’élément unique

Juste pour la référence, les variables de type primitif ont l’impact suivant sur la mémoire:

  • booléen -1 bit
  • octet-8 bits
  • short, char-16 bits
  • int, float-32 bits
  • long, double– 64 bits

En pratique, ces valeurs peuvent varier en fonction de l’implémentation de la machine virtuelle. Dans la machine virtuelle d’Oracle, le type booléen, par exemple, est mappé aux valeurs int 0 et 1, il prend donc 32 bits, comme décrit ici: Types et valeurs primitifs.

Les variables de ces types vivent dans la pile et sont donc accessibles rapidement. Pour plus de détails, nous vous recommandons notre tutoriel sur le modèle de mémoire Java.

Les types de référence sont des objets, ils vivent sur le tas et sont relativement lents à accéder. Ils ont une certaine surcharge concernant leurs homologues primitifs.

Les valeurs concrètes de la surcharge sont en général spécifiques à la JVM. Ici, nous présentons des résultats pour une machine virtuelle 64 bits avec ces paramètres:

java 10.0.1 2018-04-17Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10)Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)

Pour obtenir la structure interne d’un objet, nous pouvons utiliser l’outil de disposition d’objet Java (voir notre autre tutoriel sur la façon d’obtenir la taille d’un objet).

Il s’avère qu’une seule instance d’un type de référence sur cette machine virtuelle java occupe 128 bits sauf pour Long et Double qui occupent 192 bits :

  • Booléen–128 bits
  • Octet–128 bits
  • Court, Caractère – 128 bits
  • Entier, Float–128 bits
  • Long, Double–192 bits

Nous pouvons voir qu’une seule variable de type booléen occupe autant d’espace que 128 primitives, tandis qu’une variable Entière occupe autant d’espace que quatre int.

3.2. Empreinte mémoire pour les tableaux

La situation devient plus intéressante si l’on compare la quantité de mémoire occupée par les tableaux des types considérés.

Lorsque nous créons des tableaux avec le nombre varié d’éléments pour chaque type, nous obtenons un tracé:

qui démontre que les types sont regroupés en quatre familles par rapport à la façon dont la mémoire m(s) dépend du nombre d’éléments s du tableau:

  • long, double: m(s) = 128 + 64 s
  • court, char: m(s)= 128 +64 s
  • s) = 128 +64
  • octet, booléen : m(s) = 128 +64
  • le reste: m(s) = 128 +64

où les crochets indiquent la fonction de plafond standard.

Étonnamment, les tableaux des types primitifs long et double consomment plus de mémoire que leurs classes wrapper Long et Double.

Nous pouvons voir que les tableaux à un élément de types primitifs sont presque toujours plus chers (sauf pour long et double) que le type de référence correspondant.

3.3. Performance

La performance d’un code Java est un problème assez subtil, elle dépend beaucoup du matériel sur lequel le code s’exécute, du compilateur qui pourrait effectuer certaines optimisations, de l’état de la machine virtuelle, de l’activité des autres processus du système d’exploitation.

Comme nous l’avons déjà mentionné, les types primitifs vivent dans la pile tandis que les types de référence vivent dans le tas. C’est un facteur dominant qui détermine la vitesse à laquelle les objets sont accessibles.

Pour démontrer combien les opérations pour les types primitifs sont plus rapides que celles pour les classes wrapper, créons un tableau de cinq millions d’éléments dans lequel tous les éléments sont égaux sauf le dernier; ensuite, nous effectuons une recherche pour cet élément:

while (!pivot.equals(elements)) { index++;}

et comparons les performances de cette opération pour le cas où le tableau contient des variables des types primitifs et pour le cas où il contient des objets des types de référence.

Nous utilisons le célèbre outil de benchmarking JMH (voir notre tutoriel sur la façon de l’utiliser), et les résultats de l’opération de recherche peuvent être résumés dans ce tableau:

Même pour une opération aussi simple, nous pouvons voir qu’il faut plus de temps pour effectuer l’opération pour les classes wrapper.

Dans le cas d’opérations plus compliquées comme la sommation, la multiplication ou la division, la différence de vitesse peut monter en flèche.

3.4. Valeurs par défaut

Les valeurs par défaut des types primitifs sont 0 (dans la représentation correspondante, c’est-à-dire 0, 0.0d etc.) pour les types numériques, false pour le type booléen, \u0000 pour le type char. Pour les classes wrapper, la valeur par défaut est null.

Cela signifie que les types primitifs peuvent acquérir des valeurs uniquement à partir de leurs domaines, tandis que les types de référence peuvent acquérir une valeur (null) qui, dans un certain sens, n’appartient pas à leurs domaines.

Bien qu’il ne soit pas considéré comme une bonne pratique de laisser les variables non initialisées, nous pouvons parfois attribuer une valeur après sa création.

Dans une telle situation, lorsqu’une variable de type primitif a une valeur égale à celle par défaut de son type, nous devrions savoir si la variable a vraiment été initialisée.

Il n’y a pas un tel problème avec une variable de classe wrapper car la valeur null est une indication évidente que la variable n’a pas été initialisée.

Utilisation

Comme nous l’avons vu, les types primitifs sont beaucoup plus rapides et nécessitent beaucoup moins de mémoire. Par conséquent, nous pourrions préférer les utiliser.

D’autre part, la spécification actuelle du langage Java n’autorise pas l’utilisation de types primitifs dans les types paramétrés (génériques), dans les collections Java ou l’API de réflexion.

Lorsque notre application a besoin de collections avec un grand nombre d’éléments, nous devrions envisager d’utiliser des tableaux avec le type le plus « économique” possible, comme cela est illustré sur le graphique ci-dessus.

Conclusion

Dans ce tutoriel, nous avons illustré que les objets en Java sont plus lents et ont un impact mémoire plus important que leurs analogues primitifs.

Comme toujours, des extraits de code peuvent être trouvés dans notre dépôt sur GitHub.

Commencez avec Spring 5 et Spring Boot 2, via le cours Learn Spring:

>>CONSULTEZ LE COURS

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *