Elementarz Java #2 – Typy danych w języku Java

Wstęp

Dzisiaj w drugim artykule z serii #ElementarzJava, przedstawię Ci dość krótki, ale bardzo ważny temat związany z typami danych. Na samym początku zacznę oczywiście od omówienia sposobów deklaracji oraz inicjalizacji zmiennych, stałych oraz ich statycznych odpowiedników. W dalszej kolejności przedstawiam definicje zmiennych lokalnych, instancyjnych oraz klasy i wyjaśniam jakie są między nimi różnice. Oczywiście nie mogło również zabraknąć tematu związanego z wartościami domyślnymi, typami opakowującymi i Garbage Collection. Mimo, że jest to jeden z krótszych (o ile nie najkrótszych) artykułów z tej serii to jednak trochę nowej wiedzy na pewno zdobędziesz. Zapraszam do dalszej lektury i dyskusji w sekcji komentarzy.

Seria #ElementarzJava składa się z następujących artykułów:

  1. Podstawy języka Java (kompilacja, zmienne, struktura klasy, pakiety),
  2. Typy danych w języku Java (deklaracja i inicjalizacja zmiennych, różnica między typami, garbage collection, typy opakowujące)
  3. Operatory i konstrukcje warunkowe w Java (użycie operatorów, porównywanie obiektów, instrukcje: if, if/else, switch),
  4. Tablice (charakterystyka tablic, tablice jedno i wielowymiarowe),
  5. Pętle (tworzenie pętli, instrukcje break i continue, etykiety),
  6. Metody oraz hermetyzacja (metody statyczne, przeciążanie metod, konstruktory, modyfikatory widoczności, hermetyzacja elementów klasy, parametry) – 21.07,
  7. Dziedziczenie (implementacja, przysłanianie metod, polimorfizm, rzutowanie) – 4.08,
  8. Obsługa wyjątków (kategorie wyjątków, łapanie wyjątków, klasy wyjątków) – 18.08,
  9. API Java (String, StringBuilder, data i czas, kolekcje, wyrażenia lambda) – 1.09.

Przy wpisach, które nie zostały jeszcze opublikowane, umieszczone są daty ich planowanej premiery.

Jesteś tu pierwszy raz? Polecam rozpocząć lekturę od materiału: Elementarz Java #0 – zapowiedź.

Deklaracja i inicjalizacja zmiennych 

Sama deklaracja i inicjalizacja zmiennych w Javie nie różni się niczym od innych języków programowania. Warto jednak zwrócić uwagę na kilka istotnych szczegółów.

Pierwsze pytanie: czy taki kod skompiluje się poprawnie?

Odpowiedź brzmi – tak. Wartość zmiennej count będzie w tym wypadku wynosiła po prostu 0.

Druga ciekawostka, czy ten fragment będzie działał?

Tutaj sprawa nie jest już tak prosta jak wcześniej. Przede wszystkim w instrukcji switch mamy wyrażenie val % 2.. Dzielimy więc wartość zmiennej val, która ma typ int przez double. Wynik tego wyrażenia będzie doublem. Zgodnie ze specyfikacją Javy instrukcja wielokrotnego wyboru switch może przyjmować wyrażenia typu: char, byte, short, int, Character, Byte, Short, Integer, String lub enum. Nie mamy tutaj dozwolonego double. Dostaniemy błąd kompilacji.

Nazwy zmiennych

Specyfikacja Javy definiuje kilka reguł dotyczących nazewnictwa zmiennych:

  1. nazwa powinna zaczynać się od litery (a-z lub A-Z), znaku $, lub „_”. Nie ma limitu długości,
  2. nazwa można zawierać cyfry, ale nie może się od nich zaczynać,
  3. znak $ oraz „_” może występować w dowolnym miejscu,
  4. nie możemy używać słów kluczowych Javy (np. switch),
  5. nie możemy używać znaków specjalnych takich jak: !, @, #, %, ^, &, *, (, ), ‘, :, ;, [, ], /, \ oraz {, }.

Zmienne statyczne

W artykule na temat podstaw Javy opisywałem już w jakie sposób działają zmienne statyczne. Posługując się nimi tak naprawdę operujemy na referencji do adresu w pamięci komputera, gdzie przechowywana jest ich zawartość. Oznacza to dla nas tyle, że dowolna modyfikacja zwartości takiego pola w dowolnym momencie oraz kontekście cały czas wpływa na tą samą wartość. Popatrzmy na przykład.

Efekt działania powyższego kodu będzie następujący:

Bierzemy tutaj pod uwagę ostatnią modyfikacje zmiennej statycznej. Nie interesuje nas w jakim kontekście została ona wywołana.

Kiedy mają być inicjalizowane finalne pola statyczne?

Ciekawy problem powstaje, kiedy nasze pole statyczne będzie równocześnie stałą. Jak wiemy stałe to zmienne, które po inicjalizacji nie mogę być modyfikowane. Jeśli myślisz, że pole statyczne może zostać zainicjalizowane choćby poprzez konstruktor to muszę wyprowadzić Ciebie jak najszybciej z tego błędu. Do pola statycznego możemy mieć przecież dostęp bez tworzenia instancji obiektu, więc konstruktor nie będzie tutaj uruchamiany. W grę nie wchodzi również definicja takiego pola bez przypisania wartości. Przy takiej próbie otrzymamy błąd kompilacji. Kiedy zatem pole statyczne może zostać zainicjowane?

Na to pytanie są dwie odpowiedzi. Po pierwsze, podczas jego definiowania, po drugie, przez statyczny blok inicjalizujący (pisałem o nich w poprzednim artykule).

Pobawmy się trochę taką zmienną i przenalizujmy taki oto przykład.

Na początek małe wyjaśnienie. Przy definicji pola name dopisałem komentarz, że to pole i tak ma modyfikator package-private (czyli domyślny). Po lewej stronie jest jednak napisane dużymi literami słowo „public” o co więc tutaj chodzi? Otóż wszystkie pola statyczne w danej klasie mogą mieć modyfikator domyślny lub bardziej restrykcyjny, czyli prywatny. Nawet gdy wpiszemy tam słowo public to i tak nie będzie dostępu do tej zmiennej z poziomu innego pakietu (chyba, że wykonamy odpowiedni import).

W kolejnej linijce mamy zdefiniowane pole age, które jest poprawnie zainicjalizowane. Kolejny element naszej klasy, czyli zmienna placeOfBirth jest również poprawnie zainicjalizowana tym razem przez statyczny blok inicjalizujący. Pole numberOfHumps nie jest już jednak nigdzie inicjalizowane. Miejsce, w którym przypisujemy do tej zmiennej wartość dwa to zwykły blok inicjalizujący. Otrzymujemy więc błąd kompilacji.

Stałe jak sama nazwa wskazuje nie mogą być ponownie modyfikowane. Tak więc linijka w metodzie main gdzie przypisujemy do zmiennej name słówko „Łukasz” powoduje kolejny błąd kompilacji.

Zmienne lokalne, instancyjne i klasy

Zmienne mogą mieć kilka zakresów. Specyfikacja Javy wyróżnia: zmienne klasy (ang. class variable), zmienne instancyjne (ang. instance variable) oraz zmienne lokalne (ang. local variable). Zmienne lokalne definiowane są wewnątrz metod. Zaliczają się do nich również zmienne definiowane w pętlach. Dostęp do nich (co opisywałem w artykule na temat podstaw Javy) możliwy jest tylko w metodzie, w której taka zmienna została zdefiniowana. Zmienne lokalne muszą zostać zainicjalizowane przez programistę. Nie jest dopuszczalne na przykład takie rozwiązanie.

Przy takim kodzie dostajemy błąd kompilacji. Zanim użyjemy zmiennej variable (jest ona lokalna) musimy ją zainicjalizować.

Instance variable są definiowane wewnątrz klasy poza metodami (są inicjalizowane, kiedy obiekt danej klasy zostanie stworzony).

Class variable są wspólne dla wszystkich obiektów klasy. Można do nich uzyskać dostęp nawet jeśli nie ma utworzonej instancji.

Jak wspomniałem wcześniej. Przed użyciem zmiennych lokalnych musimy je najpierw zainicjalizować. W przypadku zmiennych lokalnych i instancyjnych nie musimy tego robić. Przyjmują one domyślne wartości.

Taki kod nie zostanie skompilowany poprawnie. Zmienna arg jest zmienną lokalną więc przed użyciem musi zostać zainicjalizowana. Co prawda jej inicjalizacja odbywa się w instrukcji if, ale jest ona uzależniona od wartości zmiennej num. Problem polega na tym, że kompilator w momencie kompilacji „nie wie”, że warunek ten zostanie spełniony. Dostaniemy więc błąd kompilacji. Moglibyśmy go uniknąć, gdyby zmienna num była stałą (ale tylko wtedy, jeśli była by ona inicjalizowana w momencie deklaracji).

Wartości domyślne

  1. byte: 0,
  2. short: 0,
  3. int: 0,
  4. long: 0L,
  5. float: 0.0f,
  6. double: 0.0d,
  7. char: 'u0000',
  8. String (lub inny typ obiektowy): null,
  9. boolean: false.

Czytelniejszy zapis liczb

Java również umożliwia nam zastosowanie znaku „_” do czytelniejszego zapisu liczb. Jak go używać? Tutaj zasada jest prosta. Wstawiamy go tam, gdzie nie jest „nadmiarowy”. Zobaczmy kilka przykładów.

Wartości binarne, szesnastkowe…

Możemy też za pomocą specjalnego prefixu przypisywać do zmiennych wartości binarne lub też szesnastkowe. Jak to wygląda w praktyce?

Zastosowane prefixy:

0b – prefix liczby binarnej,

0x – prefix liczby szesnastkowej (hex),

9L – to long.

Różnica między typami prostymi a obiektowymi

Podstawowa różnica między typami prostymi, a obiektowymi polega na tym, że typy proste takie jak byte, short, int, long… w przeciwieństwie do typów obiektowych nie przyjmują wartości null. Wiąże się z tym takie pojęcie jak boxing i autoboxing o którym piszę dokładniej nieco niżej. To jednak nie wszystko. Typy opakowujące traktowane są jak obiekty. Oznacza to, że mając zmienną na przykład typu Long (pisane z dużej litery) możemy traktować ją jako instancję obiektu, a co za tym idzie wywoływać szereg metod, które ten obiekt dostarcza.

Na powyższym przykładzie widać, że rzutowanie obiektu typu Long odbywa się przez dostarczoną przez ten obiekt metodę intValue(). Nie możemy zrobić tego w taki sposób jak odbywa się to w przypadku prymitywów. To samo działa zresztą w drugą stronę. Typy prymitywne nie dostarczają nam żadnych metod, które moglibyśmy wywołać.

Cykl życia obiektu oraz Garbage Collection

Java w przeciwieństwie do C++ nie udostępnia takiego rozwiązania jak destruktor. Aby odpowiednio zarządzać dostępną pamięcią został w tym celu stworzony specjalny mechanizm sterowany przez maszynę wirtualną Javy, a odpowiedzialny za usuwanie niepotrzebnych danych z pamięci. Mowa oczywiście o Garbage Collection. Oznacza to tyle, że podczas pracy nad kodem mamy o jedno zmartwienie mniej, nie musimy samodzielnie zarządzać pamięcią. Zobaczmy na krótki przykład.

Kiedy może zostać uruchomiony garbage collection? Odpowiedź na to pytanie jest dość prosta. Stanie się to po linijce 6 i 7 (oczywiście jak JVM stwierdzi, że jest taka potrzeba). Dlaczego wtedy? Mamy obiekt Car, stworzony w linii 1, którego referencja kopiowana jest również w linii 3. W następnej linijce do one przypisujemy null, ale cały czas obiekt ten jest używany. Dopiero w linii 6 tracimy do niego dostęp. Wtedy gc może zadziałać. Analogiczny tok rozumowania dotyczy linijki 7.

Wywołanie gc bezpośrednio z kodu

Garbage Collection można wywołać bezpośrednio z kodu Javy. Służy do tego polecenie System.gc(). Sugeruje ono, że Java chce uruchomić garbage collection ale pamiętajmy, że to nie programista decyduje czy tak się faktycznie stanie.

Metoda finalize()

Metoda finalize() jest wywoływana w momencie w którym zostanie uruchomiony garbage collection. Dotyczy to oczywiście obiektu w którym została ona nadpisana.

Popatrzmy na mały przykład.

Co tutaj się stanie? Po pierwsze dwukrotnie zadziała garbage collection. To już powinno być jasne z poprzednich przykładów. Ale drugi element jest ciekawszy. Wydawać by się mogło, że metoda finalize() zostanie uruchomiona dwa razy. Za każdym razem, kiedy gc działał. Nieprawda. Zgodnie ze specyfikacją Javy dla danego typu obiektu może być ona wywołana tylko jeden raz w trakcie działania programu.

Użycie klas opakowujących typy proste

Pisałem już o różnicy między typami prostymi i obiektowymi. Nad tym tematem warto się jednak nieco bardziej pochylić. Takim klasycznym przykładem są tutaj prymitywy i klasy opakowujące. Spójrzmy na poniższy kod.

Na tym prostym przykładzie widać bardzo dobrze różnicę między typem prostym long, a typem opakowującym Long.

Autoboxing i unboxing

Wspomniałem już kilka razy na temat autoboxingu. Dochodzi do niego, jeśli do typu opakowującego próbujemy przypisać prymitywa. Zobaczmy na krótki przykład:

Mamy tutaj kolekcję, która przyjmuje obiekty typu Integer. Przypisanie int’a spowoduje uruchomienie autoboxingu – co zresztą widzimy na przykładzie. Kod ten skompiluje się poprawnie ale podczas jego działania zobaczymy wyjątek NullPointerException. Dzieje się tak dlatego, że pętla for, odpowiedzialna za iteracje kolekcji, przyjmuje obiekt typu int. Przy każdym przejściu będzie więc dochodziło do unboxingu. Oczywiście do momentu kiedy nie natrafimy na ostatnim element. Wielokrotnie już o tym pisałem, int nie może przyjmować wartości null. Stąd więc wspomniany wyjątek.

Typy proste (primitive type) i ich odpowiedniki obiektowe (wrapper class)

Typy prosteTypy opakowujące
booleanBoolean
byteByte
charCharacter
floatFloat
intInteger
longLong
shortShort
doubleDouble
Tabela pokazująca typy proste i ich obiektowe odpowiedniki

Podsumowanie

To by było na tyle jeśli chodzi o typy danych w języku Java. Mam nadzieję, że materiał ten był dla Ciebie pomocny i poszerzył Twój zakres wiadomości na ten temat. Już teraz zapraszam do lektury kolejnego artykułu, który ukaże się równo za dwa tygodnie oraz do zapoznania się z poprzednim wpisem z tej serii. Zgrabny spis treści wszystkich materiałów na temat Javy znajdziesz w moim artykule zapowiadającym tą serię.

Przeczytaj również

, , , , , , , , , , , , , , , , , , ,