Programowanie obiektowe w Javie

Marek Wierzbicki

 

Programowanie obiektowe to sposób tworzenia programów, który przestał już być modną nowinką. To standard, bez którego nie można sobie wyobrazić powstawania zaawansowanych programów, czy pracy zespołowej. Na świecie używa się wielu obiektowych języków programowania. Turbo Pascal (obecnie Delphi) czy C++ to kanony istniejące od ponad 10 lat, często wykorzystywane do tworzenia zaawansowanych aplikacji oraz w szkołach czy uczelniach, jako modelowe języki obiektowe. Jednak te najpopularniejsze standardy przestają być wystarczające. Ograniczenia wynikające z ich korzeni uniemożliwiają dalszy rozwój tych języków. Pojawiające się wolne miejsce wypełnia JAVA. Stworzony od podstaw, nowoczesny, bezpieczny, niezależny od typu komputera i systemu operacyjnego, w pełni obiektowy język programowania.

Z punktu widzenia dystrybutora i odbiorcy oprogramowania, najważniejszą zaletą jest całkowita przenośność programów między różnymi komputerami i systemami operacyjnymi. Cecha ta, dostępna dla standardowego ANSI C na poziomie kodu źródłowego, w JAVIE dotyczy programów wykonywalnych. Oznacza to, że raz skompilowany program pracuje poprawnie na wszystkich maszynach (nie trzeba tworzyć różnych wersji wynikowych dla każdego typu komputera). Do najważniejszych zalet widocznych w czasie tworzenia oprogramowania należy bardzo silna obiektowość i efektywne zabezpieczenia przed popełnianiem błędów w czasie pisania i uruchamiania kodu. Dzięki tym cechom JAVA doskonale nadaje się zarówno jako język do nauki, jak i do tworzenia użytecznych i profesjonalnych aplikacji.

Odrobina historii

W zamyśle JAVA miała służyć do obsługi sterowników lodówek, pralek i magnetowidów. Największą popularność zdobyła jednak jako język do tworzenia apletów i serwletów, czyli programów funkcjonujących w strukturach Internetu. Występuje też jako język wewnętrzny większości poważnych projektów, działających na wielu różnych platformach (na przykład serwery SQL Oracle czy Sybase intensywnie korzystają z jej możliwości). Obecnie zaczyna też trafiać do telefonów komórkowych. Ze względu na swoje wszechstronne możliwości i bezpieczeństwo staje się standardem, którego trudno nie zauważać. Każdy nowoczesny informatyk powinien więc znać JAVĘ i jej obiektowe cechy.

Programowanie obiektowe robi karierę już od ponad 10 lat. Powstało na bazie badań z zakresu psychologii, które dowodziły, że im większa liczba szczegółów jest prezentowana człowiekowi, tym trudniej jest mu się na nich skupić. A szczegóły nie zawsze są potrzebne do poprawnego wykorzystania przedmiotów, nawet bardzo mocno nimi nasyconych. Przykładem niech będzie telefon komórkowy. Mało kto wie, jak dokładnie działają wszystkie jego składowe, jakie są protokoły przekazywania informacji między nimi i stacjami bazowymi oraz jak powstają elementy półprzewodnikowe, z których są one zbudowane. W zupełności nie przeszkadza to jednak milionom ludzi na całym świecie codziennie aktywnie korzystać z ich możliwości.

W miarę rozwoju techniki komputerowej szybkość działania komputerów i ich potencjalne możliwości obliczeniowe stale rosły. Aby w pełni wykorzystać te możliwości (poza prostym wykorzystaniem starych programów, licząc jedynie na wzrost szybkości wynikający z lepszego sprzętu) należało coraz bardziej rozbudowywać programy działające na tych maszynach. W miarę wzrostu stopnia złożoności programów, ich twórcy starali się wyodrębnić jak największą ich część w postaci niezależnych, gotowych do ponownego użycia procedur i bibliotek. Miało to zapewnić odejście od najstarszego modelu programowania, w którym każdy program zaczynało się zawsze tworzyć od początku. Jednak te pozornie rozłączne fragmenty często operowały na danych wspólnych dla siebie i innych procedur. Dwie procedury działań na macierzach często operowały na wskaźniku do tej samej macierzy, czyli tak naprawdę modyfikowały jednocześnie ten sam fragment pamięci. Aby zapobiec takim coraz częstszym pomyłkom, należało stworzyć taki model programowania, który zapewniłby bezpieczeństwo w tej kwestii. I to był pierwszy krok w kierunku stworzenia metodologii programowania obiektowego.

