Pisanie programu komputerowego jest rozkoszą, uruchamianie - zmorą.
Część 3.4. Klasy i obiekty

Wstęp

W rzeczywistym świecie spotykamy wiele obiektów. Obiektem jest człowiek, dom, komputer, itd. Każde z nich posiada indywidualne cechy takie jak kolor, wymiary, itd. Każde z nich może też realizować pewne zadania.

Podobnie w ujęciu programistycznym obiekt posiada zbiór cech, które nazywamy właściwościami (polami danych) oraz potrafi wykonać pewne czynności, które nazywamy metodami. Uogólniając: obiekt jest to zestaw właściwości i możliwych do wykonania zadań. Przykładem obiektu może być na przykład okienko wyświetlające komunikat. Posiada ono cechy: wysokość, szerokość, tytuł, położenie na ekranie, kolor tła, ramkę itd. Do jego metod należą: możliwość zmiany rozmiaru okienka, przesunięcie, odświeżenie, zamknięcie, reakcja na przesuwanie lub kliknięcie myszki, wyświetlenie komunikatu, itp.

Konkretny człowiek lub budynek jest obiektem. Potrafimy jednak mówić o człowieku czy domu w ogóle: podając, że każdy posiada określone cechy i zachowania. Np. każdy człowiek posiada wzrost, wagę, kolor włosów i oczu, wiek, nazwisko, itd. Stanowi to zbiór właściwości człowieka. Z jego możliwych działań możemy wymienić: poruszanie się, czytanie, pisanie, praca, itd. Taki ogólny zbiór cech i właściwości charakteryzuje każdego człowieka, stanowi jego wzorzec.

W programowaniu obiektowym zbiór takich ogólnych cech nazywamy klasą. Innymi słowy klasa stanowi zbiór wszystkich możliwych właściwości i zachowań (metod) każdego utworzonego na jej podstawie egzemplarza czyli obiektu: klasa definiuje potencjalne właściwości i metody możliwego zbioru obiektów. Dopiero utworzenie (zadeklarowanie) obiektu danej klasy tworzy programową "rzeczywistość": zmienna obiektową o określonych właściwościach zdolną wykonywać pewne zadania.

Widać z tego, że klasa stanowi pojęcie abstrakcyjne i istnieje tylko w czasie projektowania i pisania programu, obiekt zaś stanowi fizyczne wystąpieniem klasy i istnieje tylko w czasie wykonywania programu.

Programowanie obiektowe w dużym uproszczeniu polega na tworzeniu klas, powoływaniu do "życia" obiektów tych klas oraz obsłudze możliwych zdarzeń związanych z tym obiektem. Takie podejście pozwala programiście skupić się na tym, co dzieje się w obrębie danego obiektu i jak ma on reagować na występujące zdarzenia. Obiekty mogą istnieć równolegle i działać niezależnie od siebie.

Tyle tytułem wstępu. Jeżeli chcesz poznać dokładniej terminologie związana z klasami i obiektami, to zajrzyj na stronę terminologia obiektowa, gdzie pojęcia klasy i obiektu omówione są obszerniej.

Definicja klasy

Każdy program w Javie składa z klas i tylko z klas. Poza klasami nic innego w programie nie występuje. W klasach deklarowane są wszystkie używane zmienne (pola danych) oraz zawarty jest cały kod wykonywalny programu, który realizuje jego zadania. W dotychczasowych przykładach była to tylko jedna klasa o nazwie Przyklad. Definiowała ona pewien typ, który do "życia" powoływany był w momencie uruchomienia programu.

Dotychczas deklarowaliśmy klasę w jeden ustalony sposób. Pełna definicja klasy może mieć następującą składnię:

[modyfikatory] class NazwaKlasy [extends NazwaKlasyBazowej]
              [implements NazwyKlasInterfejsów]
{
 	// Ciało klasy:
 	// definicje pól danych , metod i klas wewnętrznych definiowanej klasy
}

Gdzie modyfikatory deklarują dostępność klasy (np.: public, abstract lub final static), NazwaKlasy określa nadaną przez programistę nazwę deklarowanej klasy, NazwaKlasyBazowej jest nazwą klasy nadrzędnej, a NazwyKlasInterfejsów to lista nazw klas interfejsów implementowanych przez deklarowana klasę. Ciało klasy stanowią definicje jej pól danych, metod i klas wewnętrznych.

