następny punkt »

1. Obiekty i referencje

Za wyjątkiem operacji na liczbach, programowanie w Javie - tak naprawdę - polega na posługiwaniu się obiektami. Jak pamiętamy z wykładu 4 obiekty są egzemplarzami klas.
Np. łańcuchy znakowe (napisy) są egzemplarzami klasy String, a klasa ta definiuje wspólne właściwości tego rodzaju obiektów (napisów). Klasa String jest jedną z wielu klas "standardowych", dostarczonych wraz ze środowiskiem Java 2 SDK, już gotową do wykorzystania przez programistę. Klasy możemy definiować sami (za chwilę zobaczymy jak), możemy także korzystać z klas, które przygotowali jacyś inni programiści (i które nie wchodzą w skład klas "standardowych").
Przypomnijmy sobie również, że na obiektach operujemy wydając im polecenia (komunikaty do obiektów), a wydawanie poleceń nazywane jest wywoływaniem metod (odpowiednika pojęcia funkcji) na rzecz obiektów.

Wyobraźmy sobie teraz, że ktoś przygotował dla nas klasę o nazwie Para i że jej obiekty są parami liczb całkowitych tzn. każdy obiekt składa się z dwóch elementów - liczb całkowitych (pierwszego składnika pary i drugiego składnika pary).
To co możemy robić z obiektami klasy Para określone jest przez zestaw metod tej klasy (poleceń, które można wydawać obiektom-parom liczb całkowitych). Wyobraźmy sobie dalej, że w klasie Para są zdefiniowane następujące polecenia (metody):

  • o nazwie set - ustalająca wartość pary (obu składników pary)
  • i o nazwie show - pokazująca parę (wyprowadzająca na konsolę oba składniki pary)

Ta informacja wystarczy nam teraz by posługiwać się obiektami klasy Para (jej budowę poznamy w następnych wykładach).

Co trzeba zrobić, żeby ustalić wartość pary i wyprowadzić ją na konsolę?

Po pierwsze, musimy mieć zmienną, która będzie oznaczać obiekt- parę.
Po drugie, musimy utworzyć obiekt-parę.
Po trzecie, temu obiektowi trzeba wydać polecenia set i show.

Może to wyglądać w następujący sposób:

public class ParaSetAndShow {

  public static void main(String[] args) {

    Para para1 = new Para(); // 1
    para1.set( 1, 2 );       // 2
    para1.show();            // 3

  }
}

Wydruk działania programu:
( 1 , 2 )
Po kolei:


  • w wierszu oznaczonym 1 deklarujemy zmienną o nazwie para1 i tworzymy obiekt parę za pomocą wyrażenia new (jak widać wyrażenie new składa się ze słowa kluczowego new i następującej po nim nazwie klasy obiektu oraz nawiasów okrągłych, w których - jak się można domyslić - podawane mogą być - jako argumenty - informacje niezbędne do tworzenia obiektu. Tu nie podajemy takich informacji. Po następnym wykładzie będziemy dobrze rozumieć dlaczego wyrażenie new ma taką formę);
  • teraz mamy już (jeden) obiekt-parę i zmienną (o nazwie para1) za pomocą której będziemy na tym obiekcie operować;
  • w wierszu // 2 do obiektu oznaczanego przez zmienną para1 posyłamy polecenie set. Aby posłać polecenie musimy wybrać jakiego konkretnie obiektu ma ono dotyczyć. Temu wyborowi służy kropka (.), nazywana selektorem . Gdy piszemy para1. - znaczy to, że następujące po kropce polecenie ma być wysłane do obiektu oznaczanego przez zmienną para1. Poleceniem tym jest set. W nawiasach okrągłych podajemy jego argumenty (dodatkową informację, która ma być uwzględniona przy wykonaniu polecenia). W tym przypadku pierwszy argument jest wartością, która ma być nadana pierwszemu składnikowi pary, a drugi - drugiemu. W sumie para1.set( 1, 2); znaczy: ustalić wartość pierwszego elementu obiektu (składnika pary) oznaczanego przez zmienną para1 na 1, a drugiego elementu (składnika pary) - na 2;
  • w wierszu // 3 do obiektu, oznaczanego przez para1 posylamy komunikat show ("pokaż się") i w rezultacie na konsolę wyprowadzana jest "wartość pary", w tym przypadku podana jako ( wartość_pierwszego_skladnika , wartość_drugiego_skladnika ) czyli ( 1 , 2 );