Bardzo ważnym czynnikiem pobudzającym rozwój programowania obiektowego była popularność systemów operacyjnych pracujących w środowisku graficznym. Jakkolwiek pierwsze systemy graficzne nie były tworzone obiektowo, naturalne i intuicyjne podejście do nich było jak najbardziej obiektowe. Przesuwanie, zmiana rozmiaru, koloru czy tytułu to typowe operacje charakterystyczne dla okna. Zawiera ono elementy danych, które nie muszą być udostępniane innym oknom. A więc natura takiego środowiska jest obiektowa.

Kolejny ważny krok do przodu został wykonany dzięki językom Borland Pascal i C++. Mimo bazowania na językach strukturalnych, oferowały one zupełnie nową jakość tworzenia programów. Dzięki wielkiej popularności umożliwiły ewoluowanie standardu, co zaowocowało bardziej efektywnym i intuicyjnym sposobem programowania. Kiedy okazało się, że dalsze rozszerzanie i modyfikowanie idei obiektowej nie jest możliwe w ramach dotychczasowych standardów kodowania, powstała Java.

Klasy w JAVIE

Klasy określają postać, strukturę i działanie obiektów, które są egzemplarzami klas. W związku z zastosowaniem w Javie skrajnie ortodoksyjnego podejścia, program napisany z użyciem tego języka musi mieć strukturę oraz działanie, zaprojektowane z użyciem klas (a zrealizowane z użyciem ich egzemplarzy, czyli obiektów).

Najprostsza możliwa do stworzenia klasa ma postać:

class Simple {}

Charakteryzuje ją słowo kluczowe class, nazwa klasy (w tym wypadku Simple) oraz para nawiasów klamrowych, które reprezentują jej ciało (w tym przypadku są puste). Klasa ta musi być umieszczona w pliku Simple.java. Tak utworzony plik może zostać poddany poprawnej kompilacji i stanowić zupełnie poprawną (choć całkiem nieprzydatną) klasę Javy.

Definicja klasy podstawowej musi być tworzona według szablonu:

[modyfikator] class NazwaKlasy {
[modyfikator] typ nazwa_pola_1;
...
[modyfikator] typ nazwa_pola_k;
[modyfikator] typ nazwa_metody_1([lista_parametrów])
{
ciało_metody_1
}
...
[modyfikator] typ nazwa_metody_L([lista_parametrów])
{
ciało_metody_L
}
}

Klasa może posiadać dowolną liczbę pól i metod (w tym zero, nawet łącznie dla pól i metod, jak pokazałem to wcześniej w najprostszej klasie Simple).

Poniżej umieszczam objaśnienie poszczególnych elementów zaprezentowanych w szablonie na listingu 2.1.

  • class – słowo kluczowe określające definicję klasy.
  • NazwaKlasy – identyfikator określający nazwę klasy.
  • modyfikator – słowo lub słowa kluczowe, oddzielone od siebie spacją, określające sposób traktowania elementu, do którego się odnoszą. Modyfikator może też oznaczać ograniczenie lub rozszerzenie dostępu do elementu.
  • typ – typ pola lub metody – może to być typ prosty (byte, short, int, long, char, float, double lub boolean oraz void – tylko w odniesieniu do metody), klasa bądź tablica (array) elementów jednego typu.
  • nazwa_pola_x – identyfikator jednoznacznie określający pole konstruowanej klasy.
  • nazwa_metody_x – identyfikator, który wraz z listą parametrów jednoznacznie określi metodę.
  • lista_parametrów – lista par rozdzielonych przecinkami, składających się z określenia typu i nazwy egzemplarza danego typu. Jeśli nie zamierzamy przekazać do metody żadnych parametrów, jej deklaracja powinna zawierać parę pustych nawiasów. Zwracam tu uwagę na odstępstwa od C++, które w takim przypadku powinno (zamiast pustych nawiasów) zawierać słowo void, oraz różnice w stosunku do Object Pascala niezawierającego w takim przypadku nawiasów.
  • ciało_metody_x – zbiór instrukcji języka Java określający funkcjonalność danej metody.

