3. Obiektowość Javy III.

Interfejsy i klasy wewnętrzne

3.1. Interfejsy

Interfejs klasy – to sposób komunikowania się z jej obiektami.
 Inaczej - zestaw jej dostępnych do użycia z poziomu innej klasy metod


W Javie słowo interfejs ma też dodatkowe, specjalne "techniczne" znaczenie, odwołujące się zresztą do ogólnego pojęcia interfejsu.
 
Rozważmy przykład.
Nie wszystkie zwierzęta wydają głos. Zatem umieszczanie (abstrakcyjnej) metody getVoice() oraz metody speak()  w klasie Zwierz  nie jest "czystym rozwiązaniem".

Co więcej nie tylko zwierzęta mówią.
Chciałoby się więc mieć klasę obiektów wydających głos, którą móglby dziedziczyć np. Wodospad i Pies.

Ale Pies jest Zwierzem (dziedziczy Zwierza) i nie może odziedziczyć klasy obiektów "wydających głos". W Javie nie ma bowiem  wielodziedziczenia: każda klasa może dziedziczyć bezpośrednio tylko jedną klasę.

Pies jest Zwierzem (dziedziczy właściwości Zwierza), w Javie nie może dodatkowo odziedziczyć funkcjonalności klasy "obiektów wydających głos".
Ale byłoby to bardzo wskazane.

Pewne rozwiązanie tego problemu uzyskano w Javie wprowadzając (jako element skladni języka)  pojęcie interfejsu , jakby biedniejszej klasy, nie zawierającej pól, ale tylko metody (i/lub stałe statyczne). Uniknięto w ten sposób niejasności związanych z polami, zachowując - jak zobaczymy dalej - pewne zalety wielodziedziczenia.

Interfejs (deklarowany za pomocą słowa kluczowego interface) to:


Implementacja interfejsu w klasie - to zdefiniowanie w tej klasie wszystkich metod interfejsu.
To że klasa ma implementować interfejs X oznaczamy słowem kluczowym implements.



Np.  interfejs określający abstrakcyjną funkcjonalność "wydającego głos" mógłby wyglądać tak:
interface Speakable {

  int QUIET = 0;              // <- publiczne stałe statyczne
  int LOUD  = 1;              // domyślnie public static final

  String getVoice(int voice); // <- metoda abstrakcyjna;
                              // ponieważ w interfejsie mogą być
                              // tylko publiczne metody abstrakcyjne,
                              // specyfikatory public i abstract niepotrzebne
}
a jego implementacja w klasie Wodospad:

class Wodospad implements Speakable {

  public String getVoice(int voice) { // metody interfejsu są zawsze publiczne! 

    if (voice == LOUD) return "SZSZSZSZSZSZ....";
    else if (voice == QUIET) return "szszszszszsz....";
         else return "?"
  }
}
Klasa, w definicji której zaznaczono (za pomocą slowa implements), że ma implementowac interfejs musi zdefiniować wszystkie metody tego interfejsu, albo być deklarowana jako klasa abstrakcyjna.
W przeciwnym razie wystąpi błąd w kompilacji.

Podsumujmy:


Rozważmy teraz interfejs, opisujący obiekty zdolne się poruszać:

interface Moveable {
   void start();
   void stop();
}
Ta funkcjonalność dotyczy zarówno Psa, jak i  Samochodu (np. obiektów klasy Car), jak również innych pojazdów.

Weźmy Psa:
Mamy trzy właściwości.
Właściwość bycia Zwierzem zrealizujemy przez dziedziczenie, pozostałe dwie przez implementację interfejsów.

W Javie klasa (oprócz dziedziczenia innej klasy) może implementować dowolną liczbę interfejsów.


Możemy więc napisać:

class Pies extends Zwierz implements Speakable, Moveable {

  Pies() {}
  Pies(String s) { super(s); }
  String getTyp() { return "Pies"; }
  public String getVoice(int voice) {
    if (voice == LOUD) return "HAU... HAU... HAU... ";
    else return "hau... hau...";
  }
  public void start() { System.out.println("Pies " + name + " biegnie"); }
  public void stop()  { System.out.println("Pies " + name + " stanął"); }

}

i użyć np. tak :

     Pies kuba = new Pies("Kuba");
     kuba.start();
     System.out.println(kuba.getVoice(Speakable.LOUD));
     kuba.stop();
