You are on page 1of 18

Programowanie obiektowe - Spis tresci

Lekcja 1: Wprowadzenie
Lekcja 2: Programowanie zorientowane obiektowo
Segment 1: Zasady programowania obiektowego
Segment 2: Od projektu do programu - metodologia programowania obiektowego
Segment 3: Porównanie języków programowania obiektowego: JAVA, PASCAL, C++
Lekcja 3: Podstawowe elementy języka Java
Lekcja 4: Zaawansowane elementy języka Java
Lekcja 5: Wyjątki krytyczne
Lekcja 6: Pakiety - biblioteki Javy
Lekcja 7: Interfejs graficzny AWT
Lekcja 8: Interfejs graficzny JFC (Swing)
Lekcja 9: Programowanie współbieżne - wielowątkowość
Lekcja 10: Operacje wejścia/wyjścia
Lekcja 11: Komunikacja sieciowa
Lekcja 12: Aplikacje multimedialne

Dodatek: Program Kalkulator - stosowanie dobrych praktyk programistycznych


Programowanie obiektowe - Lekcja 2

Zadaniem lekcji drugiej jest wprowadzenie w podstawowe zagadnienia programowania


zorientowanego obiektowo. Programowanie takie różni się od programowania proceduralnego w sposób
zasadniczy, ale na szczęście dosyć dobrze współgra z naturalnym dla człowieka, uogólniającym podejściem
do rozwiązywania problemów.

Lekcję rozpoczniemy od określenia czym jest programowanie obiektowe, a następnie spróbujemy


wytłumaczyć kluczowe dla niego mechanizmy, czyli przede wszystkim enkapsulację, dziedziczenie oraz
polimorfizm. Brzmi to może nieco nieprzyjemnie, ale po bliższym przyjrzeniu nabiera pewnego uroku.
Ponadto parę słów poświęcimy metodologii projektowania aplikacji obiektowych — ot, tyle żeby wiedzieć z
grubsza jak pisać programy w sposób jasny i przejrzysty. Zapraszamy do świata obiektów.
Programowanie obiektowe - Lekcja 2

1. Zasady programowania obiektowego


Zasadniczo programowanie można określić jako pewnego rodzaju symulowanie zjawisk rzeczywistych
— program obliczający silnię z zadanej liczby odpowiada wykonaniu pewnej ilości mnożeń kolejnych liczb
naturalnych np. na kartce papieru. Zjawiska te zazwyczaj są daleko bardziej skomplikowane niż wspomniane
obliczenia silni, a nieszczęsny programista musi umieć przedstawić je w formie umożliwiającej
wykorzystanie możliwości komputera. Przez długi czas jedyną drogą programowania było rozbicie
zagadnienia na szereg małych kroczków — koncepcja programu polegała na zaprojektowaniu sekwencji
odpowiednich poleceń, czyli algorytmu, a następnie zapisaniu go w odpowiedni dla danego języka
programowania sposób. Podstawowym zadaniem programisty jest tu rozłożenie skomplikowanego problemu
na szereg prostych, podstawowych zagadnień prowadzące do zdefiniowania odpowiednich funkcji i
procedur. To podejście do programowania, nazywane proceduralnym albo strukturalnym, jest ściśle
powiązane z naturalnym sposobem "myślenia" komputera i, siłą rzeczy, niezbyt naturalne dla umysłów
ludzkich (a w każdym razie dla większości umysłów). Co gorsza — w miarę komplikowania zagadnienia do
rozwiązania — możliwości utrzymania kontroli nad powstającym programem znacząco maleją. Dobrym
przykładem na to, jak traci się kontrolę nad wielokrotnie rozbudowywanym programem są niektóre systemy
operacyjne .

W celu ograniczenia ilości zagadnień nad którymi programista musi jednocześnie zapanować przy
tworzeniu programu (szczególnie obszernego) opracowana została koncepcja programowania obiektowego,
której jedną ze szczęśliwszych realizacji jest JAVA. Można powiedzieć, że programowanie obiektowe
koncentruje się raczej na zadaniu do wykonania niż na sposobie jego realizacji. Program obiektowy można
opisać jako zbiór obiektów współpracujących ze sobą w określony sposób w celu realizacji postawionego
zadania, a istotę programowania obiektowego można ująć jako "sterowanie obiektami". Dobrą analogią może
być znów wspomniany wcześniej komputer. Stanowi on zbiór podzespołów (obiektów) — np. kart
rozszerzeń, pamięci, procesorów itp. połączonych ze sobą przy pomocy standardowych złącz przekazujących
informacje w odpowiednim, uzgodnionym standardzie. Jeżeli nawet każdy z nich zostanie wyprodukowany
przez innego producenta i zakupiony u innego sprzedawcy to będą ze sobą współpracować tak długo, jak
długo będą korzystać ze standardowych zestawów złącz. Mało tego — urządzeniami tymi możemy sterować
w łatwy sposób przy pomocy klawiatury i myszki, nie wnikając w szczegóły ich działania.

Nawiasem mówiąc nieuchronny rozwój technologiczny co jakiś czas wymusza zmianę wspomnianych
standardów — wie o tym każdy, kto próbował zainstalować nowoczesną kartę grafiki na starej płycie
głównej. Mówiąc trywialnie — nie ma jej w co wsadzić — co jest konsekwencją faktu, że nie tworzy się już
obiektów klasy "stara płyta główna". Podobne zjawisko można zaobserwować przeglądając dokumentację
JAVY — przy części klas figuruje nazwa "deprecated" oznaczająca wycofanie z użycia.

Obiektowy sposób programowania w szczególności zmniejsza wrażliwość na starzenie i


rozbudowywanie programu. Poszczególne obiekty działają niezależnie od siebie, a powiązania pomiędzy
nimi są łatwe do rozszyfrowania, co umożliwia łatwe usunięcie z programu niektórych jego elementów bez
obawy, że inne przestaną działać. Dodatkową zaletą jest naturalna możliwość pracy nad programem w
dowolnie dużym zespole — każdy programista zajmuje się swoim obiektem lub grupą obiektów, które po
połączeniu ze sobą powinny bez problemu współdziałać (oczywiście pod warunkiem dobrego projektu
wstępnego).