Pola są miejscami, w których przechowywane są informacje charakterystyczne dla całej klasy bądź dla jej konkretnego egzemplarza. O polach mówi się też czasami, że są to egzemplarze zmiennych należące do konkretnego egzemplarza klasy. W praktyce możemy traktować pola jako lokalne zmienne danej klasy – z zastrzeżeniem, że zakres ich widzialności i zachowania jest określony przez modyfikatory poszczególnych pól. Klasyczna deklaracja pola odbywa się według schematu:
[modyfikator] typ nazwa_pola_k;

Przykład klasy zawierającej tylko dwa pola pokazałem poniżej:

class Point {
int x; // położenie na osi 0X
int y; // położenie na osi 0Y
}

W przykładzie tym pola są zmiennymi prostymi. Nie ma jednak żadnych przeciwwskazań, żeby były zmiennymi złożonymi, w tym również obiektami.

Inaczej niż inne języki obiektowe takie jak C++ czy Object Pascal, Java nie tylko gromadzi wszystkie informacje w plikach jednego rodzaju, ale również stara się je przechowywać w możliwie najbardziej skoncentrowany sposób. W C++ istnieją pliki nagłówkowe, które przechowują strukturę obiektów, i właściwe pliki z programem przechowujące między innymi obiekty. W Object Pascalu informacje te są co prawda zawarte w jednym pliku, jednak część jest w sekcji interface, część w implementation. W Javie wszystko jest w jednym miejscu. Cała informacja o metodzie zawarta jest tuż przed jej ciałem:

[modyfikator] typ nazwa_metody([lista_parametrów])
{
// blok instrukcji
}

Jeśli typ metody jest różny od void (czyli funkcja zwraca jakąś wartość), powinna ona być zakończona wierszem:

return wyliczonaWartosc;

gdzie wyliczonaWartosc musi być takiego samego typu jak typ metody.

Po zaprezentowaniu schematu tworzenia klas mogę przystąpić do przedstawienia przykładu prostej klasy, która umożliwia przechowywanie informacji o położeniu punktu na płaszczyźnie wraz z metodami umożliwiającymi określenie położenia początkowego punktu i przemieszczenia go:

class Point {
int x; // położenie na osi 0X
int y; // położenie na osi 0Y

// ustawienie nowej pozycji
public void newPosition(int newX, int newY) {
x = newX;
y = newY;
}

// przemieszczenie punktu
public void changePosition(int dX, int dY) {
x = x+dX;
y = y+dY;
}
}

W dalszej części tego rozdziału będę rozszerzał definicję tej klasy i precyzował jej znaczenie.

Jedną z ważnych cech programowania obiektowego jest hermetyzacja. Klasa (a wraz z nią obiekt) może gromadzić w jednym miejscu dane i procedury ich obsługi. Jednak ma to być zgromadzone w taki sposób, aby osoba używająca obiektu miała jak najmniejszy dostęp do danych (tylko do tych niezbędnych). Ma to zapewnić zarówno zmniejszenie liczby błędów popełnianych w czasie kodowania, jak i podniesienie przejrzystości programu. Przedstawiona wcześniej klasa Point nie stanowiła idealnej reprezentacji hermetycznej klasy, gdyż udostępniała na zewnątrz wszystkie, a nie tylko niezbędne elementy. Aby uniemożliwić dostęp do pól, które w idei klasy nie muszą być dostępne z zewnątrz, należy je oznaczyć modyfikatorem private:

