Primitives Java versus Objets

Primitives Java vs Objets

1. Vue d'ensemble

Dans ce didacticiel, nous montrons les avantages et les inconvénients de l’utilisation des types primitifs Java et de leurs équivalents encapsulés.

2. Système de type Java

Java a un système de type à deux volets composé de primitives telles queint,boolean et de types de référence tels queInteger,Boolean. Chaque type de primitive correspond à un type de référence.

Chaque objet contient une valeur unique du type primitif correspondant. Leswrapper classes are immutable (pour que leur état ne puisse pas changer une fois l’objet construit) et sont définitifs (pour que nous ne puissions pas en hériter).

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

Integer j = 1;          // autoboxing
int i = new Integer(1); // unboxing

Le processus de conversion d'un type primitif en un type de référence s'appelle la substitution automatique, le processus opposé s'appelle unboxing.

3. Avantages et inconvénients

Le choix de l'objet à utiliser dépend des performances de l'application que nous essayons d'atteindre, de la quantité de mémoire disponible, de la quantité de mémoire disponible et des valeurs par défaut que nous devons gérer.

Si nous ne sommes confrontés à aucun de ces problèmes, nous pouvons ignorer ces considérations, même s’il vaut la peine de les connaître.

3.1. Empreinte mémoire unique

Juste pour référence, lesprimitive type variables ont l'impact suivant sur la mémoire:

  • booléen - 1 bit

  • octet - 8 bits

  • court, caractère - 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 VM d'Oracle, le type booléen, par exemple, est mappé aux valeurs int 0 et 1, donc il prend 32 bits, comme décrit ici:Primitive Types and Values.

Les variables de ces types résident dans la pile et sont donc rapidement accessibles. Pour plus de détails, nous recommandons nostutorial 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 un certain surcoût concernant leurs homologues primitifs.

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

java 10.0.1 2018-04-17
Java(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'outilJava Object Layout (voir notre autretutorial pour savoir comment obtenir la taille d'un objet).

Il s'avère qu'une seule instance d'un type de référence sur cette JVM occupe 128 bits sauf pourLong etDouble qui occupent 192 bits:

  • Booléen - 128 bits

  • Octet - 128 bits

  • Court, caractère - 128 bits

  • Entier, Float - 128 bits

  • Double long - 192 bits

Nous pouvons voir qu'une seule variable de typeBoolean occupe autant d'espace que 128 variables primitives, tandis qu'une variableInteger occupe autant d'espace que quatre variablesint.

3.2. Empreinte mémoire pour les tableaux

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

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

image

cela démontre que les types sont regroupés en quatre familles en fonction de la façon dont la mémoirem(s) dépend du nombre d'éléments s du tableau:

  • long, double: m (s) = 128 + 64 s

  • court, car: m (s) = 128 + 64 [s / 4]

  • octet, booléen: m (s) = 128 + 64 [s / 8]

  • le reste: m (s) = 128 + 64 [s / 2]

où les crochets indiquent la fonction de plafond standard.

De manière surprenante, les tableaux des types primitifs long et double consomment plus de mémoire que leurs classes wrapperLong etDouble.

On peut voir non plus quesingle-element arrays of primitive types are almost always more expensive (except for long and double) than the corresponding reference type.

3.3. Performance

La performance d’un code Java est une question assez subtile, elle dépend beaucoup du matériel sur lequel le code est exécuté, du compilateur qui peut effectuer certaines optimisations, de l’état de la machine virtuelle, de l’activité des autres processus du processus. système opérateur.

Comme nous l'avons déjà mentionné, les types primitifs résident dans la pile, tandis que les types de référence résident dans le tas. C'est un facteur dominant qui détermine la rapidité d'accès aux objets.

Pour démontrer à quel point les opérations pour les types primitifs sont plus rapides que celles des classes wrapper, créons un tableau de cinq millions d’éléments dans lequel tous les éléments sont égaux à l’exception du dernier; puis nous effectuons une recherche pour cet élément:

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

et comparez 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 d'analyse comparativeJMH (voir notretutorial pour savoir comment l'utiliser), et les résultats de l'opération de recherche peuvent être résumés dans ce graphique:

image

 

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 complexes telles que la sommation, la multiplication ou la division, la différence de vitesse peut monter en flèche.

3.4. Les valeurs par défaut

Les valeurs par défaut des types primitifs sont0 (dans la représentation correspondante, i.e. 0,0.0d etc) pour les types numériques,false pour le type booléen, pour le type char. Pour les classes wrapper, la valeur par défaut estnull.

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 de type par défaut, il convient de déterminer si la variable a été réellement initialisée.

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

4. Usage

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'un autre côté, la spécification actuelle du langage Java ne permet pas l'utilisation de types primitifs dans les types paramétrés (génériques), dans les collections Java ou dans l'API Reflection.

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

5. Conclusion

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

Comme toujours, des extraits de code peuvent être trouvés dans nosrepository on GitHub.