Nieco niepokojące jest w tym opisie ciagłe używanie sformułowania "obiektu oznaczanego przez (zmienną) para1". Czyż nie łatwiej byłoby mowić: obiektu para1, tak jak mówimy np liczby calkowitej x? I w ogóle czym jest, tak naprawdę, zmienna para1?
Widzimy wyraźnie, że została ona zadeklarowana, ale jednocześnie pojawił się nieco dziwny punkt o tworzeniu obiektu.
Co by się stało gdybyśmy po prostu napisali:

Para para1;
para1.set( 1, 2 );
para1.show();

Jak już było powiedziane, deklaracje zmiennych, które oznaczają obiekty zapisujemy w analogiczny sposób jak deklaracje zmiennych typów pierwotnych.

int x; // deklaracja zmiennej typu int
Para p; // deklaracja zmiennej p, za pomocą której możemy operować na obiektach klasy Para
Pomiędzy tymi deklaracjami występuje jednak subtelna różnica znaczeniowa.

Otóż, deklaracja zmiennej x wydziela pamięć dla przechowywania liczby całkowitej (cztery bajty). W tym momencie x jest synonimem jednostki danych - liczby całkowitej.
Piszemy np. x = 4; i do miejsca pamięci oznaczanego przez zmienną x wpisywana jest liczba 4. Wygląda to mniej więcej tak:

Rys

W innych językach programowania (np. C++) mówi się, że jest to deklaracja wraz z definicją

Zatem sama deklaracja zmiennej calkowitoliczbowej x tworzy "obiekt" - liczbę całkowitą (przed ustaleniem wartości zmiennej liczba ta ma jakąś domyślną wartość, być może 0).

W przypadku deklaracji zmiennej, która może oznaczać obiekt jakiejś klasy sytuacja jest zupełnie inna. Deklaracja nie tworzy obiektu (nie wydziela pamięci do przechowywania obiektu klasy).
Sam obiekt musi być dopiero utworzony - za pomocą wyrażenia new.
Jego zastosowanie powoduje przydzielenie pamięci dla obiektu w dynamicznej (zmieniającej się w trakcie działania programu) części pamięci, zwanej stertą.
Wynikiem wyrażenia new jest adres (lokalizacja) miejsca w pamięci, przydzielonego obiektowi. Ten adres możemy przypisać zmiennej, za pomocą której chcemy na danym obiekcie operować.

Np. deklaracja:

Para p;

nie tworzy obiektu klasy Para.

A jeśli nie ma obiektu, to nie możemy posłać do niego żadnego komunikatu (wydać polecenia, wywołać na jego rzecz metody). Zatem, w tym kontekście, p.set(...) i p.show() będą niepoprawnymi odwołaniami.

Zatem zmienna p nie zawiera obiektu Para.
Może natomiast zawierać jego lokalizację (adres w pamięci) - inaczej nazywaną referencją do obiektu.

Referencja to wartość, która oznacza lokalizację (adres) obiektu w pamięci

Obiekt klasy Para możemy utworzyć używając wyrażenia new Para(), a przypisując wartość tego wyrażenia zmiennej p, uzyskujemy możliwość operowania na tym obiekcie:

Para p;
p = new Para();

co wcześniej, w skrócie, zapisywaliśmy stosując inicjację przy deklaracji:

Para p = new Para();


Dokładnego wyjaśnienia dostarcza poniższy ideowy schemat

Rys

W literaturze polskiej rozróżnia się czasem dwa pojęcia:
  • odniesienie - to co tutaj nazywamy referencją do obiektu, lokalizacją, adresem obiektu.
  • odnośnik - zmienną, której wartością jest odniesienie
Nota bene, w niniejszych wykladach będziemy posługiwać się czasem terminem "referencja" na oznaczenie obu tych pojęć.. Różnice będą zawsze jasne z kontekstu.