co da:
Pies Kuba biegnie
HAU... HAU... HAU...
Pies Kuba stanął




W tym fragmencie programu zmienna kuba jest typu Pies, a to znaczy, że jest również typu Zwierz ORAZ typu Speakable i Moveable, ponieważ:
  1. klasa Pies dziedziczy klasę Zwierz - zatem kuba jest typu Zwierz,
  2. klasa Pies implementuje interfejs Speakable - zatem kuba jest typu Speakable,
  3. klasa Pies implementuje interfejs Moveable - zatem kuba jest typu Moveable
Interfejsy - podobnie jak klasy - wyznaczają typy zmiennych



Możemy więc robić konwersje w górę do typu wyznaczanego przez implementowany interfejs, jak również używać operatora instanceof,  by stwierdzić czy obiekt jest obiektem klasy implementującej dany interfejs.

Dlatego warto wprowadzić subtelną zmianę w definicji omawianej wcześniej klasy Vehicle, dodając:

    class Vehicle implements Moveable {
    ...
    }

Wszystkie klasy pochodne wobec Vehicle – prawem dziedziczenia – także będą implementować ten interfejs.

Teraz np. jeśli mamy klasy Car, Rower, Pies i Kot implementujące interfejs Moveable
oraz metodę:
void wyscig(Moveable[] objects) {
  for (int i =0; i < objects.length; i++)   {
    objects[i].start();
    if (objects[i] instanceof Vehicle) System.out.println(""+objects[i]);
  }
}

to po wywołaniu:

    wyscig(new Moveable[]  { new Pies(...), new Car(...), new Kot(...), new Rower(...)} );

moglibyśmy  otrzymać np. taką informację:

Pies biegnie
Samochód nr rej WB4545 - JEDZIE
Kot sie skrada
Pojazd 4 ,właścicielem którego jest Janek  - JEDZIE


3.2. Adaptery

Adapter – to klasa implementująca metody interfejsu w taki sposób, że ich ciała są puste (nie wypełnione treścią).


Po co są adaptery ?
Dla wygody: kiedy w klasach musimy implementować jakiś interfejs z wieloma metodami, a interesują nas w tych klasach tylko definicje niewielu metod tego interfejsu (zawsze chcemy używać tylko jakiegoś małego podzbioru metod interfejsu) – to możemy stworzyć adapter, a następnie dziedziczyć go w naszych klasach, przedefiniowując za każdym razem tylko interesujące nas metody.

Pokazano to na poniższym schemacie.

r
Uwaga: ze względu na oszczędność miejsca na schemacie pominięto słowo kluczowe public przy definicjach metod w klasach. Należy jednak pamiętać, że w realncyh programach, przy implementacji metod intefejsu musi ono występowac obowiązkowo.

W standardowych pakietach Javy jest wiele rozbudowanych interfejsów i wiele (dostarczonych nam dla wygody) odpowiadających im adapterów (np w  standardowych pakietach obsługi zdarzeń).

Adaptery są szczególnie użyteczne, gdy wykorzystujemy je, dziedzicząc w anonimowych klasach wewnętrznych (o czym dalej).


3.3. Referencyjne konwersje zawężające i interfejsy

Jeśli mamy referencję do obiektu typu Zwierz na którą podstawiono odniesienie do obiektu typu Pies, to możemy zrobić konwersję "w dół" hiererchii dziedziczenia. 

    Pies p = new Pies();
    Zwierz z = p;
    Pies p1 = (Pies) z; // Konwersja z typu Zwierz do typu Pies

Mówiąc obrazowo (ale pamiętając o tym, że mamy do czynienia z konwersjami referencyjnymi i tak naprawdę jest tu tylko jeden obiekt, który po prostu, w kolejnych przekształceniach referencyjnych, traktujemy inaczej): 
Pies pochodzi od Zwierza, możemy więc z Psa uzyskać Zwierza, a później z tego Zwierza z powrotem Psa.

Referencyjne konwersje zawężające (zwane też obiektowymi konwersjami  "w dół") :


Na przykład w sytuacji:

    Zwierz z;
    Rower r = (Rower) z; 

JVM w trakcie wykonania programu wykryje błąd i zgłosi wyjątek ClassCastException,  bo klasa Rower nie dziedziczy klasy Zwierza.

