Primitives Java et objets

1. Vue d'ensemble

Dans ce tutoriel, 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 types Java

Java a un système de type à deux volets composé de primitives telles que les types int , boolean et reference tels que Integer, Boolean . Chaque type primitif correspond à un type de référence.

Chaque objet contient une valeur unique 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 définitives (de sorte que nous ne pouvons 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 est appelé autoboxing, le processus inverse est appelé unboxing.

3. Avantages et inconvénients

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

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

3.1. Empreinte mémoire d'un seul élément

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

  • booléen - 1 bit
  • octet - 8 bits
  • court, caractère - 16 bits
  • int, flottant - 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 primitifs et valeurs.

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 d'accès. 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 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'outil Java Object Layout (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 JVM 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, flottant - 128 bits
  • Long, double - 192 bits

Nous pouvons voir qu'une seule variable de type booléen occupe autant d'espace que 128 variables primitives, tandis qu'une variable Integer occupe autant d'espace que quatre int .

3.2. Empreinte mémoire pour les baies

La situation devient plus intéressante si nous comparons la quantité de mémoire occupant les tableaux des types considérés.

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

cela démontre que les types sont regroupés en quatre familles en fonction de 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, 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.

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

Nous pouvons voir soit que les tableaux à un seul é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, cela 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é d'autres processus dans le système opérateur.

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 à quel point les opérations des 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 sauf le dernier; puis nous effectuons une recherche pour cet élément:

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

and compare the performance of this operation for the case when the array contains variables of the primitive types and for the case when it contains objects of the reference types.

We use the well-known JMH benchmarking tool (see our tutorial on how to use it), and the results of the lookup operation can be summarized in this chart:

Even for such a simple operation, we can see that it's required more time to perform the operation for wrapper classes.

In case of more complicated operations like summation, multiplication or division, the difference in speed might skyrocket.

3.4. Default Values

Default values of the primitive types are 0 (in the corresponding representation, i.e. 0, 0.0d etc) for numeric types, false for the boolean type, \u0000 for the char type. For the wrapper classes, the default value is null.

It means that the primitive types may acquire values only from their domains, while the reference types might acquire a value (null) that in some sense doesn't belong to their domains.

Though it isn't considered a good practice to leave variables uninitialized, sometimes we might assign a value after its creation.

In such a situation, when a primitive type variable has a value that is equal to its type default one, we should find out whether the variable has been really initialized.

There's no such a problem with a wrapper class variables since the null value is quite an evident indication that the variable hasn't been initialized.

4. Usage

As we've seen, the primitive types are much faster and require much less memory. Therefore, we might want to prefer using them.

On the other hand, current Java language specification doesn't allow usage of primitive types in the parametrized types (generics), in the Java collections or the Reflection API.

Lorsque notre application a besoin de collections avec un grand nombre d'éléments, nous devrions 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 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 référentiel sur GitHub.