Personal tools

PO Klasy i kapsułkowanie

From Studia Informatyczne

<<< Powrót

Spis treści

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 słowa class (poprzedzonego być może pewnymi modyfikatorami), po którym znajduje się nazwa klasy[1] 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[2]. 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[3]. 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 (imię).

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)[4]. 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

Modyfikatory dostępu do składowych klas

Deklarując składowe klasy (przez składową rozumiemy tu atrybut, metodę, konstruktor, klasę lub interfejs) można poprzedzać je jednym z modyfikatorów dostępu. Modyfikator określa, gdzie będzie można używać danej składowej klasy. Dostęp do składowej jest oczywiście możliwy tylko wtedy, gdy dostępny jest typ, do którego składowej chcemy się dostać. Ponadto składowa musi mieć odpowiednie modyfikatory dostępu. Oto lista modyfikatorów występujących w Javie:

  • public (dostęp publiczny) umożliwia dostęp bez ograniczeń.
  • protected (dostęp chroniony) umożliwia dostęp z pakietu, w którym jest zdefiniowana klasa zawierająca składową i z jej podklas, nawet jeśli są zdefiniowane w innym pakiecie[5].
  • private (dostęp prywatny) umożliwia dostęp z klasy zadeklarowanej na najwyższym poziomie struktury programu, w której jest zawarta (nie koniecznie bezpośrednio) deklaracja opatrzona tym modyfikatorem dostępu.
  • brak modyfikatora (dostęp domyślny) umożliwia dostęp z pakietu, w którym jest zawarta rozważana deklaracja.

Oczywiście dla jednej deklaracji można podać tylko jeden modyfikator dostępu.

Ogólnie zasady stosowania modyfikatorów dostępu są dość intuicyjne ale zwróćmy uwagę na pewne niuanse:

  • dostęp chroniony zezwala na dostęp nie tylko w podklasach, ale też w całym pakiecie,
  • dostęp prywatny umożliwia dostęp w całej klasie zadeklarowanej na najwyższym poziomie, zatem w poniższej sytuacji:
class Zewnętrzna{
  class Lokalna1{
    private int i1;
  }
  class Lokalna2{
    int met(){return new Lokalna1().i1;}
  } 
}

metoda z klasy Lokalna2 ma prawo odwołać się do prywatnej składowej obiektu z klasy Lokalna1, co jest mało intuicyjne.

  • prawa dostępu dotyczą klas, a nie obiektów.

Jest niezwykle istotne, by definiując klasę starannie przemyśleć, co obiekty klasy mają chronić, a co udostępniać na zewnątrz. Jeśli deklarujemy atrybuty, to praktycznie zawsze powinniśmy zadeklarować je jako prywatne lub chronione. Metody zwykle deklarujemy jako publiczne, ale zdarzają się też metody, które powinny być używane tylko w obrębie danej klasy lub hierarchii, wtedy definiujemy je jako prywatne lub chronione.

Modyfikatory dostępu do pozostałych konstrukcji Javy

Modyfikatorami dostępu możemy dekorować także inne niż składowe konstrukcje w Javie, oto podsumowanie ich użycia w tym kontekście:

  • Pakiet jest zawsze dostępny.
  • Klasa lub interfejs zadeklarowany na najwyższym poziomie zagnieżdżenia mogą być poprzedzone jedynie modyfikatorem public (lub żadnym). Jeśli modyfikator public występuje, to klasa (interfejs) jest dostępna wszędzie, gdzie zawierająca je jednostka kompilacji jest widoczna. Jeśli tego modyfikatora nie ma, to klasa (interfejs) jest widoczna tylko w swoim pakiecie.
  • Typ tablicowy jest dostępny wtedy i tylko wtedy, gdy dostępny jest typ elementów tablicy.

Przypisy

  1. 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.
  2. Można też zadeklarować lokalne klasy lub interfejsy, można też zadeklarować blok inicjujący.
  3. 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
  4. 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.
  5. Pełna definicja znaczenia protected jest dość subtelna, więc ją tu pomijamy. W większości przypadków wystarcza podana tu, nieco uproszczona wersja.