Opisywany mechanizm konwersji zawężających w równym stopniu dotyczy interfejsów (interfejsy, tak samo jak klasy, też określają typy).
Rozważmy dwa krótkie przykłady.

Załóżmy, że klasa Pies ma jeszcze dodatkową własną metodę:
void merda() { System.out.println("Merda ogonem"); )
Można napisać metodę:

static void info(Zwierz z) {
   say(z.getTyp() + z.getName());   // say własna metoda =  System.out.println
   if (z instanceof Speakable) {
      Speakable zs = (Speakable) z;
      say(zs.getVoice(Speakable.LOUD));
      }
   if (z instanceof Pies)  ((Pies) z).merda();
}

Co wywołane dla obiektu klasy Kot (implementującej interfejs Speakable):

    Kot mruczek = new Kot("mruczek");
    info(mruczek)

może wypisać:
Kot Mruczek
Miauuu....


bo:
  1. Przy przekazywaniu argumentu następuje konwersja: Kot -> Zwierz,.
  2. Polimorficznie jest wołana metoda getTyp() (z jest Zwierz, ale Java wie, że w tym Zwierzu siedzi Kot).
  3. Ponieważ w z siedzi Kot, który implementuje interfejs Speakable, można zrobić konwersję do typu Speakable i odwołać się polimorficznie do getVoice().
  4. Ponieważ Kot nie jest Psem - nie merda. ogonem.
dla obiektu hipotettycznej klasy Ryba - info( new Ryba()). możemy dostać w wyniku tylko:

Ryba

bo wartość wyrażenia z instanceof Speakable jest false (Ryba nie implementuje interfejsu Speakable) i oczywiście nie jest też Psem.

