Typy referencyjne vs typy proste
Jak wspomniałem we wcześniejszym wpisie w programowaniu wyróżniamy typy proste i typy referencyjne. Jak mogłeś/aś przeczytać wcześniej nie jest prawdą, że wszystko w Javie jest referencją. W praktyce jednak częściej będziemy operować na referencjach niż na konkretnych wartościach w pamięci komputera. Czym jest referencja i do czego służy? Otóż dawno, dawno temu, kiedy na świecie królował C++ programista sam musiał zarządzać pamięcią. Było to bardzo wydajne rozwiązanie, co biorąc pod uwagę ówczesne ceny pamięci RAM, miało sens. Niestety samodzielne przydzielanie pamięci wymaga olbrzymiej wiedzy i dyscypliny u programisty. W praktyce powodowało to mnóstwo problemów. Na szczęście pamięć z czasem stała się bardzo tania, więc twórcy Javy zaprojektowali własne algorytmy zarządzania pamięcią poprzez użycie wirtualnej maszyny Javy (ang. Java Virtual Machine).
Na tym kursie nie będę omawiał szczegółów, w jaki sposób Java radzi sobie z referencjami oraz jak po nich ‘sprząta’. Pamiętaj tylko o tym, że zarządzanie pamięcią w Javie odbywa się automatycznie, a pomaga w tym proces nazywający się ‘Garbage Collector’. W C++ sami zarządzamy pamięcią (m.in. używając destruktorów, których w Javie nie ma). W Javie nie ma także wskaźników (które ‘wskazują’ na odpowiednią referencje, co pozwala zoptymalizować zużycie pamięci).
Czym się różnią się typy referencyjne od typów prostych?
Najprościej referencję można zrozumieć jako etykietę do konkretnej wartości. Referencje można przypisywać innym zmiennym (aby wszystkie ZAWSZE wskazywały na tą samą komórkę pamięci, pod którą kryje się jakaś wartość). Typ prosty reprezentuje wartość a nie referencję, także nie można jego adres w pamięci przypisywać do innych zmiennych. Podejrzewam, że nie bardzo rozumiesz. 😉 Nie martw się spójrz na przykład poniżej, rozjaśni Ci on wszystkie wątpliwości.
public class ReferenceTypes { public static void main(String[] args) { int smallIntTen = 10; int smallIntCopy = smallIntTen; MyInteger objectIntTwenty = new MyInteger(20); MyInteger objectIntCopy = objectIntTwenty; System.out.println("Small int number: " + smallIntTen + ", small int copy " + smallIntCopy + ", object int number: " + objectIntTwenty.getValue() + ", object int reference: " + objectIntCopy.getValue()); smallIntTen++; System.out.println("Small int number after increment: " + smallIntTen + ", copied number value: " + smallIntCopy); objectIntCopy.increment(); System.out.println("Object int number after increment: " + objectIntTwenty.getValue() + ", copied number value: " + objectIntCopy.getValue()); } } public class MyInteger { private int value; public MyInteger(int value) { this.value = value; } public int getValue() { return value; } public void increment () { value++; } }
Efekt na konsoli:
Small int number: 10, small int copy 10, Object int number: 20, Object int reference: 20
Small int number after increment: 11, copied number value: 10
Object int number after increment: 21, copied number value: 21
Referencje w Javie
Jak łatwo zauważyć po zwiększeniu zmiennej smallIntTen o jeden (zapis wartość++ oznacza to samo co: wartość+1) jej kopia zachowała poprzednią wartość. Jest to logiczne w końcu przecież, gdy przypisałem ją do zmiennej smallIntCopy wynosiła ona 10. Jednak taka sama operacja dla typu obiektowego daje zgoła inny rezultat. Teoretycznie obiekt (napisanej przeze mnie klasy MyInteger), który przechowuje małego inta, powinien zachować się tak samo. Okazuje się, że tak nie jest, ponieważ wartość nie została skopiowana z jednej zmiennej do drugiej, jak w przypadku pierwszym. Tym razem została przypisana ta sama referencja do wartości. Jak widzisz, teraz mogę edytować tą samą wartość przy pomocy dwóch zmiennych: objectIntTwenty i objectIntCopy. Jeśli zmienię jedną, zmieni się też druga.
Modyfikatory dostępu
Spójrz jeszcze raz na klasę MyInteger. Posiada ona kilka ciekawych fragmentów. Pierwsze co powinno Ci się rzucić w oczy to słowa kluczowe private i public. Nie są one konieczne, ale napisałem je dla poprawności kodu. Są to modyfikatory dostępu (mają je tylko typy referencyjne), omówię je dokładnie w kolejnych wpisach.
Zauważyłeś na pewno jedną różnicę w przypadku klasy poprzedniej, brak metody main. Metoda main służy do odpalania programu i nie musi być w każdej klasie (wystarczy jedna na cały program). Od niej zaczyna działanie cała aplikacja (w uproszczonej wersji, bo nie zawsze tak musi być ;)). W niej też zadeklarowaliśmy zmienne typów prostych i referencyjnych (obiektowych w tym przypadku). Jeśli chcemy stworzyć obiekt nie zawsze możemy go zdefiniować przy pomocy znaku „=”, tak jak w typach prostych. Z reguły wykonuje się to używając słowa kluczowego ‘new’. Zapamiętaj, że obiekt (czyli egzemplarz) danej klasy tworzony jest poprzez new. Na koniec używając polecenia System.out.println zostały wyświetlone ich wartości na konsolę.
Struktura klasy
Klasy zawierają trzy specyficzne fragmenty: pola, konstruktory i metody. Pola to inaczej zmienne klasowe. Klasa ma do nich dostęp w każdym miejscu wewnątrz siebie. Pola mogą być tez dostępne poza klasą, jeśli ustawi się odpowiedni modyfikator (jak już mówiłem o tym później). Konstruktor to specyficzna metoda, której zadaniem jest stworzenie obiektu. Poza modyfikatorem zwraca typ tej samej klasy, w której jest zdefiniowany. W przeciwieństwie do metod nie posiada nazwy, może za to zawierać argumenty. W swoim ciele zazwyczaj zawiera operacje przypisania wartości do pól. Mówiąc wprost, konstruktor ustawia pola przed użyciem obiektu.
Czy można stworzyć obiekt klasy, która nie posiada konstruktora? Tak, kompilator Javy samodzielnie stworzy domyślny konstruktor. W tym przypadku byłby to: public MyInteger() {} . W zależności ile argumentów posiada konstruktor, tak też będzie wyglądało tworzenie obiektu. Ponieważ w klasie MyInteger istnieje tylko jeden konstruktor, który posiada jeden argument (value), to stworzenie obiektu wygląda tak: MyInteger myVariable = new MyInteger(1); Czy istnieje w takim razie pusty konstruktor domyślny? Nie, ponieważ ten dodawany jest automatycznie tylko wtedy, gdy nie napiszesz żadnego w klasie. Także taki kod: MyInteger myObject = new MyInteger(); nie zadziała. Ostatnią rzeczą, o której powinieneś wiedzieć, to metody. Metody to funkcje, które wykonują operacje na zmiennych/polach. Mogą, ale nie muszą zwracać wartości. Mogą, ale też nie muszą zawierać argumenty.
Metody zwracające wartości i void
Metoda, która zwraca wartość:
public int getValue() { return value; }
Metoda, która nic nie zwraca (tzw. typu void):
public void increment () { value++; }
Każda metoda musi mieć nazwę, modyfikatory także nie są obowiązkowe. Na koniec pokażę Ci jeszcze metodę z argumentem (którą już dobrze znasz).
public static void main(String[] args) { System.out.println(“Hello world!”); }
Jest to metoda void (nic nie zwraca) o nazwie ‘main’ z jednym argumentem (String[] args). Nie przejmuj się słowem static, je także wytłumaczę w następnych wpisach.
Obiekty to nie jedyne typy referencyjne jakie w Javie można spotkać, a są to:
- tablice
- obiekty
- adnotacje
- enumy (typy wyliczeniowe)
- interface’y
W kolejnej części wytłumaczę, czym się różni mały int od dużego Integer i jak jeden wiąże się z drugim.
Link do lekcji na githubie: https://github.com/developeronthego/java-basics/tree/main/src/main/java/basics/lesson2
Lind do lekcji jak stworzyć pierwszy projekt w środowisku Eclipse: https://developeronthego.pl/eclipse-srodowiska-programistyczne/