gdzie:
  1. Przydzielenie pamięci zmiennej p do przechowania referencji do obiektu. Referencja jest nieustalona, ma wartość null, co oznacza, że nie odnosi się do żadnego obiektu.
  2. Opracowanie wyrażenia new powoduje przydzielenie pamięci dla obiektu klasy Para na stercie pod jakimś wolnym adresem (tu symbolicznie 1304). Wielkość przydzielonego obszaru jest wystarczająca, by zmieścić dwie liczby całkowite (składniki pary). W tym momencie oba skladniki pary równe są 0.
  3. Wartością wyrażenia new jest referencja (adres 1304). Jest ona umieszczana w uprzednio przydzielonym zmiennej p obszarze pamięci
  4. Zmienna p ma teraz wartość = referencji do obiektu klasy Para, któremu w kroku 2 przydzialono pamięć na stercie (adres 1304).

Zatem zmienna p w naszym przykładzie zawiera (w końcu) referencję do obiektu klasy Para. Powiemy też "wskazuje na obiekt". Powiemy też czasem w skrócie: jest referencją.
I: "referencja wskazuje na obiekt".
Mówiliśmy wcześniej: "zmienna p może oznaczać obiekt klasy Para" w tym właśnie sensie, iż może zawierać referencję do obiektu klasy Para (zatem jakoś go "oznaczać", ale na pewno nie zawierać). A dlaczego może? Bo nie zawsze zawiera referencję do obiektu, czasami (np. zaraz po deklaracji bez inicjacji) nie zawiera referencji do żadnego obiektu (bo żaden nie został jeszcze utworzony).

Powstaje pytanie - jakiego typu jest zmienna p i wszystkie podobne zmienne, te o których mówiliśmy, że mogą oznaczać obiekty?

Otóż w Javie oprócz typów numerycznych i typu boolowskiego istnieje jeszcze tylko jeden typ - typ referencyjny.

Wszystkie zmienne deklarowane z nazwą klasy w miejscu nazwy typu są zmiennymi typu referencyjnego. Zmienne te mogą zawierać referencje do obiektów lub nie zawierać żadnej referencji (nie wskazywać na żaden obiekt).

Wartość zmiennej typu referencyjnego, która nie zawiera referencji do obiektu równa jest null. Słowo null jest słowem kluczowym języka

Zatem dopuszczalne wartości zmiennych typu referencyjnego - to referencje do obiektów lub wartość null. Tak samo jak 1 jest literałem typu int - null jest literałem typu referencyjnego.

Referencje są bardzo podobne do wskaźników w C, z tą istotną różnicą, że nie ma w Javie arytmetyki "na referencjach". Dzięki temu programowanie w Javie jest bardziej odporne na błędy. Arytmetyka wskaźnikowa w C jest częstą przyczyną błędów, gdyż pozwala sięgać do dowolnego miejsca w pamięci (np. poprzez zwiększanie wskaźnika, który wskazuje na obszar przydzielony jakiejś zmiennej).

Dla wartości typów referencyjnych (które to wartości w istocie są liczbami, bo adresy obiektów są liczbami) nie są dopuszczalne operacje arytmetyczne. Możemy natomiast:
  • porównywać referencje na równość (==) lub nierówność (!=),
  • przypisywać im wartości innych referencji oraz wartość null

Musimy zawsze pamiętać, że operacje te (wykonywane na zmiennych, oznaczających obiekty) dotyczą referencji, a nie obiektów (na obiektach, ich wnętrzu operujemy za pomocą metod, poleceń posyłanych do obiektów za pośrednictwem referencji i za pomocą "operatora" kropka).

Wyobraźmy sobie, że na dwóch "danych" - liczbach całkowitych i na dwóch "danych" - obiektach klasy Para wykonujemy podobne operacje:

  1. Nadanie wartości pierwszej danej, nadanie wartości drugiej danej,
  2. Przypisanie zmiennej oznaczającej drugą daną wartości zmienej oznaczającej pierwszą daną,
  3. Zmianę wartości drugiej danej,
  4. Porównanie zmiennych, oznaczających obie dane.

A dodatkowo (w obu przypadkach) wprowadzimy trzecią daną, której wartość ustalimy na wartość drugiej i porównamy zmienne oznaczające te dane (drugą i trzecią).
Program mógłby wyglądać tak:

public class Roznica {