natomiast  dla Psa kuby (kuba = new Pies("Kuba") po info(kuba) dostaniemy pewnie:
Pies Kuba
Hau... hau... hau...
Merda ogonem





I jeszcze jeden krótki przykład.
Mając metodę:
void run(Moveable m) {
   m.start();
   if (m instanceof Pies) {
      System.out.println(" ...  i  .... " );
      ((Pies) m).merda();
      }
  }
i wołając ją  z argumentem typu Pies, otrzymamy wynik:

Pies biegnie
... i ....
merda ogonem


bo uzyskaliśmy:
  1. Konwersję Psa do typu Moveable (w górę).
  2. Polimorficzne wywołanie metody start() na rzecz obiektu oznaczanego przez m (formalnego typu Moveable, który jednak faktycznie jest typu Pies).
  3. I jeżeli m odnosi się do Psa (a odnosi się), to po jawnej konwersji z typu Moveable do Psa możemy na rzecz przekształconego m wywołać "indywidualną" metodę merda z klasy Pies



3.4. Klasy wewnętrzne


Klasa wewnętrzna – to klasa zdefiniowana wewnątrz innej klasy.

class A {
....
     class B {
     ....
     }
....
}

Klasa B jest klasą wewnętrzną w klasie A.
Klasa A jest klasą otaczającą klasy B.



Po co są klasy wewnętrzne?  W jakim celu są używane?


Klasa wewnętrzna może:
  • być zadeklarowana ze specyfikatorem private (normalne klasy nie!), uniemożliwiając wszelki dostęp spoza klasy otaczającej,
  • odwoływać się do niestatycznych składowych klasy otaczającej (jeśli nie jest zadeklarowana ze specyfikatorem static),
  • być zadeklarowana ze specyfikatorem static (normalne klasy nie!), co powoduje, że z poziomu tej klasy nie można odwoływać się do składowych niestatycznych klasy otaczającej (takie klasy nazywają się zagnieżdżonymi, ich rola sprowadza się wyłacznie do porządkowania przestrzeni nazw i ew. lepszej strukturyzacji kodu)
  • mieć nazwę (klasa nazwana),
  • nie mieć nazwy (wewnętrzna klasa anonimowa),
  • być lokalna – zdefiniowana w bloku (metodzie lub innym bloku np. w bloku po instrukcji if),
  • odwoływać się do zmiennych lokalnych (o ile jest lokalna, a zmienne są deklarowane ze specyfikatorem final).



Uwaga.
Zawarcie klasy wewnętrznej w klasie otaczającej NIE OZNACZA, że obiekty klasy otaczającej zawierają elementy (pola) obiektów  klasy wewnętrznej.
Obiekt niestatycznej klasy wewnętrznej zawiera referencję do obiektu klasy otaczającej, co umożliwia odwoływanie się do jej wszystkich składowych.
Między obiektami statycznej klasy wewnętrznej a obiektami klasy otaczającej nie zachodzą żadne związki.



Rozważmy przykładowe zastosowanie klas wewnętrznych do ulepszenia znanej nam klasy Car,

Problem.
Jadąc, samochody zużywają paliwo. Zatem w klasie Car  należałoby dostarczyć mechanizmu symulującego zużycie paliwa i ew. tego skutki (zatrzymanie pojazdu). Mechanizm ten nie powinien być w żaden sposób dostępny z innych klas, powinien być dobrze zlokalizowany i odseparowany. Jednocześnie, musi odwoływać się do prywatnej zmienej klasy Car, obrazującej bieżącą ilość paliwa w baku (fuel).

Koncepcja rozwiązania:
prywatna klasa wewnętrzna.

Przyjęte założenie symulacji:
w każdej jednostce czasu jazdy (1 sek czasu programu) zużywany jest podana ilość  paliwa.

Dodatkowe szczegóły realizacyjne:
Do symulacji wykorzystamy klasę Timer z pakietu javax.swing. Uruchomiony (metodą start()) obiekt tej klasy z zadaną częstotliwością (pierwszy argument konstruktora klasy Timer) wywołuje metodę actionPerformed(...)  z klasy i na rzecz obiektu podanego jako drugi argument konstruktora. Klasa drugiego argumentu implementuje interfejs ActionListener i definiuje jego jedyną metodę void actionPerforemd(ActionEvent e).

Rozwiązanie.
import javax.swing.*;
import java.awt.event.*;

public class Car extends Vehicle  {

  private String nrRej;
  private int tankCapacity;
  private int fuel;

  // Klasa wewnętrzna. Prywatna - nie możemy jej użyć poza klasą Car
  // Dostarcza definicji metody actionPerformed(...), wywoływanej przez Timer

   private class FuelConsume implements ActionListener {

     public void actionPerformed(ActionEvent e)  {
       if (getState() != MOVING) fuelTimer.stop();  // nie zużywaj paliwa,
       else  {                                      // gdy nie jedziesz
         fuel -= 1;               // odwolanie do pryw. składowej klasy otaczajĄcej
         if (fuel == 0) stop();
       }
     }
   }

  // Timer będzie co sekundę wywoływać metodę actionPerformed(...)
  // z klasy obiektu podanego jako drugi argument konstruktora
  // ( obiekt klasy FuelConsume)
  // w rezultacie co sekunde czasu komputerowego bedzie zuzywany 1 l paliwa

    private Timer fuelTimer = new Timer(1000, new FuelConsume());

    public Car(String nr, Person owner, int w, int h, int l,
               int weight, int tankCap)  {
        super(owner, w, h, l, weight);
        nrRej = nr;
        tankCapacity = tankCap;
    }

    public void fill(int amount)  {
      if (getState() == MOVING)
          System.out.println("Nie moge tankowac w ruchu");
      else  {
        fuel += amount;
        if (fuel > tankCapacity) fuel = tankCapacity;
      }
    }

    public void start()  {
      if (fuel > 0)   {
          super.start();
          fuelTimer.start();     // start Timera
      }
      else System.out.println("Brak benzyny");
    }

    public void stop()  {
      super.stop();
      fuelTimer.stop();          // zatrzymanie Timera
    }

    public String toString()  {
       return "Samochód nr rej " + nrRej + " - " + getState(getState());
    }
}

Oczywiście klasy wewnętrzne nie muszą być prywatne,
Wtedy możliwe jest odwoływanie się do nich spoza kontekstu klasy otaczającej.

Takie odwołanie ma formę:

      NazwaKlasyOtaczającej.NazwaKlasyWewnętrznej


Tworzenie obiektu niestatycznej klasy wewnętrznej wymaga zawsze istnienia obiektu klasy otaczającej. Mówi się, że obiekt klasy wewnętrznej opiera się na obiekcie klasy otaczającej .

Gdyby zatem nasza klasa FuelConsume była publiczna, to moglibyśmy spoza klasy Car odwoływać się do niej i tworzyć jej obiekty
r

Jeżeli niepotrzebna nam zmienna car, moglibyśmy zapisać to szybciej:

Car.FuelConsume cfc =    new Car().new FuelConsume();

3.5. Anonimowe klasy wewnętrzne.

Anonimowe klasy wewnętrzne nie mają nazwy.
Jeśli tak, to jakiego typu będą referencje do obiektów tych klas i po co taka możliwość?

Otóż, najczęściej tworzymy klasy wewnętrzne po to, by przedefiniować jakieś metody klasy dziedziczonej przez klasę wewnętrzną bądź zdefiniować metody implementowanego przez nią interfejsu na użytek jednego obiektu. Referencję do tego obiektu chcemy traktować jako typu klasy dziedziczonej lub typu implementowanego interfejsu. Nazwa klasy wewnętrznej jest więc nam niepotrzebna i nie chcemy jej wymyślać. Wtedy stosujemy anonimowe klasy wewnętrzne.

Definicję anonimowej klasy wewnętrznej dostarczamy w wyrażeniu new.

    new NazwaTypu( parametry ) {
         // pola i metody klasy wewnętrznej
    }

gdzie:


Np. jeśli mamy klasę DBase, zawierają zestaw metod działania na bazie danych (m.in metodę void add(Record  r), dodającą nowy rekord do bazy)  i chcemy w naszym programie utworzyć jeden obiekt tej klasy (operujemy na jednej bazie), a jednocześnie uzupełnić działanie metody add (w stosunku do jej definicji zawartej w klasie DBase), to możemy użyć anonimowej klasy wewnętrznej:

r


i wykorzystać utworzony obiekt anonimowej klasy wewnętrznej, na który wskazuje referencja db np.

Record jakisRekord;
//....
db.add(jakisRekord);

W przypadku zużycia paliwa w samochodzie (poprzedni przykład) również możemy (i powinniśmy) użyć anonimowej klasy wewnętrznej
Po co nam nazwa klasy - FuelConsume? Potrzebujemy tylko jednego obiektu tej klasy, przy czym interesuje nas jego funkcjonalność  jako ActionListenera. Tak naprawdę potrzebujemy więc jednego obiektu typu ActionListener, a ponieważ ActionListener jest interfejsem i musimy zdefiniować jego metodę actionPerformed(...), to powinniśmy zrobić to w anonimowej klasie wewnętrznej.

class Car  extends Vehicle {

 private ActionListener fuelConsumer  = new ActionListener()    {
       public void actionPerformed(ActionEvent e)  {
          if (getState() != MOVING) fuelTimer.stop();
          else  {
            fuel -= 1;
            System.out.println("Fuel "+ fuel);
            if (fuel == 0) stop();
          }
       }
    };

private Timer fuelTimer = new Timer(1000, fuelConsumer);
....
}


Możemy jeszcze bardziej uprościć sobie życie. Zauważmy, że wyrażenie new zwraca referencję do nowoutworzonego obeiktu. Wszędzie tam, gdzie może wystąpić referencja może wystąpić wyrażenie new. Może zatem wystąpić jako drugi argument wyrażenia new, tworzacego timer.

class Car  extends Vehicle {

private Timer fuelTimer = new Timer(1000,  new ActionListener() {

       public void actionPerformed(ActionEvent e)  {
          if (getState() != MOVING) fuelTimer.stop();
          else  {
            fuel -= 1;
           if (fuel == 0) stop();
          }
       }

    }   // nawias zamykający definicję klasy wewnętrznek
);      // nawias zamykający  new Timer(...),
        // i średnik kończący instrukcję deklaracyjną

...
}

Uwagi:
W dalszej części wykładu poznamy bardzo dużo zastosowań i przykładów użycia anonimowych klas wewnętrznych.

3.6. Wewnętrzne klasy lokalne

Klasy wewnętrzne (nazwane i anonimowe) mogą być definiowane w blokach lokalnych (np. w ciele metody). Będziemy je krótko nazywać klasami lokalnymi.


Ma to dwie zalety.

Wewnętrzne klasy lokalne są  doskonale odseparowane (nie ma do nich żadnego dostępu spoza bloku, w którym są zdefiniowane), a mogą odwoływać się do składowych klasy otaczającej oraz zmiennych lokalnych zadeklarowanych w bloku (pod warunkiem, że są one zadeklarowane ze specyfikatorem final, o czym dalej).

Poza tym możliwość definiowania wewnętrznych klas lokalnych umożliwia umieszczenie odpowiedniego kodu w miejscu jego wykorzystania.

Rozpatrzmy przykład metody wypisywania zawartości katalogu.

Obiekty plikowe (pliki i katalogi) są obiektami klasy File z pakietu java.io. Wobec katalogu można użyć metody list z klasy File, która zwraca tablicę nazw plików (i podkatalogów) w nim zawartych. Używając metody list z argumentem typu FilenameFilter możemy określić kryteria filtrowania
wyniku wg nazw (np. otrzymać tylko listę plików o rozszerzeniu .java).

FilenameFilter jest interfejsem, w którym zawarto jedną metodę boolean accept(File dir, String filename).
Musimy zatem mieć obiekt klasy implementującej FilenameFilter, w której to klasie zdefiniujemy metodę accept i podać referencję do tego obiektu jako argument metody list. Metoda accept będzie wtedy wywoływana dla każdego obiektu plikowego, zawartego w katalogu z argumentami – katalog, nazwa pliku lub podkatalogu.
Powinniśmy ją zdefiniować w taki sposób, by zwracała true tylko wtedy, gdy nazwa spełnia wymagane przez nas kryteria, a w przeciwnym razie false.

Naturalnym sposobem oprogramowania jest tu umieszczenie definicji anonimowej klasy wewnętrznej implementującej FilenameFilter w wyrażeniu new podanym jako argument metody list. A ponieważ listowanie umieszczamy w jakiejś metodzie, to ta anonimowa klasa będzie lokalną klasą wewnętrzną.

Np.

void listJavaFiles(String dirName)  {    // argument - nazwa katalogu

   File dir = new File(dirName);         // katalog jako obiekt typu File

   // listowanie z filtrowaniem nazw
   // kryteria wyboru nazw podajemy za pomocę
   // implementacji metody accept
   // w lokalnej anonimowej klasie

   String[] fnames = dir.list( new FilenameFilter()  {

         public boolean accept(File directory, String fname)  {
            return fname.endsWith(".java");

         }
   });

   for (int i=0; i < fnames.length; i++)  {  // lista -> stdout
      System.out.println(fnames[i]);
   }

}

Jednak gdybyśmy chcieli określić rozszerzenie listowanych plików w jakiejś zmiennej lokalnej metody  (np. ext), to tę zmienną lokalną musielibyśmy zadeklarowac ze specyfikatorem final.


r



Przypomnijmy, że słowo kluczowe final oznacza, że wartość zmiennej może być ustalona tylko raz i nie może potem ulegać zmianom.

Dlaczego takie wymaganie przy lokalnych klasach wewnętrznych?

Zauważmy: obiekt klasy wewnętrznej jest odrębnym bytem. Ma dostęp do pól klasy otaczającej (elementów obiektu, na którym się opiera), ale tylko dlatego, że wewnątrz zawiera referencję do tego obiektu. Przy tworzeniu obiektu klasy wewnętrznej ta referencja jest zapisywana w "jego środku".

Jedynym sposobem by zapewnić dostęp do zmiennych lokalnych jest – analogicznie – skopiowanie ich wartości "do środka" obiektu klasy wewnętrznej.

Gdyby więc wartości zmiennych lokalnych, do których odwołuje się klasa wewnętrzna mogły się zmieniać, to mogłaby powstać niespójność pomiędzy kopią i oryginałem.
Dlatego zmiany są zabronione i  konieczny jest specyfikator final.

Należy podkreślić, że parametry metody są również zmiennymi lokalnymi i wobec nich stosuje się tę samą regułę.

// Metoda listuje pliki z rozszerzeniem podanym jako drugi argument,
// z podanego jako  pierwszy argument katalogu

void listFilesWithExt(String dirName, final String ext )  {
      File dir = new File(dirName);
      String[] fnames = dir.list( new FilenameFilter()  {
         public boolean accept(File dir, String fname)  {
            return fname.endsWith(ext);
         }
      });
      for (int i=0; i < fnames.length; i++)  {
          System.out.println(fnames[i]);
      }
   }


Zzmienne lokalne używane w anonimowych klasach wewnętrznych muszą być deklarowane jako final