Dopiski
Dalsze uwagi o widoczności, klasach i dziedziczeniu
Już wiemy jak wyglądają klasy, wiemy też jaką mają strukturę. Przyjrzymy się teraz nieco dokładniej ich budowie, głębiej wnikając w rozwiązania przyjęte w Javie. Ale nie zapominajmy, że o najważniejszym - o przeznaczeniu klas. Są one narzędziem do wyrażania abstrakcji pojęć występujących w implementowanym (opisywanym) systemie. Udostępniane przez klasę metody stanowią jej interfejs<ref>Słowo interfejs zostało tu użyte w szeroko rozumianym sensie informatycznym, a nie jako jeden z elemnentów składni Javy.</ref> oferowany reszcie tworzonego systemu. Mówimy o kontrakcie pomiędzy klasą a resztą systemu. Omawiane tu narzędzia pozwalają lepiej ów kontrakt wyrażać i realizować, ale nie zmieniają w zasadniczy sposób naszego rozumienia pojęcia klasy.
Deklarowanie klas
Deklarując klasę możemy podać trzy rodzaje jej składników:
- pola
- metody
- zagnieżdżone klasy i interfejsy
Deklarację klasy możemy poprzedzić tzw. modyfikatorami. Kolejność podawania modyfikatorów jest bez znaczenia (aczkolwiek dla czytelności zaleca się kolejność podaną poniżej). Oto lista możliwych modyfikatorów:
- adnotacje
- modyfikatory dostępu
- abstract
- final
- strictfp
Modyfikatory dostępu:
W przypadku klas deklarowanych na najwyższym poziomie struktury programu (na poziomie pakietu) mamy tylko dwie możliwości:
- możemy nic nie napisać (co oznacza widoczność w całym pakiecie)
- możemy podać słowo kluczowe public co oznacza widoczność w całym programie.
W jednym pliku (ale nie w jednym pakiecie) można umieścić tylko jedną klasę z modyfikatorem dostępu public, należy też nadać mu nazwę taką jak nazwa klasy (z rozszerzeniem .java). Klas bez jawnego modyfikatora dostępu można umieścić w pliku dowolnie dużo, można też w pliku nie mieć żadnej klasy z modyfikatorem public.
Modyfikator abstract
ten modyfikator oznacza, że definiujemy klasę abstrakcyjną (to jest taką, której obiektów nie będziemy tworzyć). Zwykle oznacza to też, że w naszej klasie pojawią się metody abstrakcyjne (ale nie jest to wymagane). Nie można tego modyfikatora stosować razem z modyfikatorem final.
Modyfikator final
ten modyfikator oznacza, że definiujemy klasę końcową, to jest taką, po której nie będziemy już dziedziczyć. Taka deklaracja ma podówjne znaczenie. Po pierwsze pozwala nam zdecydować, że nasza klasa nie będzie dalej rozwijana. Pozwala to na zastoswanie w niej rozwiązań, które nie byłyby bezpieczne w przypadku np. przedefiniowania metod w podklasach. Po drugie, umozliwia to kompilatorowi na przeprowadzenie optymalizacji kodu, takich jak np. bezpośrednie wywołanie metod (ponieważ klasa jest końcowa, to jej metody nie będą już przedefiniowane, więc w tej klasie jesteśmy w stanie wskazać, które konkretnie metody z naszej klasy wywołujemy. Nie można tego modyfikatora stosować razem z modyfikatorem abstract.
Adnotacje omówimy osobno, zaś strictfp oznacza, że w całej klasie stosuje się wyliczanie ściśle zgodne z regułami podanymi dla typów float i double, niezależnie od tego jaka arytmetyka zmiennopozycjna jest dostępna w procesorze wykonującym obliczenia. Gwarantuje to, że na każdej maszynie wirtualnej Javy obliczneia numeryczne dadzą dokłądnie te same wyniki. Jeśli tak bardzi nam na tym nie zależy, a chcemy wykorzystać w większym stopniu mozliwości sprzętu, to możemy skorzystać z (domyślnego) trybu obliczeń numerycznych, co może pozwolić uniknąć np. niedomiarów podczas wyliczania wyrażeń z wartościami zmiennopozycyjnymi. Podkreślmy dwie rzeczy:
- maszyna wirtualna może, ale nie musi, działać inaczej w trybie ścisłego i rozluźnionego wyliczania wartości zmiennopozycyjnych,
- ten tryb obliczeń dotyczy sposobu wyliczania wartości wyrażeń, a nie sposou przechowywania wartości w zmiennych.
Modyfikatory klas nie dziedziczą się.
Deklarowanie pól
Dekalracja pola klasy składa się z następujących elementów:
- modyfikatory (opcjonalne),
- typ,
- nazwa pola,
- inicjalizacja.
Modyfikatory pól można podawac w dowolnej kolejności (choć zalecana jest kolejnośc podana poniżej). Oto lista możliwych modyfikatorów:
- adnotacje,
- modyfikatory dostępu,
- static,
- final (nie może wystąpić razem z volatile),
- transient (przemijający),
- volatile (nieprzewidywalny, nie może wystąpić razem z final).
Adnotacje tu pomijamy, modyfikator transient jest związany z serializacją obiektów (oznacza pole, których serializacja nie dotyczy), modyfikator volatile jest związany z wątkami i współbieżnością.
Modyfikatory dostępu
Już je wcześniej omawialiśmy, dla przypomnienia tylko je wyliczymy:
- private (pamiętajmy, że dotyczy klas, a nie obiektów),
- (nic czyli dostęp na poziomie pakietu),
- protected (pamiętajmy, że w Javie obejmuje to także cały pakiet),
- public.
Modyfikator final
Modyfikator static
Inicjalizacja pól
Jeśli nie podamy jawnej inicjalizacji pola, to domyślnie będzie przypisana mu wartość odpowiedniego typu odpowiadająca wartości zero (np. false dla typu boolean, a null dla typów referencyjnych). To oznacza, że w Javie nie występują pola niezainicjowane w tradycyjnym tego zwrotu znaczeniu. Jest to bardzo ważne z punktu widzenia semantyki języka, bez domyślnej (lub wymuszanej) inicjalizacji praktycznie nie dałoby się wprowadzić automatycznego odśmiecania.
Wartość przypisywana w inicjalizacji jest zadawana wyrażeniem. Tu niestety zaczyna się pojawiać wiele szczególnych sytuacji, które (co najmniej) mogą budzić wątpliwości, przypatrzmy się im więc dokładniej.
Co mogłaby znaczyć następująca deklaracja:
class A{ int i = i + 1; // ? }
Oczywiście nie można deklarować kliku pól o tej samej nazwie, nawet jak są polami róznych typów i rodzajów (egzemplarzowe i klasowe):
class A{ static int i; float i; // Błąd kompilacji }
zatem poprzedni przykład nie może się odnosić do innej zmiennej z tej samej klasy. Ale co wtedy, gdy zmienna o tej samej nazwie istnieje w nadklasie?
class A{ int i = 13; }
class B extends A{ int i = i + 1; // ? }
Też się nie kompiluje, gdybysmy chcieli umożliwić kompilację, trzeba by się posłużyć słowem super:
class A{ int i = 13; }
class B extends A{ int i = super.i + 1; // ? }
Zatem kompilator chroni nas, przed odwołaniem się w inicjatorze do właśnie inicjowanej zawartości.
A co z wartością innej zmiennej z tej samej klasy? No cóż, to trochę zależy, od tego jak zapiszemy owe inicjalizacje.
class A { int j; int i = j; }
Powyższe jest poprawne, ale po zamianie kolejności wierszy już nie.
class A { int i = j; // Odwołanie się w przód int j; }
Kompilator stosuje dośc prostą i naturalną regułę, można się odwoływać tylko do wartości zmiennych, których dekalracje znajdują się dalej w tekście klasy. Ale taka reguła była zbyt prosta, rozważmy poniższe:
class A { int i = j; // Odwołanie się w przód, ale do zmiennej statycznej static int j; }
Tymrazem kompilator nie protestuje, czemu? Bo zmienne statyczne są inicjowane
Przypisy
<references/>