Tak jak już wspominaliśmy myślenie kategoriami obiektów, polegające na uogólnianiu zjawisk, w
zasadzie jest naturalną umiejętnością każdego człowieka. Zabierając się za rozwiązywanie jakiegoś
złożonego problemu większość ludzi najpierw formułuje koncepcję ogólną — czyli w przypadku programu
podstawowe funkcje, które następnie są uszczegóławiane — co w przypadku programu odpowiada
opracowaniu szczegółowych podprogramów, odpowiedzialnych za małe fragmenty realizowanego zadania.
Przywołując z uporem maniaka przykład komputera — jeżeli nasz kolega pokazuje nam z dumą swój nowy
komputer najpierw widzimy jednostkę w całości, a następnie, czy chcemy czy nie, usłyszymy cały ciąg
informacji szczegółowych — jaki ma procesor, jaką kartę graficzną, dysk i tak bez końca.
Programowanie obiektowe - Lekcja 2

Podobna sytuacja występuje w koncepcjach programowania — niekoniecznie nawet obiektowego —


najpierw formułowana jest ogólna koncepcja algorytmu, potem szczegółowe podprogramy. W eleganckim
programowaniu proceduralnym unika się zmiennych globalnych, dążąc do przekazywania informacji za
pomocą parametrów funkcji.

Język PASCAL czy C, w którym w każdym miejscu programu można mieć dostęp do dowolnej
zmiennej globalnej pozwolił leniwym programistom posłać elegancję programowania w kąt. Skutki bywają
opłakane — w niechlujnie napisanym programie tak naprawdę nie mamy pewności czy wywołanie określonej
funkcji nie spowoduje zmiany zmiennych globalnych. I wtedy ryzykujemy KATASTROFĄ, albo co najmniej
wyjątkiem krytycznym .

Im bardziej wykorzystujemy zmienne globalne w programie tym prawdopodobieństwo katastrofy


większe. Dlatego też języki zorientowane obiektowo (jak Java) wyposażone są w mechanizmy wymuszające
stosowanie obiektów. Należą do nich: enkapsulacja (nazywana też kapsułkowaniem lub hermetyzacją),
dziedziczenie oraz polimorfizm (wielopostaciowość). Przyjrzyjmy się im bliżej.

(1.1) Enkapsulacja

Enkapsulacja (kapsułkowanie, hermetyzacja) polega w ogólności na łączeniu zmiennych i instrukcji,


wykonujących na nich działania w obrębie wspólnego obiektu i uniemożliwianie bezpośredniego dostępu do
zmiennych z zewnątrz. Można ją sobie wyobrazić jako czarną skrzynkę chroniącą dane i funkcje przed
dostępem ze strony innych fragmentów programu. Dobrym przykładem może tu być najprostszy edytor tekstu
— niezależnie od rodzaju komputera czy systemu operacyjnego całe jego działanie dla użytkownika polega
na wpisywaniu lub kasowaniu znaków z klawiatury — wszystkie szczegóły operacji wykonywanych przez
edytor aby zapisać, odczytać czy skasować znaki statystycznego użytkownika nie obchodzą.

Osiągnięcie hermetyzacji w Javie umożliwia klasa (class). Cóż to jest? Klasa (każda) stanowi model
abstrakcyjny pewnej grupy obiektów wyróżniających się taką samą strukturą i zachowaniem. Inaczej mówiąc
klasa jest szablonem użytym do tworzenia obiektu. Proces tworzenia obiektu za pomocą klasy nazywany jest
tworzeniem egzemplarza klasy (albo jej instancji). Obiekt (object) stanowi pojedynczy egzemplarz klasy
(nazywany także instancją klasy — instance of a class) — zachowujący wszystkie jej właściwości.

Pisząc program w języku zorientowanym obiektowo nie definiujemy wszystkich obiektów z osobna —
definiujemy tylko klasy używane do tworzenia takich obiektów. Po uruchomieniu programu z zestawu
zdefiniowanych klas tworzone są odpowiednie obiekty.

Podstawowym zadaniem programisty obiektowego jest więc stworzenie odpowiedniego zestawu klas.
Język JAVA ma zdefiniowane setki klas i ich liczba ciągle rośnie, co oznacza że projektowanie zestawu klas
niejednokrotnie można ograniczyć do ich wyszukania w dokumentacji i ewentualnej twórczej adaptacji.

Dane należące do klasy i określające jej strukturę są reprezentowane przez zmienne egzemplarzowe,
umożliwiające zapamiętanie informacji o aktualnym stanie obiektu. Rodzaj wykonywanego przez klasę
zadania i sposób uzyskania dostępu do danej klasy określają jej metody (methods), czyli funkcje korzystające
ze zmiennych egzemplarzowych. Przypomina to pojęcie podprogramu w językach proceduralnych.

Zasadniczym celem hermetyzacji jest uproszczenie programu. Dla programisty istotne jest jedynie
zadanie jakie wykonuje dany obiekt, oraz sposób skorzystania z niego, a nie wnikanie w niuanse jego
działania. Ten sposób myślenia może wydawać się nieco powierzchowny, ale z całą pewnością jest właściwy
naturze ludzkiej. Nie zastanawiamy się przecież włączając światło jak działa elektrownia (a w każdym razie
niezbyt często się zastanawiamy). Z drugiej strony elektrownia dostarczając nam energię elektryczną daje
nam ograniczone do niezbędnego minimum możliwości manipulowania tą energią — możemy ją jedynie
włączyć i wyłączyć. Strach pomyśleć co by było, gdyby użytkownik mógł na przykład manipulować
częstotliwością czy napięciem w sieci energetycznej, albo prędkością obrotową turbin elektrowni. W
podobny sposób można ukryć szczegóły funkcjonowania klasy — wystarczy zadeklarować zmienne i metody
klasy jako prywatne, aby uczynić je niedostępnymi dla fragmentów programu znajdujących się poza daną
klasą. Oczywiście elementy interfejsu klasy, czyli te które umożliwiają jej komunikację z innymi klasami,
Programowanie obiektowe - Lekcja 2

