Java Primitives versus Objects

Übersicht

In diesem Tutorial zeigen wir die Vor- und Nachteile der Verwendung von Java-primitiven Typen und ihren umschlossenen Gegenstücken.

Java Type System

Java hat ein zweifaches Typsystem, bestehend aus Primitiven wie int, boolean und Referenztypen wie Integer, Boolean . Jeder primitive Typ entspricht einem Referenztyp.

Jedes Objekt enthält einen einzelnen Wert des entsprechenden primitiven Typs. Die Wrapper-Klassen sind unveränderlich (so dass sich ihr Status nach der Erstellung des Objekts nicht ändern kann) und endgültig (so dass 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 Typ unterscheidet:

Integer j = 1; // autoboxingint i = new Integer(1); // unboxing

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

Vor- und Nachteile

Die Entscheidung, welches Objekt verwendet werden soll, basiert darauf, welche Anwendungsleistung wir zu erreichen versuchen, wie viel verfügbarer Speicher wir haben, wie groß der verfügbare Speicher ist und welche Standardwerte wir verarbeiten sollten.

Wenn wir keiner von denen gegenüberstehen, können wir diese Überlegungen ignorieren, obwohl es sich lohnt, sie zu kennen.

3.1. Single Item Memory Footprint

Nur als Referenz haben die primitiven Typvariablen die folgenden Auswirkungen auf den Speicher:

  • boolean – 1 Bit
  • Byte – 8 Bit
  • kurz, char – 16 Bit
  • int, float – 32 Bit
  • lang, double – 64 Bit

In der Praxis können diese Werte je nach Implementierung der virtuellen Maschine variieren. In der Oracle-VM 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 dieses Typs befinden sich im Stack und werden daher schnell aufgerufen. Für die Details empfehlen wir unser Tutorial zum Java-Speichermodell.

Die Referenztypen sind Objekte, sie leben 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-17Java(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, können wir das Java Object Layout Tool verwenden (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
  • Ganzzahl, Float – 128 Bit
  • Lang, Doppelt – 192 Bit

Wir können sehen, dass eine einzelne Variable vom booleschen Typ so viel Platz einnimmt wie 128 primitive, während eine ganzzahlige 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 belegen.

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

das zeigt, dass die Typen in vier Familien gruppiert sind in Bezug darauf, wie der Speicher m(s) von der Anzahl der Elemente s des Arrays abhängt:

  • long, double: m(s) = 128 + 64 s
  • short, char: m(s) = 128 + 64
  • Byte, boolesch: m(s) = 128 + 64
  • der Rest: m(s) = 128 + 64

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 Einelement-Arrays primitiver Typen fast immer teurer sind (außer long und double ) als der entsprechende Referenztyp.

3.3. Leistung

Die Leistung eines Java-Codes ist ein ziemlich subtiles Problem, es hängt sehr stark von der Hardware ab, auf der der Code ausgeführt wird, vom Compiler, der möglicherweise bestimmte Optimierungen durchführt, vom Status der virtuellen Maschine, von der Aktivität anderer Prozesse im 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 werden kann.

Um zu demonstrieren, wie sehr 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++;}

und vergleichen die Leistung dieser Operation für den Fall, dass das Array Variablen der primitiven Typen enthält, und für den Fall, dass es Objekte der Referenztypen enthält.

Wir verwenden das bekannte JMH-Benchmarking-Tool (siehe unser Tutorial zur Verwendung), und die Ergebnisse der Suchoperation können in dieser Tabelle zusammengefasst werden:

Selbst für eine so einfache Operation können wir sehen, dass es mehr Zeit benötigt, um die Operation für Wrapper-Klassen durchzuführen.

Bei komplizierteren Operationen wie Summieren, Multiplizieren oder Dividieren kann der Geschwindigkeitsunterschied in die Höhe schnellen.

3.4. Standardwerte

Standardwerte der primitiven Typen sind 0 (in der entsprechenden Darstellung, d. H. 0, 0.0d usw.) für numerische Typen, false für den booleschen Typ, \u0000 für den char-Typ. Für die Wrapper-Klassen ist der Standardwert null.

Dies bedeutet, dass die primitiven Typen Werte nur von ihren Domänen erhalten können, während die Referenztypen möglicherweise einen Wert (null) erhalten, der in gewissem Sinne nicht zu ihren Domänen gehört.

Obwohl es nicht als gute Praxis angesehen wird, Variablen nicht initialisiert zu lassen, weisen wir manchmal nach ihrer Erstellung einen Wert zu.

In einer solchen Situation sollten wir herausfinden, ob die Variable wirklich initialisiert wurde, wenn eine primitive Typvariable einen Wert hat, der dem Standardwert des Typs entspricht.

Es gibt kein solches Problem mit einer Wrapper-Klassenvariablen, da der Null-Wert ein offensichtlicher Hinweis darauf ist, dass die Variable nicht initialisiert wurde.

Verwendung

Wie wir gesehen haben, sind die primitiven Typen viel schneller und benötigen viel weniger Speicher. Daher möchten wir sie vielleicht lieber verwenden.Andererseits erlaubt die aktuelle Java-Sprachspezifikation keine Verwendung primitiver Typen in den parametrisierten Typen (Generika), in den Java-Sammlungen oder der Reflection-API.

Wenn unsere Anwendung Sammlungen mit einer großen Anzahl von Elementen benötigt, sollten wir Arrays mit einem möglichst „sparsameren“ Typ verwenden, wie im obigen Diagramm dargestellt.

Fazit

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

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

Beginnen Sie mit Spring 5 und Spring Boot 2 über den Learn Spring-Kurs:

>> SCHAUEN SIE SICH DEN KURS an

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.