Pourquoi les gens utilisent-ils encore des types primitifs en Java?


163

Depuis Java 5, nous avons boxé / déballé des types primitifs pour que cela intsoit enveloppé java.lang.Integer, et ainsi de suite.

Je vois beaucoup de nouveaux projets Java ces derniers temps (qui nécessitent définitivement un JRE d'au moins la version 5, sinon 6) qui utilisent intplutôt que java.lang.Integer, bien qu'il soit beaucoup plus pratique d'utiliser ce dernier, car il a quelques méthodes d'aide pour la conversion aux longvaleurs et al.

Pourquoi certains utilisent encore des types primitifs en Java? Y a-t-il un avantage tangible?


49
avez-vous déjà pensé à la consommation de mémoire et aux performances?
Tedil

76
J'ai ajouté la balise autoboxing ... et j'ai découvert que trois personnes la suivaient. Vraiment? Les gens suivent la balise AUTOBOXING?
corsiKa

4
@glowcoder Ce ne sont pas des personnes réelles, ce ne sont que des concepts abstraits qui prennent une forme humaine pour répondre sur SO. :)
biziclop

9
@TK Kocheran Principalement parce que new IntegeR(5) == new Integer(5)les règles devraient être évaluées à false.
biziclop

10
Voir GNU Trove ou Mahout Collections ou HPPC ou ... pour des solutions aux collections de types primitifs. Ceux d'entre nous qui se soucient de la vitesse passent leur temps à utiliser des types plus primitifs, pas moins.
bmargulies

Réponses:


395

Dans Effective Java de Joshua Bloch , article 5: «Évitez de créer des objets inutiles», il publie l'exemple de code suivant:

public static void main(String[] args) {
    Long sum = 0L; // uses Long, not long
    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);
}

et il faut 43 secondes pour fonctionner. Prendre le Long dans la primitive le ramène à 6,8 secondes ... Si cela indique pourquoi nous utilisons des primitives.

Le manque d'égalité des valeurs natives est également un problème ( .equals()est assez verbeux par rapport à ==)

pour biziclop:

class Biziclop {

    public static void main(String[] args) {
        System.out.println(new Integer(5) == new Integer(5));
        System.out.println(new Integer(500) == new Integer(500));

        System.out.println(Integer.valueOf(5) == Integer.valueOf(5));
        System.out.println(Integer.valueOf(500) == Integer.valueOf(500));
    }
}

Résulte en:

false
false
true
false

EDIT Pourquoi (3) retourne-t-il trueet (4) revient-il false?

Parce que ce sont deux objets différents. Les 256 entiers les plus proches de zéro [-128; 127] sont mis en cache par la JVM, ils renvoient donc le même objet pour ceux-ci. Au-delà de cette plage, cependant, ils ne sont pas mis en cache, donc un nouvel objet est créé. Pour compliquer les choses, le JLS exige qu'au moins 256 poids mouches soient mis en cache. Les implémenteurs de JVM peuvent en ajouter plus s'ils le souhaitent, ce qui signifie que cela pourrait fonctionner sur un système où les 1024 les plus proches sont mis en cache et tous retournent true ... #awkward


54
Imaginez maintenant si elles iétaient également déclarées Long!
ColinD

14
@TREE - la spécification exige en fait que les machines virtuelles créent des poids mouche dans une certaine plage. Mais malheureusement, cela leur permet d'étendre cette plage, ce qui signifie que les programmes peuvent se comporter différemment sur différentes machines virtuelles. Tellement pour le cross platform ...
Daniel Earwicker

12
Java s'est effondré, avec de plus en plus de mauvais choix de conception. La boxe automatique est un échec complet, elle n'est ni robuste, ni prévisible, ni portable. Je me demande vraiment ce qu'ils pensaient ... au lieu de réparer la redoutable dualité primitive-objet, ils ont réussi à l'aggraver qu'au départ.
Pop Catalin

34
@Catalin Je ne suis pas d'accord avec vous pour dire que l'auto-boxe est un échec complet. Il a quelques défauts, qui ne sont pas différents de tout autre design qui aurait pu être utilisé (y compris rien.) Ils indiquent très clairement ce à quoi vous pouvez et ne pouvez pas vous attendre, et comme tout autre design, ils s'attendent à ce que les développeurs connaissent et obéissent aux contrats. de ces dessins.
corsiKa