Po bardziej szczegółowe informacje o deklarowaniu klas zajrzyj do klasy jako typu danych.

Metody i pola danych

Składowymi klasy są pola i metody. Pola to zmienne, w których możemy przechowywać związane z daną klasą. Metody to kod wykonywalny, który realizuje "zachowania" klasy.

Aby to zobrazować, utwórzmy klasę Prostokat, która będzie opisywała dowolny prostokąt poprzez określenie współrzędnych jego wierzchołków:

public class Prostokat
{ int X1;              // współrzędna X lewego górnego rogu
  int Y1;              // współrzędna Y lewego górnego rogu
  int X2;              // współrzędna X prawego dolnego rogu
  int Y2;              // współrzędna Y prawego dolnego rogu
}

Zadeklarujmy teraz zmienną typu Prostokat:

Prostokat  mojProstokat;

Jak widać deklaracja jest identyczna, jak deklaracja zmiennych typów podstawowych: najpierw podajemy typ zmiennej, a następnie nazwę deklarowanej zmiennej. W ten sposób utworzyliśmy referencje (odwołanie) do zmiennej (obiektu) typu Prostokąt. Skorzystanie z tego obiektu wymaga jednak utworzenia samego obiektu. Służy do tego operator new, który ma postać:

new  NazwaKlasy();

W naszym przypadku możemy to zrobić na 1 z dwu możliwych sposobów. Utworzyć obiekt w momencie jego deklarowania:

Prostokat  mojProstokat = new Prostokat();

lub zdeklarować referencję do obiektu, a dopiero później utworzyć sam obiekt:

Prostokat  mojProstokat;
.....
mojProstokat = new Prostokat();

Klasa Prostokąt zawiera tylko 4 pola danych: X1, Y1, X2, Y2. Ponieważ już utworzyliśmy obiekt typu Prostokat, to możemy już nadawać tym polom wartości i odczytywać ich aktualna zawartość. W naszym przypadku mogłoby to wyglądać następująco:

.....
Prostokat  mojProstokat = new Prostokat();
.....
mojProstokat.X1 = 1;
mojProstokat.Y1 = 2;
mojProstokat.X2 = 5;
mojProstokat.Y2 = 10;
.....
int x1 = mojProstokat.X1;
int y1 = mojProstokat.Y1;
int x2 = mojProstokat.X2;
int y2 = mojProstokat.Y2;
.....

W tej chwili nasza klasa służy jedynie do przechowywania współrzędnych wierzchołków prostokąta i nie potrafi ich w żaden sposób wykorzystać. Aby klasa mogła zacząć "działać", musimy dopisać do niej metody, czyli funkcje, które będą wykonywały pewne działania (szczegóły deklaracji metod). Dopiszmy do naszej klsy metody, które umożliwią jej obliczenie pola i obwodu prostokąta:

public class Prostokat
{ int X1;              // współrzędna X lewgo górnego rogu
  int Y1;              // współrzędna Y lewgo górnego rogu
  int X2;              // współrzędna X prawgo dolnego rogu
  int Y2;              // współrzędna Y prawgo dolnego rogu

  int obwod ()         // metoda obliczajaca obwód
  { int o = 2 * Math.abs(X2 - X1) + 2 * Math.abs(Y2 - Y1);
    return o;          // wartość zwracana przez metodę
  }

  int pole ()          // metoda obliczajaca pole
  { int p =  Math.abs(X2 - X1) * Math.abs(Y2 - Y1);
    return p;          // wartość zwracana przez metodę
  }
}

W powyższym przykładzie użyliśmy metody Math.abs(...), aby obliczyć wartość bezwzględna z wyrażenia. Jest to konieczne, aby wynik był poprawny niezależnie od wartości współrzędnych prostokąta.

Obecnie obiekt klasy Prostokat może już nie tylko pamiętać współczynniki wierzchołków, ale także obliczyć obwód i pole zapamiętanego prostokąta. Możemy z tego skorzystać w naszym programie:

