Java Primitives versus Objects

przegląd

w tym samouczku pokażemy zalety i wady używania prymitywnych typów Javy i ich opakowanych odpowiedników.

Java Type System

Java ma system typu dwuskładnikowego składający się z typów podstawowych, takich jak int, boolean i typów referencyjnych, takich jak Integer, Boolean. Każdy typ prymitywny odpowiada typowi referencyjnemu.

każdy obiekt zawiera jedną wartość odpowiedniego typu primitive. Klasy wrapper są niezmienne (tak, że ich stan nie może się zmienić po zbudowaniu obiektu) i są ostateczne (tak, że nie możemy od nich dziedziczyć).

pod maską Java wykonuje konwersję pomiędzy typami prymitywnymi i referencyjnymi, jeśli rzeczywisty typ jest inny niż zadeklarowany:

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

proces konwersji typu prymitywnego na referencyjny nazywa się autoboxing, proces odwrotny nazywa się unboxing.

plusy i minusy

decyzja, jaki obiekt ma być użyty, zależy od tego, jaką wydajność aplikacji staramy się osiągnąć, ile mamy dostępnej pamięci, ilość dostępnej pamięci i jakie wartości domyślne powinniśmy obsłużyć.

Jeśli nie napotkamy żadnego z nich, możemy zignorować te rozważania, choć warto je znać.

3.1. Pojedynczy element pamięci Footprint

tylko dla odniesienia, zmienne typu prymitywnego mają następujący wpływ na pamięć:

  • boolean – 1 bit
  • bajt – 8 bitów
  • krótki, char – 16 bitów
  • int, float – 32 bity
  • długi, double – 64 bity

w praktyce wartości te mogą się różnić w zależności od implementacji maszyny Wirtualnej. Na przykład w maszynie wirtualnej Oracle Typ boolean jest mapowany do wartości int 0 i 1, więc zajmuje 32 bity, jak opisano tutaj: typy i wartości prymitywne.

zmienne tego typu żyją w stosie i dlatego są szybko dostępne. Aby uzyskać szczegółowe informacje, polecamy nasz samouczek na temat modelu pamięci Java.

typy odniesienia są obiektami, żyją na stosie i są stosunkowo wolne do dostępu. Mają pewne koszty związane z ich prymitywnymi odpowiednikami.

konkretne wartości napowietrznych są ogólnie specyficzne dla JVM. Poniżej prezentujemy wyniki dla 64-bitowej maszyny Wirtualnej o następujących parametrach:

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)

aby uzyskać wewnętrzną strukturę obiektu, możemy użyć narzędzia Java Object Layout tool (zobacz nasz inny samouczek, Jak uzyskać rozmiar obiektu).

okazuje się, że pojedyncza instancja typu referencyjnego na tym JVM zajmuje 128 bitów z wyjątkiem Long i Double, które zajmują 192 bity:

  • Boolean – 128 bitów
  • Byte – 128 bitów
  • Short, Character – 128 bitów
  • Integer, Float – 128 bitów
  • Long, Double – 192 bitów

widzimy, że pojedyncza zmienna typu Boolean zajmuje tyle miejsca, co 128 prymitywnych, podczas gdy jedna zmienna całkowita zajmuje tyle miejsca, co cztery int.

3.2. Footprint pamięci dla tablic

sytuacja staje się bardziej interesująca, jeśli porównamy ilość pamięci zajmowanej przez tablice danych typów.

Gdy tworzymy tablice z różną liczbą elementów dla każdego typu, otrzymujemy Wykres:

, który pokazuje, że typy są zgrupowane w cztery rodziny w odniesieniu do tego, w jaki sposób pamięć m(s) zależy od liczby elementów s tablicy:

  • long, double: m(s) = 128 + 64 s
  • short, char: m(s) = 128 + 64 s S) = 128 + 64
  • bajt, Boolean: m(s) = 128 + 64
  • reszta: m (s) = 128 + 64