należy zadeklarować jako publiczne. Dobrą praktyką jest ograniczanie elementów publicznych do
niezbędnego minimum — gwarancja, że manipulując jakimś fragmentem programu nie można w sposób
przypadkowy zmienić stanu innych fragmentów, znacznie ułatwia programowanie. Pomimo tego, że
publiczne mogą być zarówno zmienne jak i metody, najbezpieczniej zadeklarować wszystkie zmienne jako
prywatne, a wykonywanie na nich operacji powierzyć odpowiednim metodom publicznym.

Najkrócej rzecz ujmując klasa opisuje typy atrybutów obiektu oraz metody, za pomocą których można
wykonać na nich działanie. Obiekt z kolei jest pojedynczym egzemplarzem klasy. Jeżeli zdefiniujemy sobie
np. klasę "komputery" — to mój komputer, komputer kolegi, serwer Politechniki — wszystkie one są
obiektami (egzemplarzami) tej samej klasy, gdyż wszystkie są komputerami.

Klasa w języku Java składa się z dwóch różnych typów informacji — atrybutów i zachowania.

Atrybuty to informacje odróżniające jeden obiekt od drugiego. W obrębie klasy atrybuty definiowane są
poprzez zmienne. Zmienna instancji (zmienna egzemplarzowa, zmienna obiektowa) określa atrybut
pojedynczego obiektu. Klasa obiektu definiuje rodzaj atrybutu, a każdy egzemplarz klasy przechowuje swoją
własną wartość danego atrybutu. Zmiennym instancji można przypisać wartość stałą lub zmieniającą się
podczas używania obiektu.

Niektóre zmienne posiadają taką samą wartość dla wszystkich obiektów danej klasy — są to tzw.
zmienne klasowe (class variables). Zmienna klasowa jest więc atrybutem całej klasy. Siłą rzeczy odnosi się
ona do wszystkich obiektów danej klasy, a każdy obiekt danej klasy będzie miał do niej dostęp.

Zachowania klasy są implementowane za pomocą metod. Zachowanie określa czynności, które klasa
obiektów może wykonywać w stosunku do samej siebie jak i do należących do niej obiektów. Zachowania
mogą modyfikować atrybuty obiektów, otrzymywać i wysyłać informacje albo wymuszać konkretne
działania. Metody stanowią grupy powiązanych ze sobą poleceń wewnątrz klasy obiektów, wykorzystywane
do realizacji określonych zadań. Analogicznie do zmiennych obiektowych i klasowych istnieją również
metody obiektowe i metody klasowe. Te pierwsze nazywane są zwykle po prostu metodami.

(1.2) Dziedziczenie

Dziedziczenie, które jest jednym z kluczowych założeń programowania obiektowego, jest


konsekwencją hierarchicznego sposobu uporządkowania rzeczywistości (programistycznej również). Można
je zdefiniować jako mechanizm przejmowania przez daną klasę zachowania i atrybutów innej klasy. Jako
przykład możemy tu rozważyć klasę "zwierzęta domowe" i jej poszczególne, arbitralnie określone podklasy,
przedstawione schematycznie poniżej.

Do klasy "zwierzęta domowe" zaliczymy oczywiście wszystkie zwierzęta, które ludzie świadomie i
celowo trzymają w swoich domach dla swojej (lub swoich dzieci) przyjemności, czyli m.in. psy, koty,
Programowanie obiektowe - Lekcja 2

chomiki, węże, świnki morskie, rybki itp. Wszystkim zwierzętom domowym można przypisać jakieś
charakterystyczne dla tej grupy atrybuty — każdy z nas potrafi to zrobić intuicyjnie, dlatego za zwierzę
domowe uznajemy np. psa, a nie uznajemy słonia (choć nie można wykluczyć, że ktoś może trzymać w domu
takiego milusińskiego). Takimi atrybutami mogą być: rozmiar, sposób zachowania się, rozpoznawanie
właściciela, wymaganie karmienia, etc. Zdefiniowanie atrybutów jest równoznaczne ze zdefiniowaniem
klasy. Do określonej podgrupy, wyróżnionej na podstawie cech biologicznych, mogą należeć np. koty, które
z kolei podzieliliśmy — oczywiście arbitralnie — na dwie podgrupy: kotki i kocury. Każda kotka, np. nasza
domowa (która jest obiektem klasy "kotki") ma wszystkie cechy swojej grupy nadrzędnej, czyli kotów. Z
kolei grupa "koty" ma wszystkie cechy grupy wyższej, czyli zwierząt domowych. Oczywiście grupa zwierząt
domowych również może być opisana jako podgrupa innej — np. zwierząt.

Można powiedzieć, że klasa "kotki" dziedziczy wszystkie atrybuty klasy "koty", a klasa "koty"
dziedziczy wszystkie atrybuty klasy "zwierzęta domowe". W terminologii programowania klasa dziedzicząca
nazywana jest podklasą (lub klasą podrzędną), klasa dziedziczona natomiast nadklasą (lub klasą nadrzędną,
ewentualnie bazową), czyli klasa "koty" jest klasą nadrzędną dla podklasy "kotki" i podrzędną dla nadklasy
"zwierzęta domowe", które z kolei byłyby podklasą klasy bazowej "zwierzęta". Każda klasa siłą rzeczy
dziedziczy wszystkie atrybuty wszystkich klas nadrzędnych, należących do tej samej hierarchii klas (albo
drzewa dziedziczenia). Widać z tego że w ogólności im niższą pozycję zajmuje dana klasa w hierarchii klas
tym bardziej jest wyspecjalizowana.

Poprzez mechanizm dziedziczenia dana klasa otrzymuje pełną funkcjonalność innej, już istniejącej
klasy. Zamiast więc podczas pisania programu tworzyć nową klasę od zera wystarczy odziedziczyć
odpowiednio dobraną nadklasę, a następnie wskazać czym nowa różni się od klasy dziedziczonej.

Tworzenie hierarchii klas

Jeżeli podczas pisania programu tworzymy dużą ilość nowych klas to najwygodniej będzie od razu
zaprojektować sobie odpowiednią strukturę hierarchii klas, tak aby nowe klasy dziedziczyły z już
istniejących i jednocześnie same tworzyły strukturę hierarchiczną. Zalety stosowania struktury drzewa klas są
dosyć oczywiste. Atrybuty wspólne dla wielu klas mogą być umieszczone w klasie nadrzędnej — co
umożliwia łatwe ich wykorzystanie w klasach podrzędnych, bez konieczności wpisywania od nowa linii
kodu. Ponadto każda zmiana w klasie nadrzędnej automatycznie pociągnie za sobą zmiany w klasach
dziedziczących, uwalniając nas od konieczności poprawiania kodu i rekompilacji.