  public static void main(String[] args) {

    // Operacje na zmiennych typów pierwotnych
    int x, y, z;
    x = 3;
    y = 4;
    x = y;
    y = 5;
    z = 5;
    System.out.println("x = " + x);
    System.out.println("y = " + y);
    System.out.println("z = " + z);
    if (x == y) System.out.println ("x i y równe.");
    else  System.out.println ("x i y nierówne.");
    if (y == z) System.out.println ("y i z równe.");
    else  System.out.println ("y i z nierówne.");


    // Podobne operacje na zmiennych typu referencyjnego
    Para px = new Para(), py = new Para(), pz = new Para();
    px.set( 3, 3 );
    py.set( 4, 4 );
    pz.set( 5, 5 );
    px = py;
    py.set( 5, 5 );
    System.out.print("Para px: "); px.show();
    System.out.print("Para py: "); py.show();
    System.out.print("Para pz: "); pz.show();
    if (px == py) System.out.println ("px i py równe.");
    else  System.out.println ("px i py nierówne.");
    if (py == pz) System.out.println ("py i pz równe.");
    else  System.out.println ("py i pz nierówne.");

  }
}

x = 4
y = 5
z = 5
x i y nierówne.
y i z równe.
Para px: ( 5 , 5 )
Para py: ( 5 , 5 )
Para pz: ( 5 , 5 )
px i py równe.
py i pz nierówne.