gdzie nawiasy kwadratowe oznaczają standardową funkcję sufitu.

Co zaskakujące, tablice prymitywnych typów long I double zużywają więcej pamięci niż ich klasy wrapper Long i Double.

widzimy albo, że tablice jednoelementowe typów prymitywnych są prawie zawsze droższe (z wyjątkiem długich i podwójnych) niż odpowiedni typ odniesienia.

3.3. Wydajność

wydajność kodu Java jest dość subtelną kwestią, zależy ona w dużym stopniu od sprzętu, na którym działa Kod, od kompilatora, który może wykonywać pewne optymalizacje, od stanu maszyny wirtualnej, od aktywności innych procesów w systemie operacyjnym.

jak już wspomnieliśmy, typy prymitywne żyją w stosie, podczas gdy typy referencyjne żyją w stosie. Jest to dominujący czynnik, który określa, jak szybko obiekty uzyskać dostęp.

aby zademonstrować, o ile operacje dla typów prymitywnych są szybsze niż operacje dla klas wrapper, stwórzmy tablicę pięciu milionów elementów, w której wszystkie elementy są równe z wyjątkiem ostatniego; następnie wykonamy wyszukiwanie dla tego elementu:

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

i porównamy wydajność tej operacji dla przypadku, gdy tablica zawiera zmienne typów prymitywnych i dla przypadku, gdy zawiera obiekty typów referencyjnych.

używamy dobrze znanego narzędzia benchmarkingowego JMH (zobacz nasz samouczek, jak go używać), a wyniki operacji wyszukiwania można podsumować na tym wykresie:

nawet dla tak prostej operacji widzimy, że potrzeba więcej czasu na wykonanie operacji dla klas opakowujących.

w przypadku bardziej skomplikowanych operacji, takich jak sumowanie, mnożenie lub dzielenie, różnica w prędkości może gwałtownie wzrosnąć.

3.4. Wartości domyślne

wartości domyślne typów prymitywnych to 0 (W odpowiedniej reprezentacji, tzn. 0, 0.0D itd) dla typów liczbowych, false dla typu boolean, \u0000 dla typu char. Dla klas wrapperów wartością domyślną jest null.

oznacza to, że typy prymitywne mogą nabywać wartości tylko ze swoich domen, podczas gdy typy referencyjne mogą nabywać wartość (null), która w pewnym sensie nie należy do ich domen.

chociaż nie jest dobrą praktyką pozostawianie zmiennych niezainicjalizowanych, czasami możemy przypisać wartość po jej utworzeniu.

w takiej sytuacji, gdy zmienna typu prymitywnego ma wartość równą jej typowi default one, powinniśmy dowiedzieć się, czy zmienna została rzeczywiście zainicjowana.

nie ma takiego problemu ze zmiennymi klasy wrapper, ponieważ wartość null jest dość oczywistym wskazaniem, że zmienna nie została zainicjowana.

użycie

jak widzieliśmy, prymitywne typy są znacznie szybsze i wymagają znacznie mniej pamięci. Dlatego możemy woleć ich używać.

z drugiej strony, aktualna specyfikacja języka Java nie pozwala na użycie typów prymitywnych w typach parametryzowanych (generics), w kolekcjach Java lub API Reflection.

gdy nasza aplikacja potrzebuje kolekcji z dużą liczbą elementów, powinniśmy rozważyć użycie tablic o jak najbardziej „ekonomicznym” typie, jak to pokazano na powyższym wykresie.

wniosek

To ten poradnik, zilustrowaliśmy, że obiekty w Javie są wolniejsze i mają większy wpływ na pamięć niż ich prymitywne analogi.

jak zawsze, fragmenty kodu można znaleźć w naszym repozytorium na Githubie.

zacznij od Spring 5 i Spring Boot 2, Poprzez kurs Learn Spring:

>> sprawdź kurs

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *