Informacje o nowych artykułach oraz akcjach edukacyjnych prosto na Twojej skrzynce e-mail!

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.

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?

int count = 3/4;

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ł?

int val = 4;

switch (val % 2.) { 
    case 0: System.out.println("It is ok");
}

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.

class Company {
    public static String name = "My company";
}

public class Employee {
    public static void main(String[] args) {
        Company company1 = new Company();
        Company company2 = new Company();
        company1.name = "It is not my company";
        company2.name = "It is my company";
        System.out.print(company1.name);
    }
}

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

It is my company

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.

public class Camel {

    public static final String name; //to pole i tak ma modyfikator package-private 
    private static final int age = 10;
    private static final String placeOfBirth;
    private static final int numberOfHumps; //to nigdzie nie jest inicjalizowane (błąd kompilacji)

    static {
        name = "Agnieszka";
        placeOfBirth = "Kraków";
    }

    static {
        placeOfBirth = "Warszawa"; //to jest niepoprawne
    }

    {
        numberOfHumps = 2; //to nie jest inicjalizacja zmiennej statycznej z klasy Camel (zwykły blok inicjalizujący)
    }

    public static void main(String[] args) {
        name = "Łukasz"; //to jest niepoprawe
    }
}

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.

public static void main(String[] args) {
    int variable;

    if(variable == 0) {
        System.out.println("Hello");
    }
}

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.

public class MyClass {
    int variable1; // instance variable
    public int variable2; // instance variable
    static int variable3; // class variable

    public MyClass() {
        int variable4; // local variable
    }
}

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

public class MyClass {
    public MyClass() {
        int num = 3;
        String arg;

        if(num == 3) {
            arg = "Hello";
        }

        System.out.print(arg);
    }
}

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.

int i1 = 1_234;
double d0 = 1_234.0;
double d1 = 1_234_.0; //to jest niepoprawne
double d2 = 1_234._0; //to jest niepoprawne
double d3 = 1_234.0_; //to jest niepoprawne

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?

int int1 = 0b101;
int int2 = 0xE;
double double1 = 0xE;
long long1 = 9L;

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.

public class MyClass {
    public static void main(String... args) {
        Long long1 = 300l;
        int a = long1.intValue(); // użycie metody intValue() na zmiennej typu Long
        int b = (int)long1; // to jest niepoprawne 
        long long2 = 300l;
        int c = long2.intValue(); // to jest niepoprawne – typ prosty to nie typ opakowujący
        int d = (int)long2; // a to już jest poprawne
    }
}

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.

Car one = new Car();
Car two = new Car();
Car tree = one;
one = null;
Car four = one;
tree = null;
two = null;
two = new Car();
System.gc();

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.

protected void finalize() {
    System.out.println("Goodbye!");
}

Popatrzmy na mały przykład.

public class Main {
    public static void main(String[] args) { 
        Main main1 = new Main();
        Main main2 = new Main();
        main1 = main2;
        main2 = null;
        main1 = null;
    }

    protected void finalize() {
        System.out.println("Finalize was run");
    }
}

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.

Long number1 = null;
long number2 = null; //błąd kompilacji – typ prosty nie przyjmuje null’a

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:

List list<Integer> = new ArrayList<Integer>();
list.add(Integer.parseInt("1")); //parseInt zwraca int - autoboxing
list.add(Integer.valueOf("2")); //valueOf zwraca Integer 
list.add(3); //autoboxing 
list.add(null);

for(int item : list)
     System.out.print(item);

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.

UWAGA: Jeśli interesuje Ciebie temat rzutowania typów oraz ich dziedziczenia, to zapraszam do materiału – Elementarz Java #7 – Dziedziczenie – gdzie zostało to szczegółowo omówione.

Spodobało się?

Jeśli tak, to zarejestruj się do newslettera aby otrzymywać informacje nowych artykułach oraz akcjach edukacyjnych. Gwarantuję 100% satysfakcji i żadnego spamowania!

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

Dodaj komentarz

Komentarze (2)

  • Paulina pisze:

    Hej! we fragmencie: „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.” jest błąd, powinno być „klasowych” ;)
    Cały elementarz to ogromna kopalnia wiedzy, bardzo mi się przydała :) Pozdrawiam

Odpowiedz

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

Pin It on Pinterest