Java-Grundelemente im Vergleich zu Objekten

1. Übersicht

In diesem Tutorial zeigen wir die Vor- und Nachteile der Verwendung von Java-Primitivtypen und ihrer umschlossenen Gegenstücke.

2. Java Type System

Java verfügt über ein System mit zwei Typen, das aus Grundelementen wie int , boolean und Referenztypen wie Integer, Boolean besteht . Jeder primitive Typ entspricht einem Referenztyp.

Jedes Objekt enthält einen einzelnen Wert des entsprechenden primitiven Typs. Die Wrapper-Klassen sind unveränderlich (damit sich ihr Status nicht ändern kann, sobald das Objekt erstellt wurde) und endgültig (damit wir nicht von ihnen erben können).

Unter der Haube führt Java eine Konvertierung zwischen dem primitiven und dem Referenztyp durch, wenn sich ein tatsächlicher Typ von dem deklarierten unterscheidet:

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

Der Prozess der Konvertierung eines primitiven Typs in einen Referenztyp wird als Autoboxing bezeichnet, der entgegengesetzte Prozess als Unboxing.

3. Vor- und Nachteile

Die Entscheidung, welches Objekt verwendet werden soll, hängt davon ab, welche Anwendungsleistung wir erreichen möchten, wie viel verfügbarer Speicher wir haben, wie viel Speicher verfügbar ist und welche Standardwerte wir verarbeiten sollten.

Wenn wir uns keiner davon stellen, können wir diese Überlegungen ignorieren, obwohl es sich lohnt, sie zu kennen.

3.1. Speicherbedarf für einzelne Elemente

Nur als Referenz haben die Variablen vom primitiven Typ die folgenden Auswirkungen auf den Speicher:

  • Boolescher Wert - 1 Bit
  • Byte - 8 Bits
  • kurz, char - 16 Bit
  • int, float - 32 Bit
  • lang, doppelt - 64 Bit

In der Praxis können diese Werte je nach Implementierung der virtuellen Maschine variieren. In der VM von Oracle wird der boolesche Typ beispielsweise den int-Werten 0 und 1 zugeordnet, sodass 32 Bit erforderlich sind, wie hier beschrieben: Primitive Typen und Werte.

Variablen dieser Typen befinden sich im Stapel und sind daher schnell zugänglich. Für Details empfehlen wir unser Tutorial zum Java-Speichermodell.

Die Referenztypen sind Objekte, sie befinden sich auf dem Heap und sind relativ langsam zugänglich. Sie haben einen gewissen Overhead in Bezug auf ihre primitiven Gegenstücke.

Die konkreten Werte des Overheads sind im Allgemeinen JVM-spezifisch. Hier präsentieren wir Ergebnisse für eine virtuelle 64-Bit-Maschine mit folgenden Parametern:

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)

Um die interne Struktur eines Objekts zu erhalten, verwenden wir möglicherweise das Java-Objektlayout-Tool (siehe unser weiteres Tutorial zum Ermitteln der Größe eines Objekts).

Es stellt sich heraus, dass eine einzelne Instanz eines Referenztyps in dieser JVM 128 Bit belegt, mit Ausnahme von Long und Double, die 192 Bit belegen:

  • Boolean - 128 Bit
  • Byte - 128 Bit
  • Kurz, Zeichen - 128 Bit
  • Integer, Float - 128 Bit
  • Lang, doppelt - 192 Bit

Wir können sehen, dass eine einzelne Variable vom Typ Boolean so viel Platz einnimmt wie 128 primitive, während eine Integer- Variable so viel Platz einnimmt wie vier int- Variablen .

3.2. Speicherbedarf für Arrays

Die Situation wird interessanter, wenn wir vergleichen, wie viel Speicher Arrays der betrachteten Typen belegt.

Wenn wir Arrays mit der unterschiedlichen Anzahl von Elementen für jeden Typ erstellen, erhalten wir ein Diagramm:

Dies zeigt, dass die Typen in Bezug auf die Abhängigkeit des Speichers m (s) von der Anzahl der Elemente s des Arrays in vier Familien eingeteilt sind :

  • lang, doppelt: m (s) = 128 + 64 s
  • kurz, char: m (s) = 128 + 64 [s / 4]
  • Byte, Boolescher Wert: m (s) = 128 + 64 [s / 8]
  • der Rest: m (s) = 128 + 64 [s / 2]

wobei die eckigen Klammern die Standarddeckenfunktion bezeichnen.

Überraschenderweise verbrauchen Arrays der primitiven Typen long und double mehr Speicher als ihre Wrapper-Klassen Long und Double .

Wir können entweder sehen, dass Einzelelement-Arrays primitiver Typen fast immer teurer sind (außer lang und doppelt) als der entsprechende Referenztyp .

3.3. Performance

Die Leistung eines Java-Codes ist ein ziemlich subtiles Problem. Sie hängt sehr stark von der Hardware ab, auf der der Code ausgeführt wird, vom Compiler, der bestimmte Optimierungen durchführen kann, vom Status der virtuellen Maschine und von der Aktivität anderer Prozesse in der Betriebssystem.

Wie bereits erwähnt, befinden sich die primitiven Typen im Stapel, während sich die Referenztypen im Heap befinden. Dies ist ein dominierender Faktor, der bestimmt, wie schnell auf die Objekte zugegriffen wird.

Um zu demonstrieren, um wie viel die Operationen für primitive Typen schneller sind als die für Wrapper-Klassen, erstellen wir ein Array mit fünf Millionen Elementen, in dem alle Elemente bis auf das letzte gleich sind. dann führen wir eine Suche nach diesem Element durch:

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.

Wenn unsere Anwendung Sammlungen mit einer großen Anzahl von Elementen benötigt, sollten wir in Betracht ziehen, Arrays mit einem möglichst „wirtschaftlichen“ Typ zu verwenden, wie in der obigen Darstellung dargestellt.

5. Schlussfolgerung

In diesem Tutorial haben wir gezeigt, dass die Objekte in Java langsamer sind und eine größere Auswirkung auf den Speicher haben als ihre primitiven Analoga.

Code-Snippets finden Sie wie immer in unserem Repository auf GitHub.