.....
Prostokat  mojProstokat = new Prostokat();
.....
mojProstokat.X1 = 1;
mojProstokat.Y1 = 2;
mojProstokat.X2 = 5;
mojProstokat.Y2 = 10;
.....
System.out.println ("Pole  = " + mojProstokat.pole(););
System.out.println ("Obwód = " + mojProstokat.obwod(););
.....

Najwyższa już pora na pokazanie jak wygląda cały program z wykorzystaniem utworzonej przez nas klasy Prostokat. Poniższy kod zapisz w jednym pliku o nazwie Przyklad.java, a następnie skompiluj i uruchom ten program.

public class Przyklad
{ public static void main (String args[])
  {	Prostokat  mojProstokat = new Prostokat();
    System.out.println ("Pole  = " + mojProstokat.pole());
  	System.out.println ("Obwód = " + mojProstokat.obwod());
  }
}

class Prostokat
{ int X1=10; 
  int Y1=10; 
  int X2=1; 
  int Y2=1; 

  int obwod ()
  { int o = 2 * Math.abs(X2 - X1) + 2 * Math.abs(Y2 - Y1);
    return o;
  }

  int pole ()
  { int p =  Math.abs(X2 - X1) * Math.abs(Y2 - Y1);
    return p;
  }
}

Efektem programu powinno być wyświetlenie 2 wierszy:

Jak działa nasz program. Wyjaśnijmy to krok po kroku:

  • wykonanie programu zawsze rozpoczyna się od metody main klasy publicznej o takiej samej nazwie, jak nazwa pliku;
     
  • nasza metoda main rozpoczyna się od utworzenia obiektu typu Prostokat;
     
  • podczas tworzenia obiektu inicjowane są wszystkie jego pola danych;
     
  • następne wywołania metod pole i obwod powodują wykonanie obliczeń i wstawienie obliczonych wartości w miejscu ich wywołania.
     
Należy pamiętać, że w jednym pliku może wystąpić tylko jedna klasa publiczna i musi ona mieć nazwę identyczną z nazwą samego pliku. Inne klasy zapisane w tym samym pliku muszą być klasami prywatnymi (opuszczenie modyfikatora dostępu automatycznie traktuje klasę jako privat). Istnieją metody na obejście tych ograniczeń, ale o tym będziemy mówili w dalszej części.

Konstruktory

Pozostańmy jeszcze przy naszym przykładzie. Wszystkie zmienne publiczne służące do zapamiętania wierzchołków prostokąta zainicjowaliśmy konkretnymi wartościami. Co jednak by się stało, gdybyśmy tego nie zrobili i pozostawili ich deklaracje bez inicjacji?

public class Prostokat
{ int X1;
  int Y1;
  int X2;
  int Y2;
}

Otóż w takim przypadku Java zainicjowałaby wszystkie te zmienne wartością zero. Jest to istotna różnica między deklaracją zmiennych prywatnych i publicznych: niezainicjowana zmienna prywatna powoduje błąd wykonania programu podczas próby jej użycia, natomiast wszystkie zmienne publiczne są zawsze inicjowane wartościami domyślnymi (zerem dla zmiennych numerycznych i wartością false dla zmiennych logicznych).

W powyższy sposób sama Java rozwiązała nam problem inicjacji zmiennych w naszej klasie Prostokat. W praktyce jednak na ogół nie mamy do czynienia z prostokątami, które mają współrzędne wszystkich wierzchołków równe zero. Pozostaje więc żmudny proces "ręcznego" przypisywania wartości każdej ze współrzędnych:

.....
Prostokat  mojProstokat = new Prostokat();
mojProstokat.X1 = 1;
mojProstokat.Y1 = 2;
mojProstokat.X2 = 5;
mojProstokat.Y2 = 10;
.....

Aby tego uniknąć, możemy do naszej klasy dodać metodę, która ustawi od razu wartości wszystkich współrzędnych wierzchołkowych. Nazwijmy ją ustawWspolrzene:

void ustawWspolrzene (int x1, int y1,
                             int x2, int y2)
{ X1 = x1;
  Y1 = y1;
  X2 = x2;
  Y2 = y2;
}

Teraz możemy sobie uprościć ustawianie współrzędnych w naszym programie:

