PO Klasy i kapsułkowanie
Wprowadzenie
Poznaliśmy dotąd tę cześć Javy, która praktycznie nie korzysta z możliwości oferowanych przez programowanie obiektowe. Na tym wykładzie zajmiemy się definiowaniem klas. Zobaczymy jak pozwalają one strukturalizować tworzone programy oraz ochronić dane zawarte w obiektach.
Definiowanie klas
Definicja klasy wprowadza do programu nowy typ, a co bardziej istotne, daje programiście do dyspozycji nowe pojęcie, którego może używać przy budowanie swojej aplikacji. W Javie można definiować klasy na najwyższym poziomie zagnieżdżeń struktury programu, można też deklarować klasy zagnieżdżone, czyli takie, których definicja jest zawarta w innej klasie lub interfejsie (mogą to być klasy składowe, lokalne lub anonimowe). W tym wykładzie będziemy się zajmować tylko klasami niezagnieżdżonymi, aczkolwiek zdecydowana większość zawartych tu informacji odnosi się do wszystkich rodzajów klas. Nie będziemy też zajmować się tu definiowaniem wyliczeń, które z punktu widzenia składni Javy również są klasami.
Składnia deklaracji
Deklaracja klasy może przyjąć w najprostszej postaci następującą postać
class Pusta{ }
Taka postać oczywiście nie jest zbyt użyteczna. Jak widać deklaracja klasy zaczyna się od slowa class (poprzedzonego być może pewnymi modyfikatorami), po którym znajduje się nazwa klasy<ref>Po nazwie klasy mogę się pojawić informacje o parametrach tej klasy dotyczących typów oraz informacje o nadklasie lub implementowanych interfejsach - w tym wykładzie te informacje pomijamy, zajmiemy się nimi w dalszych wykładach.</ref> oraz treść klasy ujęta w nawiasy klamrowe.
Ponieważ zwykle chcemy, by klasy definiowane na najwyższym poziomie struktury programu były dostępne w całym programie, zwykle przed deklaracją takiej klasy umieszczamy modyfikator public.
W treści klasy umieszcza się deklaracje składowych klasy, to znaczy atrybutów, metod i konstruktorów.<ref>Można też zadeklarować lokalne klasy lub interfejsy, można też zadeklarować blok inicjujący.</ref>. Te deklaracje mogą być poprzedzone modyfikatorami.
Deklaracja atrybutu składa się z (podanych w takiej właśnie kolejności) typu, nazwy i nieobowiązkowej części inicjującej. Deklaracja atrybutu kończy się średnikiem. Na przykład definicja klasy przechowującej imię i nazwisko osoby może wyglądać tak:
class Osoba{ String imię; String nazwisko; }
Deklaracja metody zaczyna się od podania typu wyniku (lub słowa kluczowego void), identyfikatora metody, następnie otoczonej okrągłymi nawiasami listy parametrów formalnych (być może pustej) oraz treści metody.<ref>Po liście parametrów może się pojawić lista wyjątków zgłaszanych przez deklarowaną metodę, ale to zagadnienie omawiamy w treści innego wykładu</ref>. Treść metody to po prostu instrukcja bloku. Załóżmy, że chcemy dodać do naszej klasy metody do odczytywania imienia i nazwiska:
class Osoba{ String imię; String nazwisko; String imię(){ return imię; } String nazwisko(){ return nazwisko; } }
Reguły składni Javy ogólnie zabraniają deklarowania dwu rzeczy w tym samym zasięgu z ta samą nazwą, ale zadeklarowanie metody i atrybutu o tej samej nazwie nie prowadzi do żadnych niejednoznaczności (przy wywołaniu metody zawsze trzeba podać po jej nazwie otoczoną nawiasami listę parametrów).
Konstruktor jest specjalnym rodzajem metody. Służy do tworzenia nowych obiektów klasy, w której jest zadeklarowany. Jego nazwa musi być taka sama jak nazwa klasy, w której jest zadeklarowany (po tym kompilator poznaje, że ma do czynienia z konstruktorem, a nie zwykłą metodą). Dla konstruktora nie podaje się typu wyniku. Liczba parametrów konstruktora może być dowolna. Definicja języka gwarantuje, że nie da się utworzyć żadnego obiektu, bez jednoczesnego wywołania konstruktora. Jest to niezwykle cenne, bo oznacza, że mamy gwarancję, że każdy utworzony obiekt zostanie zainicjowany w określony przez nas sposób. To jak ma wyglądać inicjalizacja obiektu opisujemy w treści konstruktora, zwykle są to przypisania wartości do poszczególnych atrybutów, można też wywoływać metody (ogólne reguły dotyczące treści konstruktora są takie same, jak dla zwykłych metod, pewne niuanse pojawiają się dopiero przy konstruktorach dla klas dziedziczących po innych klasach, ale tym zajmiemy się w następnym wykładzie). Jeśli sami nie zdefiniujemy konstruktora, tak jak to ma miejsce w obecnej definicji klasy Osoba, to kompilator sam wygeneruje bezargumentowy konstruktor domyślny. Dzieje się tak tylko wtedy, jeśli autor klasy nie zdefiniuje żadnego własnego konstruktora. Oczywiście taki automatycznie wygenerowany konstruktor nie jest w stanie dokonać żadnych inicjalizacji wynikających z semantyki definiowanej klasy i bardzo rzadko zdarza się sytuacja, gdy klasa nie ma jawnie zdefiniowanego konstruktora. Również w naszej klasie konstruktor jest konieczny. Jak pamiętamy z poprzednich wykładów domyślną wartością niezainicjowanych atrybutów referencyjnych jest null. To oczywiście dość kiepski pomysł, by dozwolić, żeby imię czasem nie było napisem (należałoby wtedy przy każdej próbie dostępu do imienia sprawdzać, czy nie ma ono wartości null). Poza tym pojęcie Osoby (definiowane omawianą klasą) nie ma sensu, gdy nie wiemy, o którą osobę chodzi. Z tego powodu dodajemy do naszej klasy konstruktor tworzący osobę o zadanym imieniu i nazwisku.
class Osoba{ String imię; String nazwisko; Osoba(String imię, String nazwisko){ this.imię = imię; this.nazwisko = nazwisko; } String imię(){ return imię; } String nazwisko(){ return nazwisko; } }
W treści konstruktora użyliśmy słowa kluczowego this. Może ono występować nie tylko w konstruktorach, ale w dowolnych metodach (poza klasowymi). Oznacza ono obiekt, na rzecz którego wykonywana jest metoda (w przypadku konstruktora obiekt, który jest tworzony za pomocą tego konstruktora). Dzięki użyciu this możemy łatwo wskazać, czy chodzi nam o zmienną obiektową (this.imię) czy o parametr metody (this).
Zwróćmy uwagę, że teraz próba utworzenia osoby bez podania jej imienia i nazwiska już się nie powiedzie (i bardzo dobrze, że nie!):
Osoba o = new Osoba(); // Po dodaniu konstruktora powoduje błąd kompilacji Osoba o = new Osoba("Jan","Kowalski"); // Poprawne
Kapsułkowanie
Dotąd definiowaliśmy poszczególne składowe klasy nie troszcząc się o to, kto będzie miał do nich dostęp. To duża niekonsekwencja. Dopiero co cieszyliśmy się z tego, że dzięki zdefiniowaniu konstruktora zapobiegliśmy powstawaniu bezsensownych obiektów klasy Osoba (czyli bez imienia i nazwiska)<ref>Oczywiście można podać puste napisy jako imię i nazwisko, ale nie wnikamy już w to czy podane imię lub nazwisko jest sensowne, dbamy natomiast o to, by na pewno było podane.</ref>. Spójrzmy co nam obecnie grozi:
Osoba o = new Osoba("Jan","Kowalski"); // ... o.imię = null; o.nazwisko = null;
Na pewno nie chcielibyśmy dopuścić do takiej sytuacji. To czego nam brakuje, to jakaś postać mechanizmu obronnego dla danych przechowywanych w obiektach. Chcemy, żeby obiekty były odpowiedzialne za przechowywane w nich dane (za ich poprawność i spójność). Chcemy, aby obiekty były hermetycznymi kapsułkami, do których zawartości można uzyskać dostęp tylko w sposób kontrolowany, to znaczy za pomocą metod obiektu. Tak rozumiane chronienie danych w programowaniu obiektowym nazywa się kapsułkowaniem lub hermetyzacją. Kapsułkowanie jest konieczne dla prawidłowego konstruowania programów obiektowych. Możemy je osiągnąć w Javie stosując modyfikatory dostępu.
Modyfikatory dostępu
Przypisy
<references/>