Wynik działania programu (obok) może wyglądać zaskakująco dla kogoś, kto nie uświadomi sobie braku różnicy pomiędzy operacjami na zmiennych typów pierwotnych i referencyjnych (myśląc że zmienne typów referencyjnych zawierają obiekty). Otrzymany rezultat wynika z następujących faktów:
  • wyrażenie new Para() zwraca referencję do nowoutworzonego obiektu klasy Para; uzyskiwane referencje przypisywane są zmiennym typu Para (który jest typem referencyjnym)
  • użycie metody (polecenia) set ustala wartości danych w obiekcie klasy Para; do obiektu odwołujemy się przez referencję, która na niego wskazuje (np. px.set(...)
  • przypisanie py = px powoduje skopiowanie referencji (wskazującej na obiekt-parę o składnikach (3, 3)) do zmiennej py (dotąd wskazującej na obiekt-parę o wartościach (4,4). Od tej chwili px i py oznaczają ten sam obiekt (który jest parą o wartościach (3,3)). Do obiektu-pary (4,4) nie mamy już w tej chwili żadnego dostępu.
  • ponieważ za pomocą referencji py ustalamy nowe wartości składników pary (py.set(5,5)), na którą wskazuje zarówna zmnenna py jak i px, to odwolania show wobec tych zmiennych pokażą identyczne wartości (skladniki pary o wartościach 5, 5)
  • dalej porównanie referencji px i py da wartość true (bo referencje wskazują na ten sam obiekt, a nie dlatego, że wskazują na dwa obiekty o tych samych wartościach elementów - składników pary)
  • o czym dobitnie się przekonujemy porównując zmienne py i pz. Zmienne te wskazują na dwa różne obiekty (zatem wartości tych zmiennych są różne) i dlatego wynik porównania jest false, mimo, że wartości elementów obu tych obiektów (składniki pary) są takie same (5, 5).

Przy okazji warto zastanowić się, co dzieje się obiektem-parą o wartościach (4,4) na którą wskazywała najpierw referencja py. Obiekt ten został utworzony na stercie (py = new Para()), a więc zajmuje jakiś obszar pamięci. Następnie ustalono wartości jego elementów (składników pary) - py.set(4,4) - a więc te wartości zostały wpisane do tego obszaru. Po czym zmiennej py przypisano wartość zmiennej px i w ten sposób w programie nie mamy już żadnej referencji do tego obiektu. A ponieważ na obiektach możemy dzialać tylko za pomocą referencji, to jest on już dla nas bezużyteczny i wyłącznie "zaśmieca pamięć". Czy musimy się tym martwić? Gdyby np. takich zaśmiecających pamięć obiektów pojawiło się w naszym programie tysiące, to czy nie spowodowołoby to przepełnienia pamięci?

Jest to istotne ułatwienie w porównaniu z takimi językami jak C czy C++, gdzie dynamicznie alokowane przez programistę (za pomocą operatorów lub funkcji) obszary pamięci muszą być przez programistę świadomie zwalniane

Na szczęście - nie. Bezużyteczne obiekty są automatycznie usuwane z pamięci (bez konieczności żadnej ingerencji programisty), mimo, że powstały one na skutek wykonania odpowiednich instrukcji zapisanych przez programistę w programie (np. Para py = new Para();).


Obiekty, na które w programie nie wskazuje już żadna referencja są automatycznie usuwane z pamięci. Nazywa się to automatycznym odśmiecaniem (garbage collecting)

Podsumujmy najważniejsze fakty.


  • obiekty musimy tworzyć za pomocą wyrażenia new ...
  • na obiektach operujemy za pomocą referencji (które na nie wskazują) i poleceń (metod) zdefiniowanych w klasie obiektów
  • referencje nie są obiektami - są adresami obiektów
  • zmienne typów referencyjnych musimy (tak samo jak zmienne innych typów) deklarować przed ich użyciem w programie
  • deklaracja zmiennej-referencji nie tworzy obiektu

Programując w Javie, w wielu przypadkach, można nawet nie zdawać sobie z tego wszystkiego sprawy. Można pokazać wiele programów obiektowych w Javie, które działają poprawnie i wykonują przewidziane dla nich zadania, a zostały napisane bez uświadomienia sobie różnicy pomiędzy referencjami i obiektami. Dobrym przykładem są aplikacje powitalne z poprzedniego wykładu: można je napisać nic nie wiedząc o referencjach i myśląc (błędnie), że zmienne zadeklarowane tam jako Frame czy Label zawierają obiekty.
Jednak w wielu nawet prostych przypadkach (np. poprzedni program porównujący działania na liczbach i na obiektach-parach) brak wiedzy o referencjach może doprowadzić do poważnych błędów w programie. Na pewno zaś do napisania nieco bardziej skomplikowanych programów uświadomienie sobie różnicy pomiędzy obiektami i referencjami jest bardzo istotne. Dlatego poświęciliśmy tej kluczowej sprawie sporo miejsca już na początku: byłoby chyba niedobrze zaczynać od prostszej, może bardziej zrozumiałej (nie wspominającej o referencjach), ale z gruntu fałszywej interpretacji języka, aby potem - ze zdziwieniem - musieć przestawiać swoje myślenie na całkiem inne tory.
Dlatego - mimo, że podany tu materiał może wydać się nieco skomplikowany - warto mu poświęcić i czas i wysiłek. Nie odkładając tego na poźniej.

Powiedzmy też szczerze, że model przyjęty przez twórców Javy, jest trudny w opisie. Precyzyjne opisy działania programu (ściśle zgodne z modelem) muszą roić się od słabo czytelnych zbitek.
Na przykład, opis jednej prostej instrukcji:

Color x = y.getBackground();

powinien wyglądac tak:

"zmiennej x przypisujemy referencję do obiektu klasy Color zwróconą przez metodę getBackground(), wywołaną na rzecz obiektu, do którego referencję zawiera zmienna y".

Sytuacji w niczym nie poprawia użycie terminologii odnośnik i odniesienie:

"odnośnikowi x przypisujemy odniesienie do obiektu klasy Color, zwrócone przez metodę getBackground(), wywołaną na rzecz obiektu, do którego odniesienie zawiera odnośnik y".

Dlatego w dalszej części wykładów będziemy się starali używać uproszczonego języka, stosując swoiste skróty myślowe.

Zawsze pamiętając o różnicy między referencją, zmienną zawierającą referencję oraz obiektem będziemy czasem (dla uproszczenia opisów) mówić:
  • referencja - nazywając tak zarówno adres obiektu, jak i zmienną go zawierającą
  • obiekt - mając na myśli referencję do obiektu

Na przykład w kontekście:

JakasKlasa x;
...
powiemy czasem "referencja x" (gdy będziemy chcieli uwypuklić "wskaźnikowy" charakter zmiennej x)

a czasem: "obiekt x" (gdy będziemy chcieli podkreślić operowanie na obiekcie, wskazywanym przez referencję zawartą w x)

W kontekście :

String txt;
...
powiemy czasem: "łańcuch znakowy txt".

a w kontekście:

Frame v;
...
powiemy czasem "okno v ..." .

Albo w kontekście:

Color x = y.getBackground();

powiemy czasem: "uzyskanie koloru tła obiektu y"


Należy pamiętać, że będą to wszystko skróty myślowe, służące do bardziej klarownego przedstawienia treści, istoty, wysokopoziomowej semantyki programów.

I - mimo tych skrótów - zawsze należy pamiętać o różnicy pomiędzy referencjami i obiektami.


 następny punkt »