Prostokat  mojProstokat = new Prostokat();
mojProstokat.ustawWspolrzene (1, 2, 5, 10);
.....

Możemy również stwierdzić, że w naszym przypadku najczęściej używanymi współrzędnymi są (1,1) i (10,10). Co zrobić, aby uniknąć każdorazowego ich ustawiania? Możemy do tego użyć specjalnej metody zwanej konstruktorem. Konstruktor, to metoda, która wykonywana jest zawsze podczas tworzenia obiektu danej klasy. Są one zawsze publiczne, nie posiadają typu i muszą mieć nazwę identyczną z nazwą klasy. Każda klasa może mieć wiele konstruktorów różniących się między sobą liczbą parametrów: podczas tworzenia obiektu klasy wykonany będzie ten, którego ilość parametrów zgodna jest z ilością podaną podczas tworzenia obiektu. W naszej klasie Prostokat utworzymy konstruktory, które umożliwią przyjęcie wartości typowych lub podanych podczas tworzenia obiektu:

public Prostokat ( )
{ X1 = 1;
  Y1 = 1;
  X2 = 10;
  Y2 = 10;
}

public Prostokat (int x1, int y1,
                         int x2, int y2)
{ X1 = x1;
  Y1 = y1;
  X2 = x2;
  Y2 = y2;
}

Przerobimy teraz nasz ostatni program tworząc dwa prostokąty: pierwszy z domyślnymi warościami współrzędnych i drugi - z podanymi przez nas w momencie jego tworzenia. Następnie wyświetlimy pola i obwody obu utworzonych prostokątów:

public class Przyklad
{ public static void main (String args[])
  {	Prostokat  pr1 = new Prostokat();
  	Prostokat  pr2 = new Prostokat(7, 6, 11, 13);
    System.out.println ("Pole domyślnego prostokąta  = " + pr1.pole());
  	System.out.println ("Obwód domyślnego prostokąta = " + pr1.obwod());
  	System.out.print ("\n");
    System.out.println ("Pole zdefiniowanego prostokąta  = " + pr2.pole());
  	System.out.println ("Obwód zdefiniowanego prostokąta = " + pr2.obwod());
  }
}

class Prostokat
{ int X1; 
  int Y1; 
  int X2; 
  int Y2; 

  public Prostokat ( )
  { X1 = 1;
    Y1 = 1;
    X2 = 10;
    Y2 = 10;
  }

  public Prostokat (int x1, int y1,
                           int x2, int y2)
  { X1 = x1;
    Y1 = y1;
    X2 = x2;
    Y2 = y2;
  }

  int obwod ()
  { int o = 2 * Math.abs(X2 - X1) + 2 * Math.abs(Y2 - Y1);
    return o;
  }

  int pole ()
  { int p =  Math.abs(X2 - X1) * Math.abs(Y2 - Y1);
    return p;
  }
}

Wynik działania tego programu powinien wyglądać następująco:

Użycie konstruktorów w klasach czyni ją bardziej elastyczną i wygodną w użyciu. Pozwala ponadto zdefiniować pewne właściwości obiektu już w momencie jego tworzenia. Jak widać z poprzedniego programu używanie konstruktorów nie jest konieczne. W wielu wypadkach ułatwia jednak znacznie pisanie programów i korzystanie z klas.

Hermetyzacja

W poprzednich przykładach spotkałeś się z tym, że klasy, właściwości i metody były poprzedzone modyfikatorami. Użycie ich związane jest z pojęciem hermetyzacji. Hermetyzacja polega na zamknięciu w jednej jednostce syntaktycznej danych i metod oraz ukrywanie informacji, które nie powinny być widoczne dla użytkownika. Innymi słowy od użycia odpowiedniego modyfikatora zależy czy dana klasa jest dostępna wszędzie, tylko dla określonej grupy klas, czy jej właściwości i metody mogą być używane w dowolnym miejscu programu, czy tylko w klasach potomnych lub tylko w samej klasie.