Stosowanie (rozsądne) omówionych wyżej elementów programowania pozwala na tworzenie


programów w sposób bardziej niezawodny i łatwo modyfikowalny. Dobrze zaprojektowana hierarchia klas
może być wykorzystana w wielu kolejnych programach, oszczędzając czas potrzebny na ich testowanie i
uruchamianie.

Dziedziczenie w praktyce

Jeżeli stworzymy nowy obiekt — dajmy na to egzemplarz "moja kotka" klasy "kotki" — to w jego
szablonie znajdą się wszystkie, zebrane razem elementy klas nadrzędnych — w szczególności wszystkie
zmienne utworzone w obrębie obiektu oraz zdefiniowane w klasach nadrzędnych. Podobnie zachowują się
metody — nowo utworzony obiekt "moja kotka" ma dostęp do wszystkich metod swojej klasy macierzystej
"kotki" oraz klas nadrzędnych "koty", "zwierzęta domowe". Jeżeli w obiekcie zostanie wywołana metoda
"zobacz co w misce" to interpreter będzie jej poszukiwał najpierw w klasie macierzystej "kotki", a jeżeli jej
tam nie znajdzie to kolejno w coraz wyższych klasach nadrzędnych, aż do skutku. Ponieważ w naszym
przykładzie założyliśmy, że do klasy zwierząt domowych należą również rybki, to definicji metody "zobacz
co w misce" raczej należy się spodziewać w klasie "koty" niż "zwierzęta domowe". Oczywiście tak naprawdę
wszystko zależy od nas i od naszego sposobu ustawienia hierarchii klas — właściwie niby dlaczego rybki nie
miałyby zobaczyć co jest w misce?
Programowanie obiektowe - Lekcja 2

Zagadnienie poszukiwania metody skomplikuje się nieco, gdy w podklasie znajdzie się definicja
metody o takiej samej nazwie, typie zwracanych wartości i argumentach jak metoda zdefiniowana w klasie
wyższej. No to która, do licha, zostanie użyta? Otóż po prostu ta szybciej znaleziona, czyli metoda niższej
klasy. Oznacza to, że możemy w klasie podrzędnej zdefiniować metodę, która będzie zapobiegała wywołaniu
odpowiadającej jej metody z klasy nadrzędnej! Takie postępowanie nazywane jest maskowaniem albo
przekrywaniem metody. Oczywiście metoda maskująca musi mieć taką samą nazwę, typ zwracanych
wartości i argumenty jak metoda maskowana. W naszym przypadku możemy sobie wyobrazić że klasa
"zwierzęta domowe" ma zdefiniowaną metodę "zobacz co jest w misce" jako pytanie o ogólnym charakterze
poznawczym, natomiast podklasa "koty domowe" ma taką samą metodę, ale jako konkretne pytanie
merytoryczne.

Dziedziczenie pojedyncze

Jeżeli dana podklasa dziedziczy po pojedynczej klasie nadrzędnej, to taki mechanizm nazwiemy
oczywiście dziedziczeniem pojedynczym (single inheritance). Dziedziczenie wielokrotne (multiple
inheritance) oznacza, że podklasa może mieć większą ilość rodziców (czyli klas nadrzędnych). Ten drugi
mechanizm pozwala na tworzenie klas o praktycznie dowolnych zachowaniach, ale jednocześnie znakomicie
komplikuje zagadnienie definicji klas. Ponadto — zastanówmy się — jak zachowywałby się obiekt klasy
dziedziczącej zachowania po klasach "psy", "koty" i "chomiki" — chyba zaraz sam by się zjadł .

Z tego powodu JAVA, w odróżnieniu od np. C++, umożliwia jedynie dziedziczenie jednokrotne — od
pojedynczej klasy nadrzędnej (która oczywiście może dziedziczyć po innych kolejnych nadklasach). Z faktu,
że każda klasa w JAVIE może mieć tylko jedną klasę nadrzędną i nieskończenie wiele klas podrzędnych,
wynika, że powinna istnieć jakaś superklasa, która nie dziedziczy po żadnej klasie. W hierarchii klas języka
JAVA tą superklasą jest klasa Object. W praktyce programowania — jeśli przy tworzeniu nowej klasy nie
określimy klasy nadrzędnej — to oznacza to, że nowa klasa dziedziczy bezpośrednio po klasie Object.

Interfejsy

Zastosowany w JAVIE mechanizm pojedynczego dziedziczenia ma tę zaletę, że wzajemne relacje


pomiędzy poszczególnymi klasami są łatwe do zaprojektowania i opanowania. Z drugiej strony —
konsekwencją takiego mechanizmu są pewne ograniczenia, szczególnie uciążliwe, gdy chcemy
zaimplementować jednakowe zachowania w kilku różnych klasach znajdujących się w różnych gałęziach
hierarchii klas. Z tego powodu JAVA udostępnia dodatkowo hierarchię interfejsów, czyli abstrakcyjnych
Programowanie obiektowe - Lekcja 2

zachowań, które mogą być dołączane do klasy nie dziedziczącej tych zachowań ze swojej nadklasy. W
rzeczywistości interfejs można określić jako abstrakcyjną klasę, która zawiera jedynie nagłówki
abstrakcyjnych metod i statyczne pola danych (co oczywiście oznacza, że nie można utworzyć egzemplarza
danego interfejsu, ale można dokonać przypisania do zmiennej typu konkretnego interfejsu ). Jak widać
interfejsy stanowią uzupełnienie hierarchii klas. Wykorzystując ten mechanizm można utworzyć nową klasę,
która będzie miała oczywiście tylko jedną klasę nadrzędną, ale jednocześnie będzie mogła uzyskać
dodatkowe zachowania z hierarchii interfejsów. Co więcej — w pojedynczej klasie możemy
zaimplementować wiele interfejsów.

Warto wiedzieć że interfejsy podobnie jak klasy są kompilowane do postaci plików *.class. Mało tego
— programiści bardzo często mówiąc "klasa" mają na myśli "klasa lub interfejs" — te dwa elementy mogą
być często traktowane niemal tak samo.