class Point {
private int x; // położenie na osi 0X
private int y; // położenie na osi 0Y

// odczyt wartości
public int getX() {
return x;
}

public int getY() {
return y;
}

// ustawienie nowej pozycji
public void newPosition(int newX, int newY) {
x = newX;
y = newY;
}

// przemieszczenie punktu
public void changePosition(int dX, int dY) {
x = x+dX;
y = y+dY;
}
}

Pogrubiono różnice w stosunku do wcześniejszej wersji klasy, czyli ukrycie bezpośrednich wartości x i y oraz udostępnienie w zamian ich wartości przez metody getX i getY. Zaleta takiego rozwiązania jest widoczna. Nie można, nawet przez przypadek, odwołać się bezpośrednio do x i y, dzięki czemu nie może nastąpić przypadkowa ich modyfikacja. Aby je odczytać, trzeba jawnie wywołać getX lub getY. Aby je ustawić, trzeba jawnie wywołać newPosition (można też utworzyć metody setX i setY, aby ustawiać te parametry pojedynczo). Dopiero tak skonstruowana klasa spełnia warunki hermetyzacji.

Dziedziczenie jest jedną z podstawowych cech programowania obiektowego. Mechanizm ten umożliwia rozszerzanie możliwości wcześniej utworzonych klas bez konieczności ich ponownego tworzenia. Zasada dziedziczenia w Javie ma za podstawę założenie, że wszystkie klasy dostępne w tym języku bazują w sposób pośredni lub bezpośredni na klasie głównej o nazwie Object. Wszystkie klasy pochodzące od tej oraz każdej innej są nazywane, w stosunku do tej, po której dziedziczą, podklasami. Klasa, po której dziedziczy własności dana klasa, jest w stosunku do niej nazywana nadklasą. Jeśli nie deklarujemy w żaden sposób nadklasy, tak jak jest to pokazane w przykładowej deklaracji klasy Point, oznacza to, że stosujemy domyślne dziedziczenie po klasie Object. Formalnie deklaracja klasy Point mogłaby mieć postać:
class Point extends Object {

// ...
// ciało klasy Point
// ...
}

Pogrubiony fragment deklaruje dziedziczenie po klasie Object. Jest ono opcjonalne – to znaczy, że jeśli go nie zastosujemy, Point również będzie domyślnie dziedziczył po Object.

Przedstawiony sposób musi być używany w przypadku dziedziczenia po innych klasach:

class Figura extends Point {
...
}
class Wielokat extends Figura {
...
}

W przykładzie tym klasa Wielokat dziedziczy po klasie Figura, która z kolei dziedziczy po Point, a ta po Object. W Javie nie ma żadnych ograniczeń co do zagnieżdżania poziomów dziedziczenia. Poprawne więc będzie dziedziczenie na stu i więcej poziomach. Jakkolwiek takie głębokie dziedziczenie jest bardzo atrakcyjne w teorii, w praktyce wiąże się z niepotrzebnym obciążaniem zarówno pamięci, jak i procesora. To samo zadanie zrealizowane za pomocą płytszej struktury dziedziczenia będzie działało szybciej – i to aż z trzech powodów:

  • Wywołanie metod będzie wymagało mniejszej liczby poszukiwań ich istnienia w ramach kolejnych nadklas.
  • Interpreter będzie musiał załadować mniej plików z definicjami klas (i mniej będzie ich później obsługiwał).
  • System operacyjny (a przez to również interpreter Javy) ma więcej wolnej pamięci, a przez to pracuje szybciej.

Ponadto w przypadku apletów możemy liczyć na szybsze ładowanie się strony do przeglądarki, a więc będzie to kolejna pozytywna cecha.