Dla klas mamy do dyspozycji następujące modyfikatory dostępu:

  • abstract - zawiera co najmniej jedną metodę abstrakcyjną: szablony metod - bez kodu;
     
  • final - nie można od nie tworzyć klas potomnych;
     
  • public - może być używana przez wszystkich. W jednym pliku może wystąpić tylko jedna klasa zadeklarowana jako public, a plik musi mieć nazwę <NazwaKlasy.java>;
     
  • private - klasa dostępna jest tylko w obrębie pliku, w którym występuje;
     
  • brak - klasa dostępna w obrębie pakietu, w którym występuje.
     
Więcej informacji teoretycznych znajdziesz w opisie modyfikatorów.

Dziedziczenie

W Javie istnieje możliwość stworzenia nowej klasy, która powstaje na bazie klasy już istniejącej. Nowa klasa dziedziczy wszystkie właściwości i metody klasy, z której powstała, a dodatkowo może posiadać własne metody i właściwości. Możliwa jest również zmiana odziedziczonych metod. Taki sposób tworzenia klasy na podstawie innej klasy nazywamy dziedziczeniem. Klasa z której dziedziczy się właściwości i metody nazywamy klasą nadrzędną (lub przodkiem), natomiast tworzoną klasę nazywamy podklasą (lub potomkiem). Jak zapewne pamiętasz z definicji klasy, istnieje słowo, które mówi, że powstająca klasa jest potomkiem innej klasy. Te słowo to extends.

Wyjaśnijmy to na przykładzie. Mamy napisać program, który będzie obliczał obwody prostokątów i trójkątów na podstawie wprowadzonych współrzędnych wszystkich wierzchołków oraz pola tych figur. Można tworzyć osobne klasy dla trójkątów i dla prostokątów, i w nich metody na obliczanie obwódów i pól. Ma to sens dla obliczania powierzchni pola, gdyż obie figury maja różne wzory na ich obliczanie. Obwód jednak zawsze liczymy sumując długości boków. Możemy więc zaoszczędzić sobie trochę pracy tworząc klasę Boki i na jej bazie budując dopiero nasze klasy Prostokat i Trojkat.

Oto przykładowy kod takiego programu:

public class Przyklad
{ public static void main (String args[])
  { // deklaracja obiektów z domyślnymi współrzędnymi wierzchołków
    Prostokat  pr1 = new Prostokat();
    Trojkat    tr1 = new Trojkat();

    // deklaracja obiektów ze zdefiniowanymi współrzędnymi wierzchołków
    Prostokat  pr2 = new Prostokat(0, 0, 0, 3, 3, 3, 3, 0);
    Trojkat    tr2 = new Trojkat(1, 1, 5, 5, 10, 1);

    // wyświetlenie wszystkich obwodów i pól
    System.out.println ("Obwod domyslnego prostokata = " + pr1.obwod());
    System.out.println ("Pole domyslnego prostokata  = " + pr1.pole());
    System.out.print ("\n");
    System.out.println ("Obwod domyslnego trojkata = " + tr1.obwod());
    System.out.println ("Pole domyslnego trojkata  = " + tr1.pole());
    System.out.print ("\n");
    System.out.println ("Obwod zdefiniowanego prostokata = " + pr2.obwod());
    System.out.println ("Pole zdefiniowanego prostokata  = " + pr2.pole());
    System.out.print ("\n");
    System.out.println ("Obwod zdefiniowanego trojkata = " + tr2.obwod());
    System.out.println ("Pole zdefiniowanego trojkata  = " + tr2.pole());
  }
}

// deklaracja klasy Boki, jako klasy bazowej dla Prostokat i Trojkat
class Boki
{ private int n;              // wszystkie zmienne są deklarowane 
  private int B[][];          // jako prywatne, gdyż będą używane
  private int ile;            // tylko wewnątrz tej klasy

  // konstruktor klasy, parametr podaje ilość boków figury
  public Boki(int x)
  { n   = x;                  // zapamiętanie ilości boków
    B   = new int[n][2];      // utworzenie tablicy dla współrzędnych wierzchołków
    ile = 0;                  // zerowanie ilości podanych wierzchołków
  }

  // metoda do zapamiętywania współrzędnych kolejnych wierzchołków figury
  void dodajWierzcholek(int x, int y)
  { if (ile == n) return;      // jeżeli podano już wszystkie, to przetrwij
    B[ile][0] = x;             // zapamiętanie współrzędnych
    B[ile][1] = y;
    ile++;                     // zwiększenie licznika zapamiętanych współrzędnych
  }
  