(1.3) Polimorfizm

Polimorfizm, czyli wielopostaciowość, to możliwość tworzenia w obrębie hierarchii klas wielu metod
o tej samej nazwie, czyli wielu wersji tej samej metody, realizujących w ogólności różne zadania. W
proceduralnych językach programowania, aby wykonać różne zadania należy również utworzyć różne
funkcje — jedna funkcja do jednego zadania. Polimorfizm umożliwia zastosowanie do różnych zadań tej
samej metody, a właściwie wielu metod ukrytych pod jedną nazwą. To, która implementacja zostanie
wybrana po wywołaniu metody zależy od typu przekazywanych jej parametrów, które stanowią dla
wywoływanej metody informacje wejściowe, bądź też od typu obiektu, na rzecz którego dana metoda jest
wołana.

Wyobraźmy sobie klasę "kot domowy" — reprezentującą oczywiście koty domowe i metodę "zobacz
co w misce") w dwóch wersjach. W zależności od pobranego parametru metoda będzie miała różną
implementację. Jeżeli metoda "zobacz co w misce" pobierze parametr będący obiektem klasy "pyszne
jedzonko" to implementacją tej metody będzie ukontentowane mruczenie, połączone z pałaszowaniem
zawartości miski. Jeżeli jednak metoda "zobacz co w misce" pobierze parametr "nic" to oczywiście ta sama
metoda spowoduje przeraźliwe miauczenie (w niektórych przypadkach przebieg metody może być nieco inny
— np. ponura rezygnacja). W każdym razie potwierdza to fakt, że nasza metoda może mieć różne, krańcowo
odmienne implementacje. Opisana sytuacja nazywana jest przeciążaniem.

Gdybyśmy jednak mieli klasę "kot domowy" z metodą "zobacz co w misce" w jednej wersji oraz klasę
"moja kotka" dziedziczącą po niej i posiadającą nową definicję metody "zobacz co w misce", sytuacja
nazywana byłaby przekrywaniem. W tym przypadku prawidłowa implementacja (miałczenie albo drapanie)
wybierana by była zależności od tego, czy zwartość miski sprawdza "kot domowy" czy "moja kotka".

(1.4) Definiowanie klas, interfejsów i metod

Klasy

Definicja każdej klasy zbudowana jest w sposób przedstawiony poniżej (nawiasy kwadratowe
oznaczają elementy opcjonalne):
[ModyfikatorDostępu] [ModyfikatorKlasy] class NazwaKlasy
[extends PojedynczaKlasaNadrzędna]
[implements Interfejsy]
{
ciało klasy: zmienne (pola),
ciąg instrukcji static,
konstruktor,
definicje metod
}

ModyfikatorDostępu:

public (publiczna) — klasa dostępna dla wszystkich klas, a więc udostępniana na zewnątrz pakietu, w
Programowanie obiektowe - Lekcja 2

którym się znajduje; w pojedynczym pliku może wystąpić wiele klas, ale jedna musi być publiczna;
<puste> — klasa pakietowa, czyli dostępna jedynie dla klas z danego pakietu;
private (prywatna) — klasa wewnętrzna dostępna jedynie wewnątrz klasy, w której jest zdefiniowana.

ModyfikatorKlasy:

abstract (abstrakcyjna) — klasa mogąca zawierać, poza zwykłymi metodami, metody abstrakcyjne
(niezaimplementowane); nie można utworzyć egzemplarza takiej klasy, choć można dokonać
przypisania do zmiennej typu klasy abstrakcyjnej; klasa taka stanowi szablon dla tworzenia podklas;
final (finalna) — od klasy tego typu niemożliwe jest utworzenie klas pochodnych; klasy z
modyfikatorem final stosuje się zwykle w celu podwyższenia poziomu bezpieczeństwa aplikacji.

Interfejsy

Interfejs jest kolekcją deklaracji metod bez implementacji; implementacja metod interfejsu odbywa się
dopiero w klasie, która z niego bezpośrednio korzysta. Definicja interfejsu jest podobna do definicji klasy.
[ModyfikatorDostępu] [ModyfikatorInterfejsu] interface NazwaInterfejsu
[extends Interfejsy]
{
ciało interfejsu: zmienne (pola),
definicje metod (bez implementacji)
}

ModyfikatorDostępu:

public (publiczny) — interfejs dostępny dla wszystkich klas;


protected (chroniony) — interfejs wewnętrzny dostępny jedynie wewnątrz hierarchii dziedziczenia;
private (prywatny) — interfejs wewnętrzny dostępny jedynie wewnątrz klasy, w której jest
zdefiniowany.

ModyfikatorInterfejsu:

static (statyczny) — interfejs wewnętrzny wspólny dla wszystkich obiektów klasy, w której jest
zdefiniowany.

Konstruktory

Konstruktor jest specjalną metodą o nazwie klasy wołaną podczas tworzenia obiektu.
[ModyfikatorDostępu] NazwaKonstruktora([ListaParametrów])
{
ciało konstruktora: kod źródłowy konstruktora
}

ModyfikatorDostępu:

public — konstruktor dostępny dla każdego obiektu;


protected — konstruktor może być używany przez obiekty swojej klasy i jej wszystkich klas
potomnych oraz innych klas z tego samego pakietu;
private — dostęp do konstruktora mają tylko obiekty z klasy, w której został zdefiniowany;
<puste> — dostęp domyślny — konstruktor dostępny dla obiektów klas z tego samego pakietu.

Metody

Deklaracja metody jest podobna do deklaracji funkcji w języku C:


[ModyfikatorDostępu] [ModyfikatorMetody]
TypZwracanejWartości NazwaMetody([ListaParametrów]) [throws Wyjątki]
{
ciało metody: kod źródłowy metody
Programowanie obiektowe - Lekcja 2
}

ModyfikatorDostępu:

public — metoda dostępna dla każdego obiektu;


protected — metoda może być używana przez obiekty swojej klasy i jej wszystkich klas potomnych
oraz innych klas z tego samego pakietu;
private — dostęp do metody mają tylko obiekty z klasy, w której została zdefiniowana;
<puste> — dostęp domyślny — metoda dostępna dla obiektów klas z tego samego pakietu.

ModyfikatorMetody:

