Primitive Java contro oggetti

1. Panoramica

In questo tutorial, mostriamo i pro ei contro dell'utilizzo dei tipi primitivi Java e delle loro controparti incluse.

2. Java Type System

Java ha un doppio sistema di tipi costituito da primitive come int , boolean e tipi di riferimento come Integer, Boolean . Ogni tipo primitivo corrisponde a un tipo di riferimento.

Ogni oggetto contiene un singolo valore del tipo primitivo corrispondente. Le classi wrapper sono immutabili (in modo che il loro stato non possa cambiare una volta costruito l'oggetto) e sono definitive (in modo che non possiamo ereditare da loro).

Sotto il cofano, Java esegue una conversione tra i tipi primitivi e di riferimento se un tipo effettivo è diverso da quello dichiarato:

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

Il processo di conversione di un tipo primitivo in uno di riferimento è chiamato autoboxing, il processo opposto è chiamato unboxing.

3. Pro e contro

La decisione sull'oggetto da utilizzare dipende dalle prestazioni dell'applicazione che si cerca di ottenere, dalla quantità di memoria disponibile, dalla quantità di memoria disponibile e dai valori predefiniti da gestire.

Se non affrontiamo nessuno di questi, possiamo ignorare queste considerazioni anche se vale la pena conoscerle.

3.1. Impronta di memoria di un singolo elemento

Solo per riferimento, le variabili di tipo primitivo hanno il seguente impatto sulla memoria:

  • booleano - 1 bit
  • byte - 8 bit
  • breve, char - 16 bit
  • int, float - 32 bit
  • lungo, doppio - 64 bit

In pratica, questi valori possono variare a seconda dell'implementazione della macchina virtuale. Nella VM di Oracle, il tipo booleano, ad esempio, è mappato ai valori int 0 e 1, quindi richiede 32 bit, come descritto qui: Tipi e valori primitivi.

Le variabili di questo tipo risiedono nello stack e quindi si accede velocemente. Per i dettagli, consigliamo il nostro tutorial sul modello di memoria Java.

I tipi di riferimento sono oggetti, risiedono nell'heap e sono relativamente lenti nell'accesso. Hanno un certo sovraccarico riguardo alle loro controparti primitive.

I valori concreti dell'overhead sono in generale specifici di JVM. Di seguito vengono presentati i risultati per una macchina virtuale a 64 bit con questi parametri:

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)

Per ottenere la struttura interna di un oggetto, possiamo utilizzare lo strumento Java Object Layout (guarda il nostro altro tutorial su come ottenere le dimensioni di un oggetto).

Risulta che una singola istanza di un tipo di riferimento su questa JVM occupa 128 bit ad eccezione di Long e Double che occupano 192 bit:

  • Booleano: 128 bit
  • Byte: 128 bit
  • Breve, carattere - 128 bit
  • Intero, Float - 128 bit
  • Lungo, doppio - 192 bit

Possiamo vedere che una singola variabile di tipo booleano occupa tanto spazio quanto 128 variabili primitive, mentre una variabile Integer occupa tanto spazio quanto quattro variabili int .

3.2. Impronta di memoria per gli array

La situazione diventa più interessante se confrontiamo la quantità di memoria che occupa gli array dei tipi presi in considerazione.

Quando creiamo array con il diverso numero di elementi per ogni tipo, otteniamo un grafico:

that demonstrates that the types are grouped into four families with respect to how the memory m(s) depends on the number of elements s of the array:

  • long, double: m(s) = 128 + 64 s
  • short, char: m(s) = 128 + 64 [s/4]
  • byte, boolean: m(s) = 128 + 64 [s/8]
  • the rest: m(s) = 128 + 64 [s/2]

where the square brackets denote the standard ceiling function.

Surprisingly, arrays of the primitive types long and double consume more memory than their wrapper classes Long and Double.

We can see either that single-element arrays of primitive types are almost always more expensive (except for long and double) than the corresponding reference type.

3.3. Performance

The performance of a Java code is quite a subtle issue, it depends very much on the hardware on which the code runs, on the compiler that might perform certain optimizations, on the state of the virtual machine, on the activity of other processes in the operating system.

As we have already mentioned, the primitive types live in the stack while the reference types live in the heap. This is a dominant factor that determines how fast the objects get be accessed.

To demonstrate how much the operations for primitive types are faster than those for wrapper classes, let's create a five million element array in which all elements are equal except for the last one; then we perform a lookup for that element:

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.

Quando la nostra applicazione necessita di raccolte con un numero elevato di elementi, dovremmo considerare l'utilizzo di array con il tipo più "economico" possibile, come illustrato nel grafico sopra.

5. conclusione

In questo tutorial, abbiamo illustrato che gli oggetti in Java sono più lenti e hanno un impatto sulla memoria maggiore rispetto ai loro analoghi primitivi.

Come sempre, gli snippet di codice possono essere trovati nel nostro repository su GitHub.