Dopiski: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Janusz (dyskusja | edycje)
Janusz (dyskusja | edycje)
Linia 274: Linia 274:
Można też definiować bloki inicjalizujące wykonujące się przy tworzeniu klasy (a nie obiektu).
Można też definiować bloki inicjalizujące wykonujące się przy tworzeniu klasy (a nie obiektu).
Czyli dokonujące inicjalizacji na rzecz całej klasy. Ich deklaracja wygląda tak samo jak  
Czyli dokonujące inicjalizacji na rzecz całej klasy. Ich deklaracja wygląda tak samo jak  
w przypadku zwykłych (egzemplarzowych) bloków inicjalizujących, tyle że cały blok poprzedza słowo '''static'''. Oczywiście nie ma w nim dostępu do zmiennych egzemplarzowych.
w przypadku zwykłych (egzemplarzowych) bloków inicjalizujących, tyle że cały blok poprzedza słowo '''static'''. Oczywiście nie ma w nim dostępu do zmiennych egzemplarzowych. W tych blokach nie  można także zgłaszać sprawdzalnych wyjątków (bo nie ma gdzie ich przechwytywać).


== Przypisy ==
== Przypisy ==


<references/>
<references/>

Wersja z 00:49, 10 kwi 2008

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

Deklaracja 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
Oznaczają pola, których wartości nie mogą sie już zmieniać po inicjalizacji.


Modyfikator static
Oznacza pola (zmienne) klasowe. Zmienna klasowa jest zawsze dokładnie jedna, niezależnie od liczby utworzonych egzemplarzy klasy. Do zmiennych klasowych można się odwołać zarówno poprzez obiekt, jak i za pomocą specjalnej składni: nazwa_klasy.nazwa_pola. Zauważmy, że tego drugiego sposobu nie można stosować w stosunku do zmiennych egzemplarzowych (np. do odwołania się do zmiennej z nadklasy).

class A{
 static int i;
}

// ...
System.out.println("" + new A().i + ", " + A.i);

Zmienne klasowe tworzone są w momencie ładowania klasy, wtedy są też inicjowane.

Przykładem statycznego pola jest zmienna out z klasy java.lang.System.

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, gdybyśmy 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; // OK
}

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 w innym momencie wykonywania programu niż zmienne zmienne egzemplarzowe, więc powyższy zapis nie grozi powstaniem pętli przy inicjalizacji. No coż, w takim razie naturalnym jest pytanie o poniższe:

class A{
 static int i = j;  // Odwołanie się w przód
 static int j;
}

Tym razem znów kompilator odrzuca naszą deklarację (i słusznie). W zasadzie reguła określająca co może się pojawić w inicjalizatorze jest naturalna, ale czasami, jej efekty są dośc zaskakujące. Czy poniższe dekalracje są poprawne?

class A{
 int i; 
}
class B extends A{
  int k = i;
  // ...
}

Nie wiadomo, zależy od tego, co wpiszemy w miejsce komentarza. Jeśli zostawimy komentarz, to ten przykład się skompiluje, ale jeśli odpowiednio zmienimy komentarz, tak jak poniżej:

class A{
  int i; 
}
class B extends A{
 int k = i;
 int i;
}

to już kompilacja się nie powiedzie, a co ciekawsze, błędna będzie deklaracja pola k (choć jeszcze przed zamianą komentarza nie budziła wątpliwości kompilatora). Wyjaśnienie już znamy, tym nie mniej fakt, że dopisanie inych deklaracji może zmienić poprawność deklaracji wcześniejszych nie jest oczywisty.

No dobrze, a co z metodami, czy je można wywoływać w wyrażeniach inicjalizujących? To zależy:

class A{
 int i = f(13);
 int f(int k) // ...
   if (k % 2 == 0)
    // ...
   return 0;
 }
 // ...
}

Powyższe się kompiluje, ale nauczeni poprzednimi doświadczeniami przyjrzyjmy się, co można by wspisac zamias komentarzy z kropkami:

class A{
 int i = f(13);
 int f(int k) throws E{ 
  if (k % 2 == 0)
   throw new E();
  return 3;
 }

 class E extends Exception{
 }
}

Tym razem inicjalizacja się nie skompiluje, bo nie mamy jak przechwycić wyjątku.