static — używany do tworzenia metod klasowych — wspólnych dla wszystkich obiektów danej klasy
— oznacza to, że do wywołania metody statycznej nie jest potrzebny obiekt; pola danych (zmienne)
mogą być również definiowane jako statyczne;
abstract — używany do tworzenia metod abstrakcyjnych (nie posiadających implementacji);
final — używany do uniemożliwienia przekrywania metody;
native — używany do oznaczenia, że metoda jest implementowana w języku innym niż Java, co może
być użyteczne, jeżeli mamy do dyspozycji bogatą bibliotekę funkcji napisanych w innym języku (np.
C);
synchronized — używany do synchronizacji metod w programach wielowątkowych — konkurencyjne
wątki mogą odwoływać się do metod tych samych obiektów — wywołanie metody synchronizowanej
spowoduje, że na początku działania zablokuje ona dostęp do obiektu i odblokuje go dopiero po
zakończeniu działania.

Pola danych

Składnia definicji pól jest podobna do składni deklaracji zmiennych w języku C:


[ModyfikatorDostępu] [ModyfikatorPola] Typ NazwaPola

ModyfikatorDostępu:

analogicznie jak w przypadku metod.

ModyfikatorPola:

static — oznacza pole statyczne — zmienną klasową;


final — oznacza, że wartości tego pola nie można zmienić — czyli oznacza stałą;
transient — oznacza pola wyłączane z procesu serializacji;
volatile — oznacza pole "ulotne", które może być niespodziewanie modyfikowane przez inne moduły
programu; w aplikacji wielowątkowej każdy wątek będzie miał zapewniony dostęp do aktualnej
wartości takiego pola; bez opisywanego modyfikatora wątki będą posiadać jedynie lokalne kopie
wartości pola, które co pewien czas będą synchronizowane; w pewnym sensie dostęp do pola valatile
jest atomowy (nieprzerywalny).
Programowanie obiektowe - Lekcja 2

2. Od projektu do programu - metodologia programowania


obiektowego
Z całą pewnością istnieje co najmniej kilka metodologii tworzenia programu. Niewątpliwie jedną z
bardziej popularnych jest metoda swobodnej improwizacji. Jednak stworzenie dobrego, przejrzystego
programu tą bardzo popularną metodą jest w zasadzie niemożliwe. Dlatego, jeżeli program, który próbujemy
stworzyć jest dłuższy niż kilkanaście linii, proponujemy mimo wszystko spędzić kilka chwil nad rozsądnym i
przemyślanym zaprojektowaniem jego architektury.

Poniżej przedstawiamy naszą własną propozycję metodycznego podejścia do projektowania bardziej


zaawansowanego programu.

1. Określenie zadań do realizacji — rozbicie zadania głównego na kilka fragmentów.


Na pewno łatwiej będzie tworzyć, a następnie uruchamiać nasz program, jeżeli podzielimy go na
wyspecjalizowane fragmenty, odpowiadające realizacji zadań szczegółowych, niż od razu napisać cały
program i zmusić go do przejścia choćby etapu kompilacji. Ta uwaga oczywiście nie dotyczy geniuszy
komputerowych — rzadko spotykanych, ale jednak istniejących.
2. Określenie obiektów wykonujących poszczególne zadania.
Oczywiście, jeżeli podstawową jednostką w programowaniu obiektowym jest obiekt, to jasne jest, że
do różnych zadań należy przypisać różne obiekty. Z całą pewnością innymi obiektami będziemy
posługiwać się do zapisu danych na dysku, a innymi do wyświetlania grafiki. Dobrze jest w sposób
przemyślany określić, jakich zachowań oczekujemy po naszym obiekcie i odpowiednio do nich
zaprojektować klasy (albo skorzystać z już zaprojektowanych).
3. Skonstruowanie przemyślanej hierarchii klas.
Na pewno mniej się narobimy przy pisaniu naszego programu, jeżeli wykorzystamy typowe dla
programowania obiektowego mechanizmy dziedziczenia. Stworzenie odpowiedniej hierarchii klas i
przemyślana implementacja dziedziczenia umożliwi wielokrotne wykorzystanie napisanego kodu.
4. Określenie trybów awaryjnych i efektów krytycznych. Diagnostyka awarii.
Sytuacji krytycznych, które mogą zaprowadzić nasz program prosto w krzaki, jest całe mnóstwo. Na
niektóre z nich, związane ze specyfiką systemu operacyjnego czy samym sprzętem, niewiele możemy
poradzić. Możemy jednak (i powinniśmy) zaprojektować mechanizmy obsługi sytuacji wyjątkowych
związanych z logiką działania naszego programu oraz z zakładanym brakiem logiki potencjalnego
użytkownika .
5. Implementacja.
Wszystko, co wymyśliliśmy do tej pory, musimy teraz ubrać w słowa zrozumiałe dla kompilatora, czyli
po prostu napisać program, poprawny pod względem logicznym i semantycznym. Wbrew pozorom,
wcale nie jest to najtrudniejszy fragment programowania. Musimy jeszcze nasz program zmusić do
poprawnego działania.
6. Uruchamianie i testowanie fragmentów programu.
To bardzo często najbardziej irytująca część programowania, ale zanim nasz program zacznie działać
jako całość dobrze jest uruchomić i sprawdzić działanie jego poszczególnych modułów — np.
interfejsu graficznego, obsługi sieci, metod zapisu i pobierania danych etc. Jeżeli poszczególne moduły
będą działały prawidłowo, to najprawdopodobniej cały program również nie będzie sprawiał kłopotów.
7. Uruchomienie całości.
Najważniejszy moment — łączymy wszystko w całość, kompilujemy… — i patrzymy, dlaczego nie
działa … Oczywiście zdarzają się takie sytuacje, że wszystko działa jak należy — prawdę mówiąc,
im więcej programujemy, tym częściej tak się zdarza, ale początki zazwyczaj bywają boleśnie trudne.
8. Testowanie całości programu.
Jeżeli nasz program ma służyć celom innym, niż własna satysfakcja i, co gorsza, mają z niego
korzystać inni użytkownicy — całość należy niestety bardzo solidnie przetestować, upewniając się, że
program pracuje poprawnie, jest niewrażliwy (albo mało wrażliwy) na podawanie nieprawidłowych
parametrów, nie zawiesza się w najmniej spodziewanych momentach (najlepiej gdyby w ogóle się nie
zawieszał, ale takich programów chyba nie ma). W zasadzie można zaryzykować stwierdzenie, że
Programowanie obiektowe - Lekcja 2