9
@NaftuliTzviKay Ce n'est pas un «échec». Ils rendent TRES CLAIR que l' ==opérateur effectue des comparaisons d'identité de référence sur les Integerexpressions et des comparaisons d'égalité de valeur sur les intexpressions. Integer.equals()existe précisément pour cette raison. Vous ne devez jamais utiliser ==pour comparer des valeurs dans un type non primitif. Ceci est Java 101.
NullUserException

86

L'autounboxing peut conduire à des NPE difficiles à repérer

Integer in = null;
...
...
int i = in; // NPE at runtime

Dans la plupart des situations, l'affectation nulle à inest beaucoup moins évidente que ci-dessus.


43

Les types en boîte ont de moins bonnes performances et nécessitent plus de mémoire.


40

Types primitifs:

int x = 1000;
int y = 1000;

Évaluez maintenant:

x == y

C'est true. Pas étonnant. Maintenant, essayez les types en boîte:

Integer x = 1000;
Integer y = 1000;

Évaluez maintenant:

x == y

C'est false. Probablement. Dépend du runtime. Cette raison est-elle suffisante?


36

Outre les problèmes de performances et de mémoire, j'aimerais proposer un autre problème: l' Listinterface serait cassée sans int.
Le problème est la remove()méthode surchargée ( remove(int)vs. remove(Object)). remove(Integer)se résoudrait toujours à appeler ce dernier, vous ne pouvez donc pas supprimer un élément par index.

D'autre part, il y a un piège lorsque vous essayez d'ajouter et de supprimer un int:

final int i = 42;
final List<Integer> list = new ArrayList<Integer>();
list.add(i); // add(Object)
list.remove(i); // remove(int) - Ouch!

7
Ce serait cassé, oui. Cependant, remove (int) est un défaut de conception IMO. Les noms de méthodes ne doivent jamais être surchargés s'il y a la moindre chance de confusion.
MrBackend

4
@MrBackend Assez juste. Fait intéressant, Vectoravait removeElementAt(int)dès le début. remove(int)a été introduit avec le framework de collections en Java 1.2.
xehpuk

6
@MrBackend: lorsque l' ListAPI a été conçue, ni les génériques ni l'Autoboxing n'existaient, donc il n'y avait aucune chance de se mélanger remove(int)et remove(Object)
Holger

@Franklin Yu: bien sûr, mais lors de la conception d'un nouveau langage / version sans contraintes de compatibilité, vous ne vous arrêterez pas à changer cette malheureuse surcharge. Vous vous débarrasseriez simplement de la distinction entre les primitives et les valeurs encadrées, de sorte que la question à utiliser n'apparaîtra jamais.
Holger

27

Pouvez-vous vraiment imaginer un

  for (int i=0; i<10000; i++) {
      do something
  }

boucle avec java.lang.Integer à la place? Un java.lang.Integer est immuable, donc chaque incrément autour de la boucle créerait un nouvel objet java sur le tas, plutôt que de simplement incrémenter l'int sur la pile avec une seule instruction JVM. La performance serait diabolique.

Je ne suis vraiment pas d'accord pour dire qu'il est beaucoup plus pratique d'utiliser java.lang.Integer que int. Au contraire. Autoboxing signifie que vous pouvez utiliser int là où vous seriez autrement obligé d'utiliser Integer, et le compilateur java se charge d'insérer le code pour créer le nouvel objet Integer pour vous. Autoboxing consiste à vous permettre d'utiliser un int là où un entier est attendu, le compilateur insérant la construction d'objet appropriée. Cela ne supprime ni ne réduit en aucun cas le besoin de l'int en premier lieu. Avec la boxe automatique, vous obtenez le meilleur des deux mondes. Vous obtenez un entier créé automatiquement pour vous lorsque vous avez besoin d'un objet Java basé sur un tas, et vous obtenez la vitesse et l'efficacité d'un int lorsque vous ne faites que des calculs arithmétiques et locaux.