No to juz chyba wszytko wiemy o inicjalizowaniu pól i wszystko jest jasne. To na koniec zastanówmy się nad poniższym przykładem, czy się skompiluje? A jeśli tak to co będzie wartościami zmiennych k oraz i?

class A{
 static int i = new A7().f()+1;
 final int k = i + 1;

 int f(){ 
   return k;
 }
}

Bloki inicjalizujące

Inicjalizacja pól może dokonywać się także w tzw. blokach inicjalizujących, czyli zestawach instrukcji umieszczonych bezpośrednio w treści klasy i ujętych w nawiasy klamrowe. Zawarte w nich instrukcje są wykonywane w momencie rozpoczynania wykonywania dowolnego z konstruktorów (można myśleć o tych blokach jako o wklejanych na początek treści każdego konstruktora - choć oczywiście nie oznacza to, że blok wykonuje się w zakresie widoczności wnętrza konstruktora (w bloku nie można się odwołać do parametrów konstruktora). Użycie w konstruktorach zapisu this(....) oczywiście nie zmieni faktu, że na każde utworzenie obiektu blok inicjalizujacy wykonywany jest tylko jeden raz.

Jeśli blok inicjalizujący zgłasza wyjątek, to musi on byc zadeklarowany w klauzuli throws każdego konstruktora.

Bloki inicjalizujące mogą zawierać dowolne instrukcje (np. pętle). Można zdefiniować wiele bloków inicjalizujących (ale nie należy tego robić, ze względu na czytelność).

Przykład użycia bloku inicjalizującego:

 class A { 
   final static int n = 10;
   int tab[] = new int[n];
   { 
     // Blok inicjalizujący tablicę, nie muszę tej inicjalizacji wpisywać 
     // do poszczególnych konstruktorów. Choć mógłbym zdefiniować metodę.	
     for(int i=0; i<n; i++)
       tab[i] = i;
     System.out.println("Initialization"); // Wołanie metod też jest dozwolone	 
   }
   A() { 
     //...
   }
   A(int i) { 
     //...
   }
 }

Bloki inicjalizujące są przydatne z dwu powodów:

  • w klasach anonimowych, bo tam nie da się zdefiniować konstruktora. (Dlaczego się nie da? A jaka by była jego nazwa?)
  • instrukcje w bloku inicjalizującym mogą inicjować zmienne końcowe (final), czego nie można zrobić w treści metody.

super i this w konstruktorach

Pierwsza instrukcja konstruktora może mieć postać słowa super lub this po którym następuje w nawiasach lista parametrów, co oznacza wywołanie konstruktora, odpowiednio, z bezpośredniej nadklasy lub z tej samej klasy. Oczywiście w tym drugim przypadku nie można podać zacyklonych odwołań konstruktorów tej samej klasy.

Często spotykane konstruktory

Konstruktor bezargumentowy

Jest generowany automatycznie, o ile nie podano żadnego innego.

Konstruktor kopiujący

Uwaga: w przeciwieństwie do C++, w Javie ten konstruktor nie ma dużego znaczenia. Ponadto często stosuje się tu zamiast niego metodę klonującą.

Konstruktor prywatny

Czasami stosowana technika (np. dla realizacji wzorca Singleton).

Podsumowanie

Istnieje wiele sposobów inicjowania pól w Javie. Wybieramy najbardziej nam pasujące. Kolejność inicjalizacji:

  • konstruktor z nadklasy,
  • inicjalizacja pól zapisana w ich deklaracjach,
  • inicjalizacja wyrażona blokiem (blokami) inicjalizującymi,
  • instrukcje zawarte w konstruktorze.

Klasowe bloki inicjalizujące

Można też definiować bloki inicjalizujące wykonujące się przy tworzeniu klasy (a nie obiektu). Czyli dokonujące inicjalizacji na rzecz całej klasy. Ich deklaracja wygląda tak samo jak w przypadku zwykłych (egzemplarzowych) bloków inicjalizujących, tyle że cały blok poprzedza słowo static. Oczywiście nie ma w nim dostępu do zmiennych egzemplarzowych. W tych blokach nie można także zgłaszać sprawdzalnych wyjątków (bo nie ma gdzie ich przechwytywać).

Przypisy

<references/>