autor nigdy nie przetestuje prawidłowo swojego własnego programu — do maksymalnie pełnego
testowania najlepiej zaprosić kogoś, kto z tworzeniem programu nie miał nic wspólnego.

(2.1) Diagramy UML

Diagramy UML służą do przedstawiania relacji (często bardzo złożonych) zachodzących pomiędzy
obiektami tworzącymi program. Do ich tworzenia powstało wiele specjalistycznych narzędzi. Dwa
przykładowe (i darmowe) to:

Violet UML Editor, pozwalający na projektowanie struktury aplikacji; do ściągnięcia ze strony


http://alexdp.free.fr/violetumleditor/page.php ;
EssModel, odtwarzający strukturę aplikacji na podstawie kodu źródłowego lub bajtowego; do
ściągnięcia ze strony http://essmodel.sourceforge.net/ .

Asocjacja

Relacja asocjacji zachodzi między obiektami luźno związanymi. Współpracują one ze sobą, ale mogą
bez siebie istnieć, np. fotograf współpracujący z modelkami i miesięcznikiem o modzie.

Nietrudno zauważyć, że oczywiście modelki mogą pracować z innym fotografem , a sam fotograf może
pracować dla magazynu motoryzacyjnego .

Agregacja

Relacja agregacji jest nieco ściślejsza. Obiekt agregujący może istnieć samodzielnie, obiekty w nim
agregowane już nie (a przynajmniej ich samodzielne istnienie nie ma sensu). Obiekt agregujący musi zostać
stworzony jako pierwszy. Dobrym przykładem takiej relacji jest zegar ze wskazówkami.

Sam zegar — bez wskazówek — może istnieć i działać, rzecz jasna pod warunkiem, że ktoś lubi słuchać
tykania nie wiedząc jednocześnie, która jest godzina . Wskazówki bez zegara są zupełnie bezużyteczne .

Kompozycja

Relacja kompozycji jest jeszcze ściślejsza. W tym przypadku obiekt komponowany nie może istnieć
bez części składowych, bo po prostu jest z nich budowany. Idealnym przykładem jest tu komputer.
Programowanie obiektowe - Lekcja 2

Komputer nie może istnieć bez procesora, pamięci RAM itd. Części składowe mogą istnieć samodzielnie, ale
ich funkcjonalność manifestuje się dopiero w całym komputerze . Chcąc np. sprawdzić działanie pamięci
RAM czy procesora, trzeba je umieścić w komputerze.

Generalizacja-specjalizacja (dziedziczenie)

Relacja ta jest odzwierciedleniem podstawowego mechanizmu wykorzystywanego w programowaniu


obiektowym — dziedziczenia. Przykład prezentuje poniższy rysunek.

Strzałki pokazują kierunek generalizacji: pojazd generalizuje (uogólnia) samochód osobowy, autobus
specjalizuje (uszczegóławia) pojazd. Pociąg dziedziczy wszystkie cechy po obiekcie pojazd, zatem go
specjalizuje.

(2.2) Struktura MVC

MVC to tzw. architektoniczny wzorzec projektowy. Budując aplikację według jego założeń dzielimy ją
na trzy podstawowe moduły:

M jak model, czyli struktura danych, na których przeprowadza się operacje; są to same dane (zmienne,
tablice, zbiory, etc.) jak i logika programu (wszystkie zależności między danymi oraz dopuszczalne na
nich operacje);
V jak widok, czyli interfejs użytkownika; służy do prezentacji danych z modelu użytkownikowi;
można stworzyć wiele widoków przedstawiających dane w różny sposób;
C jak kontroler, czyli logika sterowania; obsługuje polecenia wydawane przez użytkownika, czyli
tłumaczy je na polecenia zrozumiałe przez model, nakazuje ich wykonanie, a następnie zmusza widok
do wyświetlenia zmodyfikowanych danych z modelu (model może również bezpośrednio informować
widok o konieczności wyświetlenia zmienionych danych); może istnieć wiele kontrolerów do obsługi
różnych klas poleceń.
Programowanie obiektowe - Lekcja 2

Wzorzec zapewnia separację danych od sposobów ich prezentacji oraz modyfikacji. Ułatwia to poprawianie i
rozwijanie stworzonego według jego założeń programu.

(2.3) Dobre rady dobrych wujków

Poza uwagami o charakterze ogólnym, można dodać kilka dobrych rad dotyczących samej techniki
pisania programów.

1. Metody powinny być w miarę możliwości krótkie — najwygodniej, gdy będą mieściły się na
pojedynczym ekranie — wtedy łatwo zapanować nad kodem i prześledzić, co tak naprawdę nasza
metoda wyprawia.
2. Dobrze jest stosować jasne, przejrzyste nazwy zmiennych, metod, klas i obiektów — oczywiście
wpisanie np. nazwy znajdzZnak zajmuje znacznie więcej czasu niż wpisanie nazwy zz, ale za to po
kilku tygodniach nadal wiemy, co mieliśmy na myśli.
3. Zadbajmy o przejrzystość kodu — korzystajmy z wcięć, otwierające i zamykające nawiasy klamrowe
piszmy w jednej kolumnie — te drobiazgi znakomicie ułatwią analizę kodu, a kosztują naprawdę
niewiele wysiłku.
4. Komentarze powinny być bezwzględnie pisane na bieżąco — jeżeli nie napiszemy ich od razu,
prawdopodobnie nie napiszemy ich nigdy (albo ich pisanie będzie męczarnią). Dobrze skomentowany
kod źródłowy czyta się dużo łatwiej, co szczególnie docenia się przy próbach modyfikacji programu.
5. Warto od razu, podczas pisania, uwzględniać obsługę wyjątków — bardzo często odkładamy ją na
później, a w efekcie albo o nich zapominamy, albo nie mamy czasu, aby się nimi zająć. Niektóre, co
bardziej wrażliwe, klasy Javy na szczęście same wymuszają na programiście obsługę wyjątków.
Programowanie obiektowe - Lekcja 2