19

Les types primitifs sont beaucoup plus rapides:

int i;
i++;

Integer (tous les nombres et aussi une chaîne) est un type immuable : une fois créé, il ne peut pas être modifié. Si iétait Integer, i++cela créerait un nouvel objet Integer - beaucoup plus cher en termes de mémoire et de processeur.


Vous ne voulez pas qu'une variable change si vous le faites i++sur une autre variable, donc Integer doit tout à fait être immuable pour pouvoir le faire (ou du moins cela i++devrait créer un nouvel objet Integer de toute façon). (Et les valeurs primitives sont également immuables - vous ne le remarquez tout simplement pas car ce ne sont pas des objets.)
Paŭlo Ebermann

4
@ Paŭlo: Dire que les valeurs primitives sont immuables n'a pas de sens. Lorsque vous réaffectez une variable primitive à une nouvelle valeur, vous ne créez rien de nouveau. Aucune allocation de mémoire n'est impliquée. Le point de Peter tient: i ++ pour une primitive ne fait pas d'allocation de mémoire, mais pour un objet il le fait nécessairement.
Eddie

@Eddie: (Il n'a pas nécessairement besoin d'allocation de mémoire, il pourrait également renvoyer une valeur en cache. Pour certaines petites valeurs, c'est le cas, je pense.) Mon point était que l'immutabilité des nombres entiers ici n'est pas le point décisif, vous voudriez de toute façon avoir un autre objet, indépendamment de l'immuabilité.
Paŭlo Ebermann

@ Paŭlo: mon seul point était que Integer est un ordre de grandeur plus lent que les primitives. Et cela est dû au fait que les types encadrés sont immuables et à chaque fois que vous modifiez une valeur, un nouvel objet est créé. Je n'ai pas prétendu qu'il y avait quelque chose qui ne va pas avec eux ou le fait qu'ils sont immuables. Juste qu'ils sont plus lents et qu'un codeur devrait le savoir. Regardez comment Groovy tarifs sans types primitifs jroller.com/rants/entry/why_is_groovy_so_slow
Peter Knego

1
Immuabilité et ++est un hareng rouge ici. Imaginez Java a été amélioré pour l' opérateur de soutien surcharge d'une manière très simple, de sorte que si une classe ( par exemple Integera une méthode plus, alors vous pouvez écrire au i + 1lieu de i.plus(1). Et aussi supposer que le compilateur est assez intelligent pour développer i++en i = i + 1. Maintenant , vous pourriez dire i++et effectivement "incrémenter la variable i" sans Integerêtre mutable.
Daniel Earwicker

16

D'abord et avant tout, l'habitude. Si vous codez en Java depuis huit ans, vous accumulez une quantité considérable d'inertie. Pourquoi changer s'il n'y a aucune raison impérieuse de le faire? Ce n'est pas comme si l'utilisation de primitives en boîte présentait des avantages supplémentaires.

L'autre raison est d'affirmer que ce nulln'est pas une option valable. Il serait inutile et trompeur de déclarer la somme de deux nombres ou une variable de boucle comme Integer.

Il y a aussi l'aspect performance, alors que la différence de performance n'est pas critique dans de nombreux cas (bien que quand c'est le cas, c'est assez mauvais), personne n'aime écrire du code qui pourrait être écrit aussi facilement et plus rapidement que nous sommes déjà habitué.


15
Je ne suis pas d'accord. L'aspect performance peut être critique. Très peu de ceci est probablement inertiel ou force d'habitude.
Eddie

7
@Eddie C'est possible, mais c'est très rarement le cas. Croyez-moi, pour la plupart des gens, les arguments de performance ne sont qu'une excuse.
biziclop

3
Je voudrais moi aussi protéger l'argument de la performance. Sur Android avec Dalvik, chaque objet que vous créez augmentera le "risque" d'appeler GC et plus vous aurez d'objets, les pauses seront plus longues. Donc, créer des entiers au lieu de int dans une boucle vous coûtera probablement quelques images perdues.
Igor Čordaš

1
@PSIXO C'est un bon point, je l'ai écrit avec Java purement côté serveur à l'esprit. Les appareils mobiles sont un tout autre animal. Mais mon point était que même les développeurs qui écrivent autrement du code terrible sans aucun égard pour les performances citeront cela comme une raison, de leur part, cela semble être une excuse.
biziclop

12

À propos, Smalltalk n'a que des objets (pas de primitives), et pourtant ils avaient optimisé leurs petits entiers (en n'utilisant pas tous les 32 bits, seulement 27 ou autres) pour ne pas allouer d'espace de tas, mais simplement utiliser un modèle de bits spécial. De plus, d'autres objets courants (vrai, faux, nul) avaient ici des modèles de bits spéciaux.

Ainsi, au moins sur les JVM 64 bits (avec un espace de noms de pointeur 64 bits), il devrait être possible de ne pas avoir d'objets de type Integer, Character, Byte, Short, Boolean, Float (et small Long) du tout (à part ceux créés par explicite new ...()), uniquement des modèles de bits spéciaux, qui pourraient être manipulés par les opérateurs normaux assez efficacement.


J'aurais dû dire "quelques implémentations", car cela n'est pas régi par les spécifications du langage, je pense. (Et malheureusement, je ne peux citer aucune source ici, ce n'est que de ce que j'ai entendu quelque part.)
Paŭlo Ebermann

ŭlo, JIT garde déjà meta dans le pointeur; incl, le pointeur peut conserver les informations GC, ou le Klass (l'optimisation de la classe est une bien meilleure idée que l'optimisation des nombres entiers, pour laquelle je me soucie moins). Changer le pointeur nécessiterait un code shift / cmp / jnz (ou quelque chose comme ça) avant chaque chargement de pointeur. La branche ne sera probablement pas très bien prédite par le matériel (car elle peut être à la fois de type valeur et objet normal) et cela entraînerait une baisse des performances.
bestsss

3
J'ai fait Smalltalk pendant quelques années. L'optimisation était encore assez coûteuse, car pour chaque opération sur un int, ils devaient les démasquer et les réappliquer. Actuellement, java est à égalité avec C lors de la manipulation de nombres primitifs. Avec unmask + mask, il sera probablement> 30% plus lent.
R.Moeller

9

Je ne peux pas croire que personne n'ait mentionné ce que je pense être la raison la plus importante: "int" est tellement plus facile à taper que "Integer". Je pense que les gens sous-estiment l'importance d'une syntaxe concise. Les performances ne sont pas vraiment une raison pour les éviter, car la plupart du temps, lorsque l'on utilise des nombres, on utilise des index de boucle, et l'incrémentation et la comparaison de ces derniers ne coûtent rien dans une boucle non triviale (que vous utilisiez int ou Integer).

L'autre raison donnée était que vous pouvez obtenir des NPE, mais c'est extrêmement facile à éviter avec les types encadrés (et cela est garanti à éviter tant que vous les initialisez toujours à des valeurs non nulles).

L'autre raison était que (new Long (1000)) == (new Long (1000)) est false, mais c'est juste une autre façon de dire que ".equals" n'a pas de support syntaxique pour les types encadrés (contrairement aux opérateurs <,> , =, etc), nous revenons donc à la raison de la "syntaxe plus simple".

Je pense que l'exemple de boucle non primitive de Steve Yegge illustre très bien mon propos: http://sites.google.com/site/steveyegge2/language-trickery-and-ejb

Pensez à ceci: à quelle fréquence utilisez-vous des types de fonctions dans des langages qui ont une bonne syntaxe pour eux (comme tout langage fonctionnel, python, ruby ​​et même C) par rapport à java où vous devez les simuler à l'aide d'interfaces telles que Runnable et Callable et classes sans nom.


8

Quelques raisons de ne pas se débarrasser des primitifs:

  • Compatibilité descendante.

S'il est éliminé, les anciens programmes ne fonctionneraient même pas.

  • Réécriture JVM.

L'ensemble de la machine virtuelle Java devrait être réécrit pour prendre en charge cette nouvelle chose.

  • Plus grande empreinte mémoire.

Vous auriez besoin de stocker la valeur et la référence, ce qui utilise plus de mémoire. Si vous avez un grand nombre d'octets, l'utilisation de byte's est nettement plus petite que celle de Byte' s.

  • Problèmes de pointeur nul.

Déclarer int iensuite faire des choses avec in'entraînerait aucun problème, mais déclarer Integer iet faire de même entraînerait un NPE.

  • Problèmes d'égalité.

Considérez ce code:

Integer i1 = 5;
Integer i2 = 5;

i1 == i2; // Currently would be false.

Ce serait faux. Les opérateurs devraient être surchargés, ce qui entraînerait une réécriture majeure des choses.

  • Lent

Les wrappers d'objets sont nettement plus lents que leurs homologues primitifs.


i1 == i2; serait faux seulement si i1> = 128. Donc, l'exemple actuel est faux
Geniy

7

Les objets sont beaucoup plus lourds que les types primitifs, les types primitifs sont donc beaucoup plus efficaces que les instances de classes wrapper.

Les types primitifs sont très simples: par exemple, un int fait 32 bits et occupe exactement 32 bits en mémoire, et peut être manipulé directement. Un objet Integer est un objet complet, qui (comme tout objet) doit être stocké sur le tas, et n'est accessible que via une référence (pointeur) vers celui-ci. Il occupe très probablement également plus de 32 bits (4 octets) de mémoire.

Cela dit, le fait que Java ait une distinction entre les types primitifs et non primitifs est également un signe de l'âge du langage de programmation Java. Les nouveaux langages de programmation n'ont pas cette distinction; le compilateur d'un tel langage est suffisamment intelligent pour déterminer par lui-même si vous utilisez des valeurs simples ou des objets plus complexes.

Par exemple, dans Scala, il n'y a pas de types primitifs; il existe une classe Int pour les entiers, et un Int est un objet réel (sur lequel vous pouvez utiliser des méthodes, etc.). Lorsque le compilateur compile votre code, il utilise des entiers primitifs dans les coulisses, donc l'utilisation d'un int est tout aussi efficace que l'utilisation d'un int primitif en Java.


1
J'aurais supposé que le JRE serait suffisamment "intelligent" pour faire cela avec les primitives Java enveloppées également. Échouer.
Naftuli Kay

7

En plus de ce que d'autres ont dit, les variables locales primitives ne sont pas allouées à partir du tas, mais à la place sur la pile. Mais les objets sont alloués à partir du tas et doivent donc être récupérés.


3
Désolé, c'est faux. Une JVM intelligente peut effectuer une analyse d'échappement sur toutes les allocations d'objets et, si elles ne peuvent pas s'échapper, les allouer sur la pile.
rlibby

2
Oui, cela commence à être une caractéristique des JVM modernes. Dans cinq ans, ce que vous dites sera vrai pour la plupart des JVM alors en usage. Aujourd'hui, ce n'est pas le cas. J'ai presque fait des commentaires à ce sujet, mais j'ai décidé de ne pas faire de commentaires à ce sujet. J'aurais peut-être dû dire quelque chose.
Eddie

6

Les types primitifs présentent de nombreux avantages:

  • Code plus simple à écrire
  • Les performances sont meilleures car vous n'instanciez pas un objet pour la variable
  • Puisqu'ils ne représentent pas une référence à un objet, il n'est pas nécessaire de vérifier les valeurs nulles
  • Utilisez des types primitifs, sauf si vous devez tirer parti des fonctionnalités de boxe.

5

Il est difficile de savoir quels types d'optimisations sont en cours sous les couvertures.

Pour une utilisation locale, lorsque le compilateur a suffisamment d'informations pour effectuer des optimisations excluant la possibilité de la valeur nulle, je m'attends à ce que les performances soient identiques ou similaires .

Cependant, les tableaux de primitives sont apparemment très différents des collections de primitives encadrées. Cela a du sens étant donné que très peu d'optimisations sont possibles au sein d'une collection.

De plus, Integera une surcharge logique beaucoup plus élevée que int: maintenant, vous devez vous soucier de savoir si int a = b + c;une exception est levée ou non .

J'utiliserais les primitives autant que possible et me fierais aux méthodes d'usine et à l'auto-boxe pour me donner les types encadrés les plus sémantiquement puissants quand ils sont nécessaires.


5
int loops = 100000000;

long start = System.currentTimeMillis();
for (Long l = new Long(0); l<loops;l++) {
    //System.out.println("Long: "+l);
}
System.out.println("Milliseconds taken to loop '"+loops+"' times around Long: "+ (System.currentTimeMillis()- start));

start = System.currentTimeMillis();
for (long l = 0; l<loops;l++) {
    //System.out.println("long: "+l);
}
System.out.println("Milliseconds taken to loop '"+loops+"' times around long: "+ (System.currentTimeMillis()- start));

Millisecondes nécessaires pour boucler '100000000' fois autour de Long: 468

Millisecondes nécessaires pour boucler '100000000' fois autour de long: 31

En passant, cela ne me dérangerait pas de voir quelque chose comme ça trouver son chemin dans Java.

Integer loop1 = new Integer(0);
for (loop1.lessThan(1000)) {
   ...
}

Où la boucle for incrémente automatiquement loop1 de 0 à 1000 ou

Integer loop1 = new Integer(1000);
for (loop1.greaterThan(0)) {
   ...
}

Où la boucle for décrémente automatiquement la boucle1 de 1000 à 0.


2
  1. Vous avez besoin de primitives pour effectuer des opérations mathématiques
  2. Les primitives prennent moins de mémoire comme indiqué ci-dessus et sont meilleures

Vous devriez demander pourquoi le type de classe / objet est requis

La raison d'avoir un type d'objet est de nous faciliter la vie lorsque nous traitons des collections. Les primitives ne peuvent pas être ajoutées directement à List / Map; vous devez plutôt écrire une classe wrapper. Le type de classes Readymade Integer vous aide ici en plus de nombreuses méthodes utilitaires telles que Integer.pareseInt (str)


2

Je suis d'accord avec les réponses précédentes, utiliser des objets wrapper primitifs peut être coûteux. Mais, si les performances ne sont pas critiques dans votre application, vous évitez les débordements lors de l'utilisation d'objets. Par exemple:

long bigNumber = Integer.MAX_VALUE + 2;

La valeur de bigNumberest -2147483647, et vous vous attendez à ce qu'elle soit 2147483649. C'est un bogue dans le code qui serait corrigé en faisant:

long bigNumber = Integer.MAX_VALUE + 2l; // note that '2' is a long now (it is '2L').

Et ce bigNumberserait 2147483649. Ce type de bogue est parfois facile à ignorer et peut conduire à des comportements inconnus ou à des vulnérabilités (voir CWE-190 ).

Si vous utilisez des objets wrapper, le code équivalent ne sera pas compilé.

Long bigNumber = Integer.MAX_VALUE + 2; // Not compiling

Il est donc plus facile d'arrêter ce genre de problèmes en utilisant des objets wrapper primitifs.

Votre question est déjà tellement répondue que je réponds simplement pour ajouter un peu plus d'informations non mentionnées auparavant.


1

Parce que JAVA effectue toutes les opérations mathématiques dans les types primitifs. Prenons cet exemple:

public static int sumEven(List<Integer> li) {
    int sum = 0;
    for (Integer i: li)
        if (i % 2 == 0)
            sum += i;
        return sum;
}

Ici, les opérations de rappel et unaire plus ne peuvent pas être appliquées au type Integer (Reference), le compilateur effectue le déballage et effectue les opérations.

Alors, assurez-vous du nombre d'opérations de mise en boîte automatique et de déballage effectuées dans le programme java. Depuis, il faut du temps pour effectuer ces opérations.

Généralement, il est préférable de conserver les arguments de type Reference et le résultat de type primitif.


1

Les types primitifs sont beaucoup plus rapides et nécessitent beaucoup moins de mémoire . Par conséquent, nous préférons peut-être les utiliser.

D'autre part, 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 devrions envisager d'utiliser des tableaux de type le plus «économique» possible.

* Pour des informations détaillées, voir la source: https://www.baeldung.com/java-primitives-vs-objects


0

Pour être bref: les types primitifs sont plus rapides et nécessitent moins de mémoire que les types en boîte

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.