Poza dziedziczeniem w dowolnie długim łańcuchu od klasy głównej do najniższej klasy potomnej, w niektórych językach programowania (na przykład C++) istnieje wielokrotne dziedziczenie jednocześnie i równorzędnie po kilku klasach. W Javie jest to niemożliwe, to znaczy w definicji każdej klasy może wystąpić co najwyżej jedno słowo extends. Zamiast wielokrotnego dziedziczenia, w Javie dostępny jest mechanizm interfejsów.

W miarę jak tworzymy kolejne klasy pochodne od innych, pojawia się często sytuacja, że musimy zastąpić działanie pewnej metody inną, o takiej samej nazwie i tym samym zestawie parametrów. Działanie takie nazywa się przykrywaniem, pokrywaniem bądź nadpisywaniem metod. Na kolejnym listingu przedstawiam klasy w łańcuchu dziedziczenia, które przykrywają po kolei swoją metodę info:

class A {
public void info() {
System.err.println("Jestem w klasie A");
}
}
class B extends A {
public void info() {
System.err.println("Jestem w klasie B");
}
}
class C extends B {
public void info() {
System.err.println("Jestem w klasie C");
}
}

Odwołanie do metody info obiektu klasy C powoduje wyświetlenie na konsoli Javy w przeglądarce napisu:

Jestem w klasie C

Nie jest to nic dziwnego, bo w klasie C metoda info przykryła swoją odpowiedniczkę z klasy B, która z kolei przykryła swoją odpowiedniczkę z klasy A. Należy jednak pamiętać, że domyślnie wszystkie metody w Javie są wirtualne. Związana jest z tym kwestia polimorfizmu, to znaczy wywołanie metody ustalane jest na podstawie rzeczywiście używanego obiektu, a nie jego deklaracji typu (chyba że są to metody statyczne).

Program w Javie jako obiekt na stronie WWW

Klasy są tylko definicją i określeniem sposobu działania bądź przechowywania informacji. Aby klasa mogła być użyta, musimy utworzyć jej egzemplarz, którym jest obiekt. Również cały program napisany w tym języku możemy traktować jako obiekt w środowisku, w którym jest uruchomiany. Najlepszym przykładem obiektowego podejścia do całego programu są aplety. Są to specjalistyczne programy, których kod przechowywany jest na serwerach internetowych bądź intranetowych razem z innymi plikami wyświetlanymi w przeglądarce stron WWW (HTML, grafika), uruchamiany w jej środowisku. Aplet, z punktu widzenia odbiorcy strony, jest jej fragmentem, najczęściej wizualnie nierozróżnialnym od pozostałej części, wyróżniający się jednak możliwościami funkcjonalnymi. Aplet może więc być fragmentem strony o bardziej skomplikowanym interfejsie, który zapewni szybkie działanie (strona nie musi być przeładowywana, żeby aplet nawet ekstremalnie zmienił swój wygląd). Może też być fragmentem wykonującym skomplikowane obliczenia bez potrzeby angażowania serwera internetowego. Innym przykładowym zastosowaniem apletu może być szybki dostęp do bazy danych znajdującej się na serwerze, który odbywa się bez blokowania innych operacji na stronie. Często spotyka się też aplety ułatwiające wymianę informacji między dwoma lub więcej osobami korzystającymi z tej samej strony internetowej (chat, gry dwu- bądź wieloosobowe itd.). W praktyce funkcjonalność apletu zależy od naszych umiejętności programistycznych, ograniczonych tylko restrykcjami nałożonymi przez system bezpieczeństwa apletów.