  // metoda służąca do obliczania obwodu figury
  // z tej metody będą korzystały klasy Prostokat i Trojkąt
  double obwod()
  { double o = 0;
    for (int i=1 ; i<=n ; i++) o += bok(i);
    return o;
  }

  // metoda obliczająca długość i-tego boku figury wg wzoru:
  // pierwiasztek z wyrażenia: (x2-x1)2 + (y2-y1)2
  // z tej metody będą korzystały klasy Prostokat i Trojkąt
  double bok(int i)
  { double o = 0;
    if (i > n) return 0;
    if (i == n)
      o = Math.sqrt((B[0][0] - B[n-1][0])*(B[0][0] - B[n-1][0]) +
                    (B[0][1] - B[n-1][1])*(B[0][1] - B[n-1][1]));
    else
      o = Math.sqrt((B[i-1][0] - B[i][0])*(B[i-1][0] - B[i][0]) +
                    (B[i-1][1] - B[i][1])*(B[i-1][1] - B[i][1]));
    return o;
  }
}

// klasa Prostokat, która bazuje na klasie Boki
class Prostokat extends Boki
  // konstruktor z nadamiem wartości wierzchołkom prostokąta
{ public Prostokat (int x1, int y1,
                           int x2, int y2,
                           int x3, int y3,
                           int x4, int y4)
  { super(4);
    super.dodajWierzcholek (x1, y1);
    super.dodajWierzcholek (x2, y2);
    super.dodajWierzcholek (x3, y3);
    super.dodajWierzcholek (x4, y4);
  }

  // konstruktor domyślny klasy
  public Prostokat ( )
  { super(4);
    super.dodajWierzcholek (1, 1);
    super.dodajWierzcholek (1, 2);
    super.dodajWierzcholek (2, 2);
    super.dodajWierzcholek (2, 1);
  }

  // metoda obliczająca pole prostokąta
  double pole ()
  { return (super.bok(1) * super.bok(2));
  }
}

// klasa Trojkat, która bazuje na klasie Boki
class Trojkat extends Boki
  // konstruktor z nadamiem wartości wierzchołkom trójkąta
{ public Trojkat (int x1, int y1,
                         int x2, int y2,
                         int x3, int y3)
  { super(3);
    super.dodajWierzcholek (x1, y1);
    super.dodajWierzcholek (x2, y2);
    super.dodajWierzcholek (x3, y3);
  }

  // konstruktor domyślny klasy
  public Trojkat ( )
  { super(3);
    super.dodajWierzcholek (0, 0);
    super.dodajWierzcholek (0, 4);
    super.dodajWierzcholek (3, 0);
  }

  // metoda obliczająca pole trójkąta
  double pole ()
  { double p =  super.obwod() / 2;
    double pole = Math.sqrt(p * (p - super.bok(1)) *
                                       (p - super.bok(2)) *
                                       (p - super.bok(3)));
    return pole;
  }
}

Tak powinien wyglądać wynik działania programu:

Czymś zupełnie nowym w tym programie jest pojawienie się słowa super. Służy ono do odwołania się w klasie potomnej do metody lub właściwości klasy bazowej. Jest to jedyna metoda bezpośredniej łączności klasy potomnej z klasą bazową.

W powyższym programie zwróć szczególną uwagę na kilka istotnych rzeczy:

  • współrzędne wierzchołków każdej figury pamiętane są faktycznie w klasie Boki;
     
  • obliczanie długości boków i obwodów wszystkich figur odbywa si w klasie Boki;
     
  • klasy Prostokat i Trojkat w ogóle nie posiadają własnych metody obliczania obwodu: wywołanie dla każdej z nich tej metody powoduje wywołanie odpowiedniej metody z klasy Boki.
     
Oczywiście można ten program rozbudowywać o kolejne klasy wielokątów bazujących na klasie Boki.
Sądzę, że dokładne przeanalizowanie tego programu wyjaśni Ci mechanizm dziedziczenia klas.
« wstecz   dalej »