3. Porównanie języków programowania obiektowego: JAVA,


PASCAL, C++
Poniżej zamieszczone zostało porównanie niektórych aspektów obiektowości trzech języków
programowania obiektowego: Javy, Pascala (w wydaniu FreePascal) i C++.

(3.1) Generalna struktura programu

Java
Czysta obiektowość. Nawet program główny — metoda main — musi być wewnątrz jakiejś klasy.
Pascal C++
Możemy deklarować zmienne globalne i definiować Możemy deklarować zmienne globalne i definiować
zwykłe (nieobiektowe) funkcje czy procedury. zwykłe (nieobiektowe) funkcje.

(3.2) Obiekty

Java
Wszystkie obiekty są dynamiczne, choć mogą posiadać statyczne pola czy metody. Nim odwołamy się
do jakiegoś niestatycznego pola albo wywołamy jakąś niestatyczną metodę — obiekt musi zostać
stworzony za pomocą konstruktora. Sama deklaracja nie wystarcza, bo nie przydziela miejsca w pamięci
na przechowywanie zmiennych obiektu.
Pascal C++
Obiekty mogą być statyczne, jak i dynamiczne. Aby Obiekty mogą być statyczne, jak i dynamiczne. Aby
używać obiektów statycznych wystarczy sama używać obiektów statycznych wystarczy sama
deklaracja (typu obiektowego i zmiennej). deklaracja.

(3.3) Skuteczność hermetyzacji obiektów

Java
Hermetyzacja jest absolutna. Do pól prywatnych obiektów nie można się dostać z innych obiektów.
Pascal C++
Do wszystkich pól obiektów można się dostać jak Do pól prywatnych obiektów można się dostać za
do pól zwykłych rekordów. pomocą tzw. funkcji zaprzyjaźnionych.

(3.4) Polimorfizm

Java
Wszystkie niestatyczne metody są polimorficzne. Gdy wołana jest metoda, najpierw następuje
sprawdzenie, na rzecz jakiego typu obiektu następuje wywołanie, a dopiero potem uruchamiana jest
odpowiednia metoda. Jest to tzw. dynamiczne wiązanie metod.
Pascal C++
Funkcje i procedury mogą stać się polimorficzne po Funkcje mogą stać się polimorficzne po
odpowiedniej deklaracji z użyciem słowa virtual. odpowiedniej deklaracji z użyciem słowa virtual.
Bez tego związanie metod z obiektami następuje w Bez tego związanie metod z obiektami następuje w
trakcie kompilacji, a nie uruchamiania programu. trakcie kompilacji, a nie uruchamiania programu.
Programowanie obiektowe - Lekcja 2

(3.5) Zmienne wskaźnikowe i zarządzanie pamięcią

Java
Nie ma zmiennych wskaźnikowych. Bezpośrednie odwoływanie się do adresów pamięci jest niemożliwe.
Nie można bezpośrednio zarządzać pamięcią operacyjną, np. kasować niepotrzebnych obiektów.
Pascal C++
Można używać zmiennych wskaźnikowych i Można używać zmiennych wskaźnikowych i
bezpośrednio zarządzać pamięcią operacyjną. bezpośrednio zarządzać pamięcią operacyjną.
Programowanie obiektowe - Lekcja 2

Zad.1.Omówić pojęcie enkapsulacji.

Zad.2.Wyjaśnić, w jaki sposób enkapsulacja poprawia kontrolę dostępu do danych.

Zad.3.Omówić pojęcie dziedziczenia.

Zad.4.Wyjaśnić, czy mechanizm dziedziczenia wpływa na przejrzystość pisanego programu.

Zad.5.Omówić pojęcie polimorfizmu.

Zad.6.Wyjaśnić, na czym polega przeciążanie metod.

Zad.7.Omówić pojęcie klasy i podać przykładową definicję.

Zad.8.Omówić pojęcie interfejsu i podać przykładową definicję.

Zad.9.Omówić różnice między klasą a obiektem.

Zad.10.Omówić różnice między klasą a interfejsem.

Zad.11.Wyjaśnić, do czego służą diagramy UML.

Zad.12.Omówić wybrane dwa typy relacji mogącymi zachodzić między obiektami.


Programowanie obiektowe - Lekcja 2

zestaw metod do zaimplementowania pozwalający ominąć ograniczenia


interfejs
wynikające z pojedynczego dziedziczenia.
zestaw deklaracji pól i definicji metod będący szablonem do tworzenia
klasa
obiektów.
metoda funkcja wykonująca operacje na polach obiektu.
Model-View-Controller, Model-Widok-Kontroler, architektoniczny
MVC
wzorzec projektowy wyodrębniający trzy podstawowe moduły aplikacji.
obiekt (instancja, egzemplarz) pojedyncze wystąpienie zdefiniowanego w klasie zestawu pól i metod.
pole (zmienna, atrybut) obszar pamięci przechowujący wartość przetwarzaną przez metody.
jeden z aspektów polimorfizmu, definiowanie w obrębie jednej klasy
wielu metod o tej samej nazwie, różniących się jednak liczbą bądź typem
przeciążanie metod argumentów (danych wejściowych; typ danych zwracanych przez metodę
(overriding) jest bez znaczenia). Przypisywanie właściwego kodu do wywołania
następuje w trakcie kompilacji programu na podstawie analizy liczby lub
typów argumentów użytych w wywołaniu metody.
jeden z aspektów polimorfizmu, definiowanie w podklasie metod o takiej
samej nazwie jak w klasie bazowej, czyli de facto przedefiniowywanie
metod. Definicje w podklasie maskują definicje z klasy bazowej
(nadklasy) i stają się od danego punktu w hierarchii klas obowiązujące (w
przekrywanie metod
dół). Jeżeli przedefiniowana w podklasie metoda ma taką samą liczbę i
(maskowanie)
typ argumentów oraz typ zwracanych danych jak — w klasie bazowej,
przypisywanie właściwego kodu do wywołania ma miejsce w trakcie
uruchamiania programu na podstawie analizy typu obiektu, na rzecz
którego następuje wołanie metody.
Unified Modeling Language, zunifikowany język modelowania, język
UML formalny służący do modelowania różnego rodzaju systemów, np.
programu obiektowego i relacji zachodzących pomiędzy jego modułami.

You might also like