Spis treści
- Rozdział 9 - Klasy
- Czym są klasy i do czego służą?
- Różnice między typami prymitywnymi i referencyjnymi
- Modyfikatory dostępu
- Podsumowanie i pytania – typy prymitywne i referencyjne, modyfikatory dostępu
- Pola klas
- Konstruktory
- Equals i porównywanie obiektów
- Referencje do obiektów
- Metody i pola statyczne
- Pakiety i importowanie klas
Zanim opowiemy sobie o kolejnych aspektach tworzenia klas, przyjrzymy się dwóm bardzo istotnym różnicom pomiędzy typami prymitywnymi oraz typami referencyjnymi.
Przypomnijmy, czym są oba te rodzaje typów:
- Typy prymitywne – są to typy wbudowane – Java oferuje 8 typów prymitywnych, które już poznaliśmy – są to: boolean, byte, short, int, long, float, double, oraz char. Są to elementy budujące, z których składają się typy złożone, definiowane przez programistów.
- Typy referencyjne – są to typy złożone, zdefiniowane w bibliotekach standardowych języka Java (jak np. typ String) oraz tworzone przez programistów poprzez pisanie klas (jak np. klasa Samochod z poprzedniego podrozdziału). Do typów złożonych zaliczają się także tablice.
W dalszej części rozdziału o klasach poznamy jeszcze kilka innych, niemniej ważnych, różnic.
Przechowywane wartości¶
Główną różnicą pomiędzy typami prymitywnymi a typami referencyjnymi jest to, że zmienne typów prymitywnych przechowują w pamięci komputera konkretne wartości, natomiast zmienne typu referencyjnego przechowują adresy obiektów w pamięci:
int a = 10; // zmienna a ma wartosc 10 boolean b = true; // zmienna b ma wartosc true // zmienna powitanie zawiera adres obiektu typu String w pamieci, // ktory przechowuje tekst Witajcie! String powitanie = "Witajcie!"; // zmienna rzeczywiste zawiera adres tablicy w pamieci, // ktora przechowuje 5 liczb rzeczywistych double[] rzeczywiste = new double[5];
Zagadnienie to obrazuje poniższy rysunek:
Adresy obiektów to także pewne wartości. Nie operujemy na nich co prawda bezpośrednio, ale możemy się nimi posługiwać:
public class ZmienneTypowZlozonych { public static void main(String[] args) { Samochod pierwszySamochod = new Samochod(); // 1 pierwszySamochod.ustawKolor("Czerwony"); // 2 pierwszySamochod.ustawPredkosc(80); Samochod drugiSamochod = pierwszySamochod; // 3 drugiSamochod.ustawKolor("Bialy"); // 4 System.out.println(pierwszySamochod); // 5 System.out.println(drugiSamochod); // 6 } }
Co zobaczymy na ekranie w wyniku wykonania powyższego programu?
- Program ten korzysta z klasy Samochod z poprzedniego podrozdziału, która zawiera m. in. pole kolor oraz metodę toString.
- Najpierw tworzymy nowy obiekt klasy Samochod i przypisujemy go do zmiennej pierwszySamochod (1), a następnie ustawiamy jego kolor i predkosc za pomocą metod ustawKolor i ustawPredkosc (2).
- W kolejnej linii tworzymy drugą zmienną typu Samochod o nazwie drugiSamochod i przypisujemy do niej zmienną pierwszySamochod (3). Następnie, ustawiamy kolor obiektu drugiSamochod na "Bialy" (4).
- Na końcu programu wypisujemy na ekran oba obiekty (5) (6).
Wynikiem działania programu jest:
Chwila, moment! Dlaczego oba obiekty są białego koloru?
Jak już wspomnieliśmy, zmienne typów referencyjnych to nie obiekty – to adresy obiektów. Gdy przypisujemy do jednej zmiennej typu referencyjnego wartość innej zmiennej typu referencyjnego, to kopiujemy adres obiektu docelowego, a nie obiekt docelowy.
Dlatego właśnie na ekranie zobaczyliśmy takie same wartości – ponieważ mamy jeden obiekt typu Samochod (utworzony w linii (1) za pomocą słowa kluczowego new) oraz dwie zmienne, które na niego wskazują:
Innymi słowy – poniższa linijka nie powoduje stworzenia kopii obiektu typu Samochod:
Samochod drugiSamochod = pierwszySamochod;
lecz przepisanie (skopiowanie) adresu obiektu, na który zmienna po prawej stronie operatora przypisania wskazuje.
Skoro mamy tylko jeden obiekt, to drugie wywołanie ustawKolor powoduje nadpisanie poprzedniej wartości koloru, którą ustawiliśmy na początku programu. Obie zmienne – pierwszySamochod oraz drugiSamochod – wskazują na ten sam obiekt w pamięci.
W takim razie można by pomyśleć, że instrukcja:
drugiSamochod.ustawKolor("Bialy");
spowodowała, że zmodyfikowana została zarówno zmienna pierwszySamochod, jak i zmienna drugiSamochod. Nie jest to jednak prawda – ani zmienna pierwszySamochod, ani zmienna drugiSamochod, nie została zmodyfikowana. Obiekt, który został zmodyfikowany, to ten, na który obie te zmienne wskazują – czyli obiekt typu Samochod, przechowywany gdzieś w pamięci komputera, którego adres przypisany jest do obu zmiennych: pirwszySamochod oraz drugiSamochod.
Tablice to także typy referencyjne, a skoro tak, to co zobaczymy w wyniku działania poniższego programu?
public class ZmienneITablica { public static void main(String[] args) { double[] rzeczywiste = new double[5]; rzeczywiste[0] = 3.14; double[] drugaTablica = rzeczywiste; drugaTablica[0] = 5; System.out.println( "Pierwszy element rzeczywiste: " + rzeczywiste[0] ); System.out.println( "Pierwszy element drugaTablica: " + drugaTablica[0] ); } }
Na ekranie zobaczymy:
Dlaczego zobaczyliśmy taki wynik? W tym programie mamy tylko jedną tablicę liczb rzeczywistych, utworzoną w linii:
double[] rzeczywiste = new double[5];
W poniższej linii nie kopiujemy tablicy, na którą wskazuje zmienna rzeczywiste, lecz adres tej tablicy:
double[] drugaTablica = rzeczywiste;
W wyniku tego, obie zmienne: rzeczywiste i drugaTablica, wskazują na ten sam obiekt – tablicę mogącą przechowywać pięć liczb rzeczywistych. Przypisanie wartość 5 do pierwszego elementu tej tablicy nadpisuje umieszczoną tam wcześniej wartość 3.14. Chociaż ustawienie każdej z tych wartości dokonaliśmy za pomocą innej zmiennej wskazującej na tę tablicę, to docelowo operowaliśmy na tej samej tablicy.
Tworzenie¶
By stworzyć zmienną typu prymitywnego, piszemy po prostu typ oraz nazwę zmiennej:
int a = 10; boolean b = true;
Natomiast tworzenie obiektów, na które będzie wskazywać zmienna typu referencyjnego, odbywa się, jak już wiemy, poprzez użycie słowa kluczowego new – ma ono za zadanie utworzyć nowy obiekt, do którego referencja zostanie zapisana w zmiennej, którą definiujemy:
Samochod pierwszySamochod = new Samochod(); String powitanie = "Witajcie!"; String java = new String("Java"); double[] rzeczywiste = new double[5]; double[] rzeczywiste2 = { 3.14, 5, -20.5 };
Typ String jest szczególnym typem złożonym w Javie – nie wymaga on użycia słowa kluczowego new dla wygody programistów – wystarczy po prostu przypisać do zmiennej typu String wartość, jak w powyższym przykładzie ze zmienną powitanie. Mało tego, słowo kluczowe new przy pracy ze stringami nie powinno być w ogóle używane ze względów wydajnościowych, ponieważ Java specjalnie przechowuje łańcuchy tekstowe.
Tablice także są szczególne, ponieważ zamiast używać słowa kluczowego new, możemy użyć skróconego zapisu z użyciem nawiasów klamrowych, w których umieszczamy elementy tablicy. Rozmiar tablicy będzie wydedukowany na podstawie liczby podanych elementów.
Typ String i tablice to dwa wyjątki w świecie typów złożonych, jeśli chodzi o możliwy sposób ich tworzenia.
Jeżeli jednak korzystamy ze słowa kluczowego new w celu utworzenia wartości typu String, to pamięć za każdym razem jest alokowana na nowo dla takiej wartości, nawet, jeżeli była ona już przechowywana w String pool, co może odbić się na wydajności naszej aplikacji – dlatego zawsze powinniśmy po prostu przypisywać do zmiennych typu String wartości za pomocą:
String nazwaZmiennej = "pewien tekst";