Rozdział 9 - Klasy - Różnice między typami prymitywnymi i referencyjnymi

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 rozdziale o metodach poznaliśmy już jedną różnicę – zmiany na obiektach typów złożonych przesłanych jako argumenty do metody wykonywane są na oryginalnych obiektach, a nie na kopiach. Wkrótce wrócimy do tego zagadnienia.
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:

Wskazanie na obiekty w pamieci przez zmienne typu zlozonego
Zmienne referencyjne (zmienne typów złożonych, czyli klas) nie są tworzonymi obiektami, lecz odniesieniami do utworzonych obiektów, które do nich przypisujemy. Natomiast zmienne typu prymitywnego przechowują wartości, które do nich przypisaliśmy.

Adresy obiektów to także pewne wartości. Nie operujemy na nich co prawda bezpośrednio, ale możemy się nimi posługiwać:

Nazwa pliku: ZmienneTypowZlozonych.java
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?

  1. Program ten korzysta z klasy Samochod z poprzedniego podrozdziału, która zawiera m. in. pole kolor oraz metodę toString.
  2. 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).
  3. 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).
  4. Na końcu programu wypisujemy na ekran oba obiekty (5) (6).

Wynikiem działania programu jest:

Jestem samochodem! Moj kolor to Bialy, jade z predkoscia 80 Jestem samochodem! Moj kolor to Bialy, jade z predkoscia 80

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ą:

Dwie zmienne typu zlozonego wskazuja na jeden obiekt w pamieci

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?

Nazwa pliku: ZmienneITablica.java
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:

Pierwszy element rzeczywiste: 5.0 Pierwszy element drugaTablica: 5.0

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.

Pamiętajmy: zmienne typów referencyjnych (klas) oraz zmienne tablicowe przechowują adresy obiektów tych klas i tablic.
Rozróżnianie adresów i docelowych obiektów jest ważne, ale mało praktyczne. Dlatego, gdy działamy na zmiennej pewnej klasy, operujemy nazwą tej zmiennej, mając na myśli docelowy obiekt. Nie mówimy w praktyce, że "zmienna samochod1 zawiera adres pewnego obiektu typu Samochod w pamięci", lecz, po prostu (ze względu na naszą wygodę), że zmienna samochod1 jest obiektem typu Samochod (mając jednak nadal świadomość, że nie jest to do końca prawda – samochod1 nie jest obiektem, lecz wskazaniem na obiekt).

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.

Ilekroć pracujemy z obiektami typu String, nigdy nie twórzmy ich za pomocą słowa kluczowego new – korzystajmy zawsze z bezpośredniego przypisywania ich do zmiennych.
Dlaczego nie powinniśmy tworzyć wartości typu String za pomocą słowa kluczowego new? Przygotowanie pamięci i tworzenie obiektów typu String może być czasochłonne, szczególnie, jeżeli będziemy tworzyli dużo łańcuchów tekstowych w naszych programach. Twórcy języka Java zoptymalizowali sposób przechowywania wartości typu String – w uproszczeniu, są one trzymane w tzw. String pool, który cache'uje użyte już raz wartości typu String, dzięki czemu ponownie użycie takiego samego stringa nie wymaga ponownego 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";

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.