Sam aplet to program uruchamiany w komputerze użytkownika przeglądającego sieć internetową po ściągnięciu go do lokalnego środowiska. Od użytkownika nie są wymagane żadne dodatkowe czynności, poza posiadaniem odpowiedniej przeglądarki, która jest w stanie uruchomić ten aplet. Rozwiązanie takie, czyli przechowywanie apletu na serwerze i ściąganie go za każdym razem do użytkownika, daje twórcy oprogramowania bardzo szerokie możliwości w zakresie natychmiastowego uaktualniania wersji programu. Od chwili umieszczenia nowej, poprawionej wersji programu na serwerze, otrzymują ją wszyscy użytkownicy. Bez względu na to, czy użytkowników jest dwóch czy dwa miliony. Pewną wadą może wydawać się konieczność każdorazowego ściągania pliku wykonywalnego poprzez sieć. Nie jest to jednak zbyt duży problem, gdyż pliki wykonywalne w Javie nie są zbyt obszerne. Ponadto większość przeglądarek potrafi korzystać z plików wykonywalnych, zapisanych wcześniej w lokalnym katalogu dyskowym (w czasie poprzednich wizyt na tej stronie). Pliki te są ładowane z serwera ponownie tylko wtedy, gdy się zmieniły, czyli na przykład dopiero po modyfikacji programu.

Projektując aplikację osadzoną na stronie internetowej, należy pamiętać, że ma on pewne ograniczenia wynikające z konieczności zachowania bezpieczeństwa (aplet nie może odwoływać się do zasobów lokalnych komputera ani w nie ingerować). Ponadto aplety posiadają kilka ograniczeń w stosunku do standardów wynikających z niepełnej implementacji Javy w przeglądarkach internetowych. Przy tworzeniu projektu powinno się już we wstępnej fazie projektowej ogarnąć cele i możliwości, dążąc do efektywnego kompromisu między potrzebami i dostępnymi warunkami pracy.

Wszystkie elementy w Javie są skrajnie obiektowe. Wyjątki, jeśli istnieją, jak na przykład interfejsy (poza tym, że można je nazwać klasami szczątkowymi) w praktyce służą jedynie podniesieniu funkcjonalności programowania obiektowego. Poza takimi drobnymi wyjątkami, wszystko w Javie jest obiektem bądź klasą. Nie należy się więc dziwić, że również program internetowy, czyli aplet, musi być klasą i musi posiadać wszystkie jej charakterystyczne cechy:

public class ApletZero
extends java.applet.Applet {

// opcjonalne elementy apletu
// bez których brak mu funkcjonalności
}

Ten aplet nie robi zupełnie nic. Doskonale jednak może posłużyć do zaznaczenia najważniejszych cech programu internetowego:

  • Program jest klasą, która musi dziedziczyć własności po klasie Applet znajdującej się w pakiecie java.applet. Od tej zasady istnieje wyjątek – w Javie o wersji wyższej niż 1.1 można zastosować dziedziczenie po klasie JApplet z pakietu javax.swing, który należy do biblioteki swing.
  • Klasa będąca apletem musi być publiczna. Jest to ważne zastrzeżenie, gdyż brak modyfikatora public nie jest dla tej klasy wykrywany przez kompilator jako błąd. Można więc zbudować poprawny pod względem formalnym aplet niebędący klasą publiczną, lecz w przyszłości nie będzie można go użyć.
  • Program do uruchomienia na stronie internetowej, czyli nasz aplet, jest klasą. Nigdzie natomiast nie widać miejsca jej użycia, żadnej inicjacji obiektu i utworzenia jego egzemplarza. Utworzeniem egzemplarza tej klasy zajmuje się sama przeglądarka. Wykorzystuje do tego dwie grupy informacji – znaną sobie strukturę programu zdefiniowaną w standardzie opisującym sposób umieszczania apletów na stronie (stąd konieczność dziedziczenia po wskazanej klasie) oraz informacje dodatkowe (takie jak na przykład nazwa klasy) umieszczone w pliku HTML. Dopiero złożenie obu informacji pozwala na poprawne uruchomienie apletu.

Podsumowanie

To co zaprezentowałem w tym artykule, to tylko początek podejścia do programowania obiektowego w Javie. Chciałbym uczulić czytelników na to, że jakkolwiek jest to język ortodoksyjnie obiektowy, to umiejętność programowania w Javie nie oznacza jeszcze, że potrafi się programować z wykorzystaniem metodologii obiektowej. W kolejnych artykułach będę się starał przybliżyć czytelnikom ten sposób myślenia, co umożliwi szybsze i efektywniejsze tworzenie rozbudowanych aplikacji.