Polimorfizm, interfejsy i klasy wewnętrzne



Materiał przedstawia przede wszystkim  istotne w programowaniu obiektowym pojęcia polimorfizmu, klas abstrakcyjnych, interfejsów oraz  klas wewnętrznych. Dyskusja ilustrowana będzie praktycznymi przykładami użyteczności tych koncepcji.

1. Metody wirtualne i polimorfizm

W klasie Car z poprzedniego wykładu przedefiniowaliśmy  metodę start() z klasy Vehicle (dla samochodów sprawdza ona czy jest paliwo by ruszyć, nie robi tego dla pojazdów "w ogóle"). Przedefiniowaliśmy też metodę toString() (dla obiektów klasy Car zwraca ona inne napisy niż dla ogólniejszych obiektów klasy Vehicle).

Jeżeli teraz:

Car c = new Car(...); // utworzymy nowy obiekt klasy Car
Vehicle v = c;        // dokonamy obiektowej konwersji rozszerzającej
to  jaki będzie wynik użycia metod start() i toString() wobec obiektu oznaczanego v:
v.start();
System.out.println(v.toString());

Czy zostaną wywołane metody z klasy Vehicle (formalnie metody te są wywoływane na rzecz obiektu klasy Vehicle) czy z klasy Car (referencja v formalnego typu "referencja na obiekt Vehicle" faktycznie wskazuje na obiekt klasy Car) ?

Rozważmy przykład "z życia" zapisany w programie, a mianowicie schematyczną symulację wyścigu pojazdów.
Uczestnicy: rowery (obiekty klasy Rower),  samochody (obiekty klasy Car), rydwany (obiekty klasy Rydwan).
Wszystkie klasy są pochodne od Vehicle.
Każda z tych klas inaczej przedefiniowuje metodę start() z klasy Vehicle (np. Rower może w ogóle jej nie przedefiniowywać, Car – tak jak w poprzednich przykładach, Rydwan – w jakiś inny sposób).
Sygnał do startu wszystkich pojazdów daje starter.
W programie moglibyśmy to symulować poprzez:
przykładowo:

Vehicle[] allveh = getAllVehiclesToStart();
for (Vehicle v : allveh) v.start();

Jeżeli nasz program ma odwzorowywać rzeczywistą sytuację wyścigu (sygnał startera, po którym wszystkie pojazdy – jeśli mogą – ruszają), to oczywiste jest, że – mimo, iż v jest formalnego typu Vehicle – powinny być wywołane metody start() z każdej z odpowiednich podklas klasy Vehicle (właściwa metoda start() dla danego rodzaju pojazdu).

I tak jest rzeczywiście w Javie.

r

Ale jak to jest możliwe?
Z punktu widzenia.łączenia przez kompilator odwołań do metody (np. start()) oraz jej definicji (wykonywalnego kodu) sytuacja jest następująca:
  1. kompilator wie tylko, że start() jest komunikatem do obiektu typu Vehicle,
  2. powinien więc związać odwołanie v.start() z definicją metody start() z klasy Vehicle
Jakże inaczej? Przecież wartość v może zależeć od jakichś warunków występujących w trakcie wykonania programu (nieznanych kompilatorowi).

Np. mając dwie klasy dziedziczące klasę Vehicle, Car i Rydwan, możemy napisać:
public static void main(String args[]) {
    Car c = new Car(...);
    Rydwan r = new Rydwan(...);
    Vehicle v;
    if (args[0].equals("Rydwan")) v = r;
    else v = c;
    v.start();
}
Kompilator nie może wiedzieć jaki konkretnie jest typ obiektu wskazywanego przez v (czy Car czy Rydwan). I nie wie!

W jaki sposób zatem uzyskujemy opisany wcześniej (zgodny z życiowym doświadczeniem) efekt, czyli np.  wywołanie metody start() z klasy Car, jeśli v wskazuje na obiekt klasy Car, natomiast wywołanie metody start() z klasy Rydwan, jeśli v wskazuje na  obiekt klasy Rydwan ?

Otóż metoda start() z klasy Vehicle jest metodą wirtualną, a dla takich metod wiązanie odwołań z kodem następuje w fazie wykonania,  a nie w fazie kompilacji.

Nazywa się to  "dynamic binding" lub "late binding".

Mówi się, że odwołania do metod wirtualnych są  polimorficzne, a słowo "polimorficzne" używane jest w tym sensie, iż konkretny efekt odwołania może przybierać różne kształty, w zależności od tego jaki jest faktyczny typ obiektu na rzecz którego wywołano metodę wirtualną.
Istotnie, jak widzieliśmy: v.start() raz może oznaczać start samochodu, a innym razem start rydwanu, czy roweru.

Wszystkie metody w Javie są wirtualne, za wyjątkiem:


2. Znaczenie polimorfizmu

Rozważmy pewną hierarchię dziedziczenia, opisującą takie właściwości różnych zwierząt jak nazwa rodzaju, sposób komunikowania się ze światem oraz imię.
Dzięki odpowiedniemu określeniu bazowej klasy Zwierz przy definiowaniu klas pochodnych (takich jak Pies czy Kot) mamy całkiem niewiele roboty.
(uwaga: dla ustalenia uwagi  w dalszych przykładach pomijamy specyfikatory dostępu, bowiem nie mają one znaczenia dla omawianych tu treści).
class Zwierz {
  String name = "bez imienia";
  Zwierz() { }
  Zwierz(String s) { name = s; }
  String getTyp()      { return "Jakis zwierz"; }
  String getName()     { return  name;   }
  String getVoice()    { return "?"; }

  // Metoda speak symuluje wydanie głosu poprzez wypisanie odpowiedniego komunikatu
  void speak()  {
    System.out.println(getTyp()+" "+getName()+" mówi "+getVoice());
  }
}

class Pies extends Zwierz {
  Pies()         { }
  Pies(String s) { super(s); }
  String getTyp()      { return "Pies";   }
  String getVoice()    { return "HAU, HAU!"; }
}

class Kot extends Zwierz {
  Kot()         { }
  Kot(String s) { super(s); }
  String getTyp()      { return "Kot";        }
  String getVoice()    { return "Miauuuu..."; }
}
W klasie Main wypróbujemy  naszą hierarchię klas  zwierząt przy symulowaniu rozmów pomiędzy poszczególnymi osobnikami. Rozmowę symuluje statyczna funkcja animalDialog, która ma dwa argumenty – obiekty typu Zwierz, oznaczające aktualnych "dyskutantów".

public class Main {
  public static void main(String[] arg) {
    Zwierz z1 = new Zwierz(),
           z2 = new Zwierz();
    Pies pies = new Pies(),
         kuba = new Pies("Kuba"),
         reksio = new Pies("Reksio");
    Kot kot = new Kot();

    animalDialog(z1, z2);
    animalDialog(kuba, reksio);
    animalDialog(kuba, kot);
    animalDialog(reksio, pies);
  }

  static void animalDialog(Zwierz z1, Zwierz z2) {
    z1.speak();
    z2.speak();
    System.out.println("----------------------------------------");
   }
}

Uruchomienie tej aplikacji da następujący wynik:
Jakis zwierz bez imienia mówi ?
Jakis zwierz bez imienia mówi ?
----------------------------------------
Pies Kuba mówi HAU, HAU!
Pies Reksio mówi HAU, HAU!
----------------------------------------
Pies Kuba mówi HAU, HAU!
Kot bez imienia mówi Miauuuu...
----------------------------------------
Pies Reksio mówi HAU, HAU!
Pies bez imienia mówi HAU, HAU!
----------------------------------------

Cóż jest ciekawego w tym przykładzie? Otóż dzięki wirtualności metod getTyp() i getVoice() metoda speak(), określona w klasie Zwierz prawidłowo działa dla różnych zwierząt (obiektów podklas klasy Zwierz).
Jest to nie tylko ciekawe, ale i wygodne: jedna definicja metody speak() załatwiła nam wszystkie potrzeby (dotyczące dialogów różnych zwerząt). Co więcej – będzie ona tak samo użyteczna dla każdej nowej podklasy Zwierza, którą kiedykolwiek w przyszłości wprowadzimy. 

Nie jest to kwestia jakichś wymyślonych przykładów. Z praktyczną użytecznością polimorfizmu stykamy się w Javie od  samego początku. Jak już wielokrotnie wspominano, metoda println z argumentem Object wyprowadza tekst opisujący przekazany obiekt. Tekst ten dostaje poprzez odwołanie do metody toString() za pośrednictwem statycznej metody valueOf(Object) z klasy String:

W klasie String:

    public static String valueOf(Object obj) {
	return (obj == null) ? "null" : obj.toString();
    }

W klasie PrintStream (lub PrintWriter): 

    public void println(Object obj) {
        String txt = String.valueOf(x);
        // wypisanie tekstu txt
    } 

Metoda toString() jest po raz pierwszy zdefiniowana w klasie Object - jej wynikiem jest tam napis w postaci: nazwa_klasy@unikalny_identyfikator_obiektu. Klasę Object dziedziczą wszystkie klasy (pośrednio lub bezpośrednio). W klasach tych można więc zawsze przedefiniować metodę toString(). A przedefiniowane metody wołane są polimorficznie - zawsze więc uzyskamy właściwy opis obiektu (określony w danej klasie), lub - jeśli nie zdefiniowano w niej metody toString - opis z pierwszej nadklasy, w której jest ona zdefiniowana.
Na przykład, mając taką hierarchię dziedziczenia jak na rysunku:

r 
i używając programu testowego:
package tostring;

public class TestToString {

  public static void main(String[] args) {
    Object[] elts = { new Para(3, 5),
                      new Vehicle(
                          new Person("Stefan", "65021123456"),
                          100, 100, 100, 100 
                      ),
                      new Car("AA7897", 
                              new Person("Janek", "78011222457"),
                              100, 100, 100, 500, 50
                      ),
                      new MojaKlasa(), new Bicycle() };
    // Iterujemy po tablicy obiektów
    // dla każdego zostanie wywołana metoda toString() z jego klasy
    // albo z pierwszej jego nadklasy, która ją definiuje 
    // (w ostateczności z klasy Object)
    for (Object elt : elts) {
      System.out.println(elt);
    }
  }

}
uzyskamy następujący wynik:
( 3, 5 )
Pojazd 1, właścicielem którego jest Stefan - STOI
Samochód nr rej AA7897 - STOI
tostring.MojaKlasa@1fb8ee3
Pojazd 3, właścicielem którego jest sklep - STOI

Pamietamy, że przedefiniowanie metody wymaga, aby miała ona identyczną sygnaturę i identyczny (lub kowariantny) typ wyniku jak metoda z nadklasy, a także by nie ograniczała jej widzialności (dostepu).
Problemy ze specyfikatorami dostępu wykryje kompilator. Np. w kontekście :
class A {
  String toString() {
    return "Obiekt klasy A";
  }
}
uzyskamy komunikat o błędzie:
Cannot reduce the visibility of the inherited method from Object  
bo metoda String toString() ma w klasie Object specyfikator dostępu public, ai nie podając żadnego specyfikatora dla tej metody w klasie A ograniczono dostęp z publicznego do pakietowego.

Gorzej, gdy popełnimy błędy w pisowni nazwy metody lub podamy niewłaściwe argumenty.
Może się wydawać, że metoda jest przedefiniowana, gdy wcale tak nie jest. A jak nie ma przedefiniowania - to nie ma polimorficznych odwołań.
Z przykładowego programu:
package tostring;

class A {
  public String ToString() {
    return "Obiekt klasy A";
  }
}

class B {
  public String toString() {
    return "Obiekt klasy B";
  }
}

class C extends B {
  public String toString(String ... myMsg) {
    String s = "Obiekt klasy C";
    if (myMsg.length == 1) s += myMsg[0]; 
    return s;
  }    
}

public class UseOverride {
  public static void main(String[] args) {
    Object[] arr = { new A(), new B(), new C() };
    for (Object o : arr) {
      System.out.println(o);
    }
  }
}
uzyskamy - zapewne wbrew intencjom - następujące wyniki:
tostring.A@a90653  // błąd w nazwie metody - wołana jest toString() z klasy Object
Obiekt klasy B     // tu dobrze
Obiekt klasy B     // ale tu metoda toString() jest przeciążona w klasie C, a nie przedefiniowana
                   // dlatego dostajemy napis z metody toString z klasy B

Aby uniknąć tego rodzaju błędów w codziennym programowaniu warto przyzwyczaić się i stosować konwencje nazewnicze Javy (wtedy nigdy nie napiszemy nazwę jako metody ToString). Oprócz tego pomocna jest adnotacja @Override, o której była już mowa w punkcie o przedefiniowaniu metod.
Za pomoca tej adnotacji informujemy kompilator, że naszą intencją jest przedefiniowanie metody. Jeśli warunki przedefiniowania nie są spełnione (nie ma metody o identycznej sygnaturze i wyniku w nadklasach) - kompilator poinformuje o błędzie.
Użycie @Override w poprzednim przykładzie ilustruje rysunek.

r


Przedefiniowując metody warto używać adnotacji @Override. Dzięki temu można łatwo uniknąć błędów, szczególnie gdy intencją jest polimorficzne wywołanie metod.

Polimorfizm działa również sprawnie dla kowariantnych typów wyników. Przypomnijmy sobie klasy Liczba, Cala i Rzecz z poprzedniego wykładu, obrazujące kowariancję typów wyniku przy przedefiniowaniu metod.
class Liczba {
  
  private Number n;
  
  public Liczba() {
  }
  
  public Liczba(Integer i) {
    n = i;
  }
  
  public Liczba(Double d) {
    n = d;
  }
  
  public Number getNumber() { 
    return n; 
  }

}

class Cala extends Liczba {
  
  public Cala(int n) {
    super(n);
  }
  
  public Integer getNumber() {
    return super.getNumber().intValue();
  }

}

class Rzecz extends Liczba {
  
  public Rzecz(double n) {
    super(n);
  }
  
  public Double getNumber() {
    return super.getNumber().doubleValue();
  }

}
Padło wtedy pytanie o sens. Otóż możliwość stosowania kowariancji wyników powoduje, że poniższy program testowy:
  public static void main(String[] args) {
    Liczba[] liczby = { new Cala(1), new Rzecz(2.1), new Rzecz(3.1), new Cala(3) };
    for (Liczba l : liczby) {
      System.out.println(l.getNumber());
    }
  }
wyprowadzi właściwe wyniki:
1
2.1
3.1
3

Metoda getNumber nie tylko jest wołana polimorficznie, ale zwraca też odpowiednio zawężony wynik, co widać na wydruku, gdzie liczby całkowite nie mają kropek (separatorów miejsc dziesiętnych).
Gdyby nie było kowariancji wyników, metody getNumber() w klasach Cala i Rzecz musielibyśmy zdefiniować z typem wyniku Double. Oczywiście, byłyby one wołane polimorficznie, ale wynik byłby inny - liczby całkowite wyprowadzane byłyby z kropką.

3. Metody i klasy abstrakcyjne


Metoda abstrakcyjna nie ma implementacji (ciała) i winna być zadeklarowana ze specyfikatorem abstract.

    abstract int getSomething(); // nie ma ciała - tylko średnik

Klasa w której zadeklarowano jakąkolwiek metodę abstrakcyjną jest klasą abstrakcyjną i musi być opatrzona specyfikatora abstract.


Np.
abstract class SomeClass {
  int n;
  abstract int getSomething();
  void say() { System.out.println("Coś tam");
} 

Po co są metody abstrakcyjne?
Metody abstrakcyjne to takie, co do których nie wiemy jeszcze jaka może być ich konkretna implementacja (lub nie chcemy tego przesądzać), ale wiemy, że powinny wystąpić w zestawie metod każdej konkretnej klasy dziedziczącej klasę abstrakcyjną.
Konkretna implementacja (definicja w klasie kodu metody) może być bardzo różna, w zależności od konkretnego rodzaju obiektów, które opisuje dana klasa.

Klasa abstrakcyjna nie musi mieć metod abstrakcyjnych.
Wystarczy zadeklarować ją ze specyfikatorem abstract.

Abstrakcyjność klasy oznacza, iż nie można  tworzy jej egzemplarzy (obiektów).

 
Moglibyśmy więc zadeklarować klasę Zwierz ze specyfikatorem abstract:

abstract class Zwierz {
   String name = "bez imienia";
   Zwierz() { }
   Zwierz(String s) { name = s; }
   String getTyp()      { return "Jakis zwierz"; }
   String getName()     { return  name;   }
   String getVoice()    { return "?"; }
   void speak()  {
     System.out.println(getTyp()+" "+getName()+" mówi "+getVoice());
   }
}
powiadając w ten sposób:
nie chcemy bezpośrednio tworzyć obiektów klasy Zwierz.
Cóż to jest Zwierz?
To dla nas jest - być może - czysta abstrakcja.
Abstrakcyjna klasa Zwierz może być natomiast dziedziczona przez klasy konkretne np. Pies czy Kot. albo może Tygrys, co daje im już pewne zagwarantowane cechy i funkcjonalność.
Dopiero z tymi konkretnymi typami zwierząt możemy się jakoś obchodzić, a zestaw metod wprowadzonych w klasie Zwierza daje nam po temu ustalone środki.

Skoro Zwierz jest abstrakcyjny, to zestaw jego metod (tu: do jakiegoś stopnia) może być też abstrakcyjny:
abstract class Zwierz {
   String name = "bez imienia";
   Zwierz() {}
   Zwierz(String s) { name = s; }
   abstract String getTyp();
   abstract String getVoice();
   String getName() { return  name; }
   void speak()  {
    System.out.println(getTyp()+" "+getName()+" mówi "+getVoice());
   }
}

Metody getTyp() i getVoice() są abstrakcyjne (nie dostarczyliśmy ich implementacji, bowiem zależy ona od konkretnego Zwierza).

Więcej są - jak domyślnie wszystkie metody w Javie - wirtualne.

Wirtualne - znaczy o możliwych różnych definicjach przy konkretyzacji.
Wirtualne - o nieznanym (jeszcze) dokładnie sposobie działania.
Wirtualne - niekoniecznie już istniejące.

W tym kontekście metoda speak() staje się jeszcze ciekawsza.
Oto używamy w niej nieistniejących jeszcze metod!
Możemy się odwoływać do czegoś co być może  powstanie dopiero w przyszłości.
Co może mieć wiele różnorodnych konkretnych kształtów, teraz nam jeszcze nie znanych.

Konkretyzacje następują w klasach pochodnych, gdzie implementujemy (definiujemy) abstrakcyjne metody getTyp i getVoice.

Klasa dziedzicząca klasę abstrakcyjną musi zdefiniować wszystkie abstrakcyjne metody tej klasy, albo sama będzie klasą abstrakcyjną i wtedy jej definicja musi być opatrzona specyfikatorem abstract.


Zatem po to, byc móc tworzyć i posługiwać się obiektami klas Pies i Kot musimy zdefiniować w tych klasach abstrakcyjne metody klasy Zwierz.
class Pies extends Zwierz {
   Pies() { }
   Pies(String s) { super(s); }
   String getTyp()      { return "Pies";   }
   String getVoice()    { return "HAU, HAU!"; }
}

class Kot extends Zwierz {
   Kot() { }
   Kot(String s) { super(s); }
   String getTyp()      { return "Kot";        }
   String getVoice()    { return "Miauuuu..."; }
}

Możliwość deklarowania metod abstrakcyjnych można też traktować jako pewne pragmatyczne ulatwienie. Nie musimy oto wymyślać (i zapisywać) sztucznej funkcjanalności, sztucznego działania na zbyt abstrakcyjnym poziomie (jak np. return "jakiś zwierz" czy return "?").

4. 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 odróżnieniu od interfejsu - implementacja określa konkretne definicje metod interfejsu oraz zestaw niestatycznych pól klasy.


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 klas: każda klasa może dziedziczyć bezpośrednio tylko jedną klasę.

Unikanie wielodziedziczenia klas w Javie wynika z niejednoznaczności, które związane są z niektórymi postaciami wielodziedziczenia (np. z tzw. problemem diamentu lub rombu wielodziedziczenia, która to nazwa wynika z kształtu relacji pomiędzy klasami - zob. rysunek).r

Wyobraźmy sobie na chwilę, że w Javie jest wielodziedziczenie klas i  spróbujmy zbudować klasę Child, które niewątpliwie dziedziczy po Father i po Mother, które to z kolei klasy są swoistymi "specjalizacjami" klasy Man.
Niech w klasie Man znajduje się abstrakcyjna metoda String getSex(), a w klasach Father i Mother zdefiniujemy tę metodę tak, by zwracała właściwą dla każdego z rodziców  płeć - np. "kobieta" lub "mężczyzna".

class Man {
    public abstract String getSex();
}

class Father extends Man {
    public String getSex() { return "male"; }
}

class Mother extends Man {
    public String getSex() { return "female"; }
}

class Child extends Mother, Father  { // hipotetyczne wielodziedziczenie
    // klasa Child nie przedefiniowuje metody getSex() !
}

Pytanie: jaki wynik dla obiektu-dziecka (klasy Child) zwróci wywołanie metody getSex() ?
Oczywiście, w tej konkretnej sytuacji - nie wiadomo.

Również pola niestatyczne (i dostępne z poziomu innych klas, np. publiczne, chronione lub pakietowe) sprawiają kłopot przy wielodziedziczeniu. Zobaczmy.

class A {
   public int a;
}

class B extends A {   // ma pole a
}

class C extends A {  //  ma pole a
}

class D extends B i C { // hipotetyczne wielodziedziczenie
}

Obiekt d z klasy D ma element definiowany przez pole a. Jeden czy dwa? Jesli jeden, to który? Czy ten który należy do obiektu B czy pochodzący z obiektu C, czy może to jest ten sam element, definiowany przez pole a klasy A? I jak rozumieć naturalne odwołanie d.a ?

Oczywiście, zawsze można przyjąć jakieś arbitralne ustalenia. Zabronić kompilacji programu z nieszczęsnym Dzieckiem bez określonej płci (bez przedefiniowania metody getSex() w klasie Child w naszym przykładzie). Albo ustalić, że klasa D z drugiego przykładu ma pole a, które jest polem klasy A.

Ale Java tego  nie rozstrzyga: po prostu unika wielodziedziczenia 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 klas.

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:
public 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:

public 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ć:

public 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 klasy Zwierz (jej zmodyfikowana postać nie zawiera metod getVoice(0 i speak(), ponieważ nie wszystkie zwierzęta mówią), pozostałe dwie właściwości przez implementację interfejsów.

W Javie klasa (oprócz dziedziczenia innej klasy) może implementować dowolną liczbę interfejsów.
Zatem w Javie nie ma wielodziedziczenia implementacji, ale za to jest możliwe wielodziedziczenie interfejsów.


Możemy więc napisać:

public abstract class Zwierz {

  private String name = "bez imienia";

  public Zwierz() {}

  public Zwierz(String s) {
    name = s;
  }

  public abstract String getTyp();

  public String getName() {
    return name;
  }
  
}

public class Pies extends Zwierz implements Speakable, Moveable {

  public Pies() {
  }

  public Pies(String s) {
    super(s);
  }

  public 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 " + getName() + " biegnie");
  }

  public void stop() {
    System.out.println("Pies " + getName() + " 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 danych


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 aplikację Wyscig z metodą wyscig:
public class Wyscig {
  
  static void wyscig(Moveable ... moveables) {
    for (Moveable m : moveables) {
      m.start();
      if (m instanceof Vehicle) System.out.println(m);
    }
  }

  public static void main(String[] args) {
    wyscig(new Pies("Kuba"), 
           new Car("WB4545", new Person("Janek", "9012102567"),100, 100, 100, 100, 100), 
           new Kot("Mruczek"), 
           new Bicycle(new Person("Ala", "7011122347"),100, 100, 100, 100)
           ); 
  }
}

to po uruchomieniu tej aplikacji moglibyśmy  otrzymać np. taką informację:

Pies Kuba biegnie
Samochód nr rej WB4545 - JEDZIE
Kot Mruczek się skrada
Pojazd 2, właścicielem którego jest Ala - JEDZIE


Zwróćmy baczną uwagę na ten przykład.

Oto - dzięki koncepcji interfejsów - uzyskaliśmy swoiste poszerzenie polimorfizmu. Polimorficzne odwołania są teraz możliwe nie tylko "wzdłuż" hierarchii dziedziczenia klas, ale również "w poprzek" tych hierarchii.

No, tak - przecież klasy Pies i Kot należą do innej hierarchii dziedziczenia niż klasa Car i Rower. Dzięki interfejsom jednak (dzięki temu, że wszystkie te klasy implementują interfejs Moveable) dla wszystkich tych klas uzyskujemy możliwość polimorficznych odwołań do metody start().

Oczywiście, możemy też w naszych przykładach zastosować konwersje zawężające.
Przypomnijmy: jeśli mamy referencję do obiektu typu Zwierz na którą podstawiono odniesienie do obiektu typu Pies, to możemy zrobić konwersję "w dół" hierarchii 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.

Mechanizm konwersji zawężających w równym stopniu dotyczy interfejsów (interfejsy, tak samo jak klasy, też określają typy, przy czym można dziedziczyć, czy raczej rozszerzać interfejsy; ten temat pozostawimy jednak na boku).

Rozważmy jeszcze dwa krótkie przykłady.

Załóżmy, że klasa Pies ma  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.
Zauważmy, że usuwając z klasy Zwierz właściwość "mówienia" - metodę speak() - ograniczyliśmy jej funkcjonalność, dziedziczoną w podklasach. A przecież to metoda speak() (jej ew. skomplikowana postać, np. wykorzystująca multimedia) służyła nam z początku za argument na rzecz polimorfizmu (metoda napisana raz, działająca właściwie dla różnych nawet nie istniejących jeszcze klas zwierząt).
Nic jednak nie stoi na przeszkodzie, aby przywrócić to wygodne ponowne użycie.

Oczywiście nie możemy przywrócić jej poprzedniej wersji:

  void speak()  {
    System.out.println(getTyp()+" "+getName()+" mówi "+getVoice());
  }
bo wystąpi błąd w kompilacji, gdyż w klasie Zwierz nie ma metody getVoice().

Jest ona deklarowana w interfejsie Speakable - więc na pewno potrzebne jest rzutowanie referencji na rzecz której wywołano metodę speak() do typu Speakable.
Jednak taki zapis nie wystarczy:

  void speak()  {
    System.out.println(getTyp()+" "+getName()+" mówi "+((Speakable) this).getVoice(Speakable.QUIET));
  }
bo gdyby metoda speak(0 została wywołana dla Ryby, która nie wydaje głosu i wobec tego nie implementuje interfejsu Speakable, to wystąpiłby wyjątek ClassCastException:
java.lang.ClassCastException: cannot cast from Ryba to Speakable

Ostatecznie metodę speak() winniśmy zapisać tak (dodajmy jeszcze możliwość wyboru natężenia głosu):

public abstract class Zwierz {

  // ...  

  public void speak(int ... v) {
    int vol = Speakable.QUIET; 
    if (v.length == 1) vol = v[0];  
    String voice;  
    if (this instanceof Speakable) voice = ((Speakable) this).getVoice(vol);
    else voice = "... (cisza) ...";
    System.out.println(getTyp()+" "+getName()+ " mówi " + voice);
  }
  
} 
a klasa testująca:
public class Dialog {
  public static void main(String[] arg) {
    Pies kuba = new Pies("Kuba"), reksio = new Pies("Reksio");
    Kot kot = new Kot("Mruczek");
    Ryba ryba = new Ryba();

    animalDialog(kuba, reksio);
    animalDialog(kuba, kot);
    animalDialog(reksio, ryba);
  }

  static void animalDialog(Zwierz z1, Zwierz z2) {
    z1.speak();
    z2.speak();
    System.out.println("--------------------------------------");
   }
}
da w wyniku:
Pies Kuba mówi Hau... Hau...
Pies Reksio mówi Hau... Hau...
--------------------------------------
Pies Kuba mówi Hau... Hau...
Kot Mruczek mówi Miau...
--------------------------------------
Pies Reksio mówi Hau... Hau...
Ryba bez imienia mówi ... (cisza) ...
--------------------------------------

5. Użyteczność interfejsów: przykłady "for-each" i formatowania

W standardzie Javy jest bardzo dużo interfejsów. Są one wykorzystywane przez wiele klas i bardzo ułatwiają programowanie, bowiem umożliwiają pisanie uniwersalnych, elastycznych kodów. Sztandarowym przykładem mogą być klasy kolekcyjne Javy - w następnym wykładzie zobaczymy jak interfejsy sprzyjają bardziej niezawodnemu i uniwersalnemu oprogramowaniu operacji na kolekcjach.
Wiele interfejsów wprowadzonych do Javy umożliwia też proste rozszerzanie funkcjonalności budowanych przez nas klas. Rozpatrzymy przykłady.

Po zaznajomieniu się z interfejsami można podać dokładną definicję składni rozszerzonej instrukcji for.


Składnia

        for ( Typ id : expr )
            stmt

gdzie:

Iterable jest interfejsem, który zawiera jedną metodę iterator(), zwracającą obiekt-iterator służący do iterowania po zestawie danych. Obiekt-iterator z kolei jest obiektem klasy implementującej interfejs Iterator, w którym znajdują się metody hasNext(), next() i remove().

Możemy więc za pomocą "for-each" iterować po tablicach i po kolekcjach, ale na tym się nie ogranicza użyteczność rozszerzonego for: można go użyć dla obiektów dowolnych klas implementujących interfejs Iterable.

Ogólny mechanizm działania jest taki:

 Iterable<Typ> classObject = ....;
 ....
 for ( Iterator<Typ> $i = classObject.iterator(); $i.hasNext(); ) {
        Typ id = $i.next();
        stmt
 }

Warto zuwazyć, że oba interfejsy Iterable i Iterator są sparametryzowane (przy ich definiowaniu powinniśmy podać w nawiasach kątowych typ zwracanej przez metodę next() wartości). Jest to podobne  do omawianej wczesniej parametryzacji kolekcji i jak już wiemy pozwala wykrywać błędy typów w fazie kompilacji i unikać referencyjnych konwersji zawężających w kodzie programu (więcej o parametryzacji typów już za chwilę).

Dla przykładu stworzymy klasę FromTo, która okresla przedizał dni (od daty do daty), a wobec jej obiektów można stosować rozszerzone for, iterujace po datach przedziału.
Klasa ta może być uzyta np. tak:
public class CalendarForEach {

  public static void main(String[] args) {
 
    Calendar from, to;
    (from = Calendar.getInstance()).set(2008, 9, 1);; // 1 października
    (to = Calendar.getInstance()).set(2008, 9, 10);   // 10 pażdziernika 
    
    FromTo timeInterval = new FromTo(from, to);

    for (Calendar day : timeInterval) {
      System.out.printf("%tF%n", day);
    }
  }

}
co da w wyniku:
2008-10-01
2008-10-02
2008-10-03
2008-10-04
2008-10-05
2008-10-06
2008-10-07
2008-10-08
2008-10-09
2008-10-10

Aby for-each działało klasa FromTo musi implementować Iterable.
import java.util.*;

public class FromTo implements Iterable<Calendar> {

   private Calendar from = Calendar.getInstance(),
                    to = Calendar.getInstance();


   public FromTo(Calendar f, Calendar t) {
      from.setTime(f.getTime());
      to.setTime(t.getTime());
   }

   public Iterator<Calendar> iterator() {
     return new CalendarIterator(from, to);
   }

}
Implementacja w klasie metody iterator() tego interfejsu zwraca obiekt typu Iterator<Calendar>.
Jest to konkretnie obiekt naszaej klasy CalendarIterator, ale ponieważ implementuje ona interfejs Iterator<Calendar> możemy podać własnie ten typ wyniku.
Klasa iteratora pokaauje wydruk.
import java.util.*;
import static java.util.Calendar.*;

public class CalendarIterator implements Iterator<Calendar> {

  private Calendar to;                                // data końca
  private Calendar current = Calendar.getInstance();  // bieżąca data iteratora

  CalendarIterator(Calendar from, Calendar to) {
    current.setTime(from.getTime());
    current.add(DATE, -1);                            // ustawiamy iterator przed początkiem zakresu 
    this.to = to;                                     // koniec zakresu 
  }

  public boolean hasNext() {
    return current.before(to);
  }

  public Calendar next() {
    if (!hasNext()) throw new NoSuchElementException();
    current.add(DATE, 1);
    return current;
  }

  // Metoda remove() nic nie robi,
  // ale musi być bo implementujemy interfejs Iterator
  public void remove() {
  }
}


Drugi przykład wykorzystania dostępnych interfejsów Javy dotyczy formatowania.
W wykładzie "Liczby, daty, algorytmy" była mowa o tym, że  dla symbolu konwersji s lub S jeżeli klasa drugiego argumentu metody format na to pozwala, to do formatowania zostanie użyta metoda formatTo.
Jest to możliwe tylko wtedy gdy klasa argumentu "do sformatowania" implementuje interfejs Formattable i definiuje jego jedyną metodę:

public void formatTo(Formatter formatter, int flags, int width, int precision)

Jeśli teraz za pomocą jakiegoś formatora następuje formatowanie obiektu z użyciem symbolu konwersji s lub S, to:
  1. wywoływana jest metoda formatTo z klasy obiektu,
  2. metodzie formatTo jest przekazywany formator (jako parametr formatter), flagi formatowania (parametr flags), szerokość pola (parametr width) i precyzja (parametr precision),
  3. w metodzie formatTo możemy sprawdzić wartości przekazanych informacji (np. locale formatora, flagi, szerokość precyzje) i na tej podstawie podjąć odpowiednie działania przetwarzające obiekt do wynikowego napisu,
  4. przed zwróceniem sterowania z metody formatTo wywołujemy metodę format przekazanego formatora,
  5. sformatowany napis będzie przez formator zapisany do odpowiedniej destynacji (np. wyprowadzony na standardowe wyjście).
Sprawdzić jakich użyto flag formatowania możemy za pomocą porównywania ze stałymi statycznymi klasy java.util.FormattableFlags. Dostępne są stałe o następujących nazwach:

Przykładowy program pokazuje zastosowanie metody formatTo do łatwej zmiany sposobu wyprowadzania informacji o obiektach klasy Person.  W trybie normalnym wyprowadzane jest nazwisko, w trybie alternatywnym (zastosowana flaga #) - nazwisko i imię. W metodzie formatTo, na podstawie przekazanych informacji wybieramy formę formatowania,i budujemy napis formatu na podstawie przekazanej informacji, po czym za jego pomocą formatujemmy obiekt, używając przekazanego formatora.

import java.util.*;
import static java.util.FormattableFlags.*;

public class Person implements Formattable{
  
  private String fname;
  private String lname;

  public Person(String fname, String lname) {
    this.fname = fname;
    this.lname = lname;
  }

  @Override
  public void formatTo(Formatter formatter, int flags, int width, int precision) {
    String txt = lname;
    if ((flags & ALTERNATE) == ALTERNATE) txt += ' ' + fname;
    String fs = "%";
    if ((flags & LEFT_JUSTIFY) == LEFT_JUSTIFY) fs += '-';
    if (width >= 0) fs += width;
    if (precision >= 0) fs += "."+precision;
    fs += ((flags & UPPERCASE) == UPPERCASE) ?  "S" : "s";
    formatter.format(fs, txt);
  }


  public static void main(String[] args) {
    Person e =  new Person("Jan", "Kowalski");
    System.out.printf("%#s\n", e);
    System.out.printf("%20s\n", e);
    System.out.printf("%#30S\n", e);
    System.out.printf("%#.10S\n", e);
  }

}
Wynik działania programu:
Kowalski Jan
            Kowalski
                  KOWALSKI JAN
KOWALSKI J


6. Klasy wewnętrzne

6.1. Pojęcie klasy wewnętrznej


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.


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.


Po co są klasy wewnętrzne?  W jakim celu są używane?
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 actionPerformed(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, gdy nie jedziesz 
       else  {                                      
         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());
    }
}

Podana na wydruku metoda main()
  public static void main(String[] args) throws InterruptedException {
    Car c = new Car("aaa", new Person("x", "c"), 100, 100, 100, 100, 100);
    c.fill(7);
    c.start();
    for (int i = 1; i <= 9; i++) {
      Thread.sleep(1000);
      System.out.println("Po " + i + " sek. - " + c);
    }
  }
wyprowadzi pokazane wyniki:
Po 1 sek. - Samochód nr rej aaa - JEDZIE
Po 2 sek. - Samochód nr rej aaa - JEDZIE
Po 3 sek. - Samochód nr rej aaa - JEDZIE
Po 4 sek. - Samochód nr rej aaa - JEDZIE
Po 5 sek. - Samochód nr rej aaa - JEDZIE
Po 6 sek. - Samochód nr rej aaa - JEDZIE
Po 7 sek. - Samochód nr rej aaa - JEDZIE
Po 8 sek. - Samochód nr rej aaa - STOI
Po 9 sek. - Samochód nr rej aaa - STOI
Uwaga: otrzymywane wyniki mogą się lekko różnić w zalezności od okolicznosci, bowiem mamy tu sytuację równoległego wykonania dwóch fragmentów kodu (metody actionPerformed() wołanej co sekundę przez Timer i metody toString() wołanej w main) wspóldzielących tę samą zmienną (state). Więcej na ten temat przy okazji  omawiania programowania współbieżnego w następnym semestrze.

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();

6.2. 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:


6.3. 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.

Możemy teraz zmodyfikować poprzedni przykłąd iterowania po datach (implementacja interfejsu Iterable w klasie FromTo). Odrębna klasa dla  kalendarzowego iteratora nie jest nam wcale potrzebna. Definicję klasy iteratora możemy umieściś bezpośrednio w metodzie iterator() - w instrukcji return, która ma zwrócić obiekt-iterator.
import static java.util.Calendar.*;

public class FromTo implements Iterable<Calendar> {

   private Calendar from = Calendar.getInstance(),
                    to = Calendar.getInstance();


   public FromTo(Calendar f, Calendar t) {
      from.setTime(f.getTime());
      to.setTime(t.getTime());
   }
   
   @Override
   public Iterator<Calendar> iterator() {
     
     // użycie lokalnej anonimowej klasy wewnętrznej
     return new Iterator<Calendar>() {
       
      Calendar current = Calendar.getInstance();
      
      // Przydatność bloku inicjacyjnego
      {
        current.setTime(from.getTime());
        current.add(DATE,-1);
      }

      @Override
      public boolean hasNext() {
        return current.before(to);
      }

      @Override
      public Calendar next() {
        if (!hasNext()) throw new NoSuchElementException();
        current.add(DATE,1);
        return current;
      }

      @Override
      public void remove() {
      }
       
     };
   }

}
Uwagi:

Rozpatrzmy inny 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]);
      }
   }


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

6.4. Anonimowe klasy wewnętrzne a domknięcia i tablice funkcji

Domknięcie jest fragmentem kodu (zwykle funkcją),  który może zawierać swobodne zmienne oraz ma dostęp do środowiska, wiążącego te zmienne z ich właściwym leksykalnym zakresem.

Między innymi domknięcia pozwalają na traktowanie funkcji jak "normalnych obiektów" czyli np. przekazywanie ich jako argumentów innym funkcjom (metodom). Przy tym kod domknięcia ma efektywny dostęp do wszystkich zmiennych np. zmiennych lokalnych bloku, w którym domknięcie jest deklarowane i pól  otaczającej klasy.
Domknięcia są dostępne w wielu językach  (np. Groovy lub Ruby) i znacząco ułatwiają programowanie. W dotychczasowych wersjach Javy domknięć jeszcze nie ma, ale Neal Gafter mocno już zaawansował projekt ich implementacji w Open JDK i jest szansa, że pojawią się one w którejś z kolejnych wersji Javy, wydawanych przez Sun.
Pewną namiastką domknięć może być jednak w dotychczasowej Javie implementacja interfejsów w anonimowych klasach wewnętrznych.
Zobaczmy to na przykładzie fragmentów kodów działających na tablicach (listach) napisów.
W języku Groovy możemy napisać następujący kod, wypisujący wszystkie napisy dłuższe od 2 znaków, następnie wszystkie napisy przekształcone do dużych liter.
	  String[] napisy = ["Ala", "ma", "kota", "i", "psa" ];
	  napisy.each { txt -> if (txt.size() > 2) println(txt) };
	  napisy.each { txt -> println(txt.toUpperCase()); };
Tutaj korzystamy z metody each, która iteruje po zestawie danych na rzecz którego została wywołana, a jako argument ma domknięcie, stosowane wobec każdego elementu zestawu. Domknięcie podajemy w nawiasach klamrowych i ma ono postać { argument -> kod_do_wykonania }. Argumentem przekazywanym domknięciu jest (w każdej iteracji) kolejny element zestawu napisów.

W tym prostym przypadku ten sam efekt możemy uzyskać w Javie stosując anonimową klasę wewnętrzną implementująca interfejs, określający - jako metodę - kod do wykonania. Obiekt takiej klasy będzie praktycznie swego rodzaju funkcją i obiekt taki podamy jako argument własnej metody each (nie ma takiej w Javie, ale zrobimy ją sobie w klasie dziedziczącej listę napisów).

interface StringOp {
  void execute(String s);
}

class MyStringList extends ArrayList<String> {
  
  public MyStringList(String ...args) {
    for(String s : args) this.add(s);
  }
  
  public void each(StringOp func) {
    for(String s : this) {
      func.execute(s);
    }
  }
}

public class QuasiClosures {

  public static void main(String[] args) {
    
    MyStringList list = new MyStringList("Ala", "ma ", "kota", "i", "psa");
    
    list.each( new StringOp() {  // kod do wykonania podajemy jako obiekt anonimowej klasy wewnętrznej

      @Override
      public void execute(String s) {
        if (s.length() > 2) System.out.println(s);
      }
    });
    

    list.each( new StringOp() {  // a tu inny kod, stworzony ad hoc

      @Override
      public void execute(String s) {
        System.out.println(s.toUpperCase());
      }
    });
    
    

  }
Wynik:
Ala
kota
psa
ALA
MA
KOTA
I
PSA

Oczywiście, jest tu więcej pisania niż przy prawdziwych domknięciach, kod jest też mniej elegancki. Niestety, zastosowanie anonimowych klas wewnętrznych charakteryzuje się również licznymi restrykcjami (i to nie tylko związanymi z dostępem do zmiennych lokalnych). Na prawdziwe domknięcia w Javie  przyjdzie więc jeszcze poczekać.
W sumie jednak już teraz zastosowanie anonimowych klas wewnętrznych, implementujących interfejsy, do przekazywania innym metodom fragmentu kodu do wykonania jest w Javie użyteczne i nieraz wykorzystywane. Przykładem może być metoda invokeLater z klasy SwingUtilities, która otrzymuje  jako argument obiekt klasy implementującej metodę run() z interfejsu Runnable i zapewniająca wykonanie kodu tej metody w tzw. wątku obsługi zdarzeń.

Na koniec dodac warto, że w wielu językach programowania są dostępne tablice funkcji. W Javie można je symulować za pomoca - a jakże - anonimowych klas wewnętrznych, Oto przykład.

public class FuncTab {
  
  interface Func {
     void func();
  }

  
  private void testFuncTab() {

    // Tablica funkcji
    Func[] tabfunc = {
        new Func() {
          public void func() {
            System.out.println("Funkcja 1");
          }
        },
        new Func() {
          public void func() {
            System.out.println("Funkcja 2");
          }
        },
        new Func() {
          public void func() {
            System.out.println("Funkcja 3");
          }
        },
    };
    
    // Wywołanie funkcji z tablicy
    tabfunc[0].func();
    tabfunc[1].func();
    tabfunc[2].func();
  
  }
  
  public static void main(String[] args) {
    new FuncTab().testFuncTab();
  }

}
Wynik:
Funkcja 1
Funkcja 2
Funkcja 3



7. Zaawansowane użycie wyliczeń (enum)

Typy wyliczeniowe (enum), wstępnie opisane w wykładzie "Klasy i obiekty II", mają szereg ciekawych, dodatkowych możliwości. Opierają się one na tym, że tak naprawdę wyliczenia są klasami, dziedzicżącymi specjalną klasę z pakietu java.lang o nazwie Enum.
A zatem:
Przykłady

Przykładowe programy pokazują  jak można rozszerzyć enumerację, traktując ją niemal jak zwykłą klasę.
Najpierw dodamy konstruktor, który wiąże ze stałymi wyliczeniowymi (gazety) ich ceny.

Uwaga. Obiektów enumeracji nie można tworzyć za pomocą wyrażenia new.

package kiosk1;

public enum Gazety {

  Głos(1), Polityka(4.5), Gazeta(2.5);

  Gazety(double p) { price = p; }

  public double getPrice() { return price; }

  private final double price;

}

Następny kod - przy okazji - pokazuje, że przy pisaniu nieco większych programów należy korzystać z komentarzy dokumentacyjnych - i jak z nich korzystać.
package kiosk1;
import java.util.*;
import static kiosk1.Gazety.*; // musi być nazwany pakiet!
import static java.lang.System.out;


public class Kiosk {

  // Mapa gazet dostępnych w kiosku: klucz - gazeta, wartość - liczba egzemplarzy
  private EnumMap<Gazety, Integer> map =
                                  new EnumMap<Gazety, Integer>(Gazety.class);

 public Kiosk() {
   supply(Głos, 20);
   supply(Polityka, 20);
   supply(Gazeta, 20);

   // co jest w kiosku:
   // Jak łatwo i elegancko!!!
   for (Gazety g : Gazety.values())
     out.println(g + " - liczba egzemplarzy " + map.get(g));

   // Sprzedajemy trochę
   double income = 0;
   income += sale(Głos, 2);
   income += sale(Polityka, 10);
   income += sale(Gazeta, 5);
   income += sale(Głos, 2);
   // Teraz w kiosku zostało
   for (Gazety g : Gazety.values())
     out.println(g + " - liczba egzemplarzy " + map.get(g));
   // a uzyskany dochód ze sprzedaży
   out.println("Dochód: " + income);
 }

   /**
   * Dostawa q sztuk gazety g
   * @param g - konkretna gazeta
   * @param q - liczba sztuk w dostawie
   */
   public void supply(Gazety g, int q) {
     // metoda containsKey() zwraca true jesli w mapie jest podany klucz
     if (map.containsKey(g)) q += map.get(g);  
     map.put(g, q);
   }

   /**
   * sprzedaż q sztuk gazety g
   * @param g - sprzedawana gazeta
   * @param q - liczba sprzedanych sztuk
   * @return wartość transakcji
   */
   public double sale(Gazety g, int q) {
     if (!map.containsKey(g)) return 0;
     int n = map.get(g);
     if (q > n) q = n;
     map.put(g, n-q);
     return q*g.getPrice();
   }

   public static void main(String[] args) {
     new Kiosk();
   }

}

Wynik działania programu:

Głos - liczba egzemplarzy 20
Polityka - liczba egzemplarzy 20
Gazeta - liczba egzemplarzy 20
Głos - liczba egzemplarzy 16
Polityka - liczba egzemplarzy 10
Gazeta - liczba egzemplarzy 15
Dochód: 61.5


I bardziej zaawansowany przykład, korzystający z możliwości znaczących rozszerzeń funkcjonalności klas enum (w tym - z wiązania ze stałymi wyliczenia przedefiniowanych metod, które będą wywoływane na ich rzecz).

W nowej wersji "gazet" (konstruktor ma dwa parametry - cenę hurtową i detaliczną), z konkretnymi stałymi (gazetami) wiążemy także przedefiniowane metody toString(), tak by wydruk był jeszcze bardziej informacyjny.
package kiosk2;

public enum Gazety {

  // W konstruktorze użyjemy dwóch parametrów
  // Ze stałymi wyliczenia zwiążemy przedefiniowane metody toString
  // tak, by wydruk był ładniejszy nieco

  Głos(0.75, 1) {
    public String toString() { return "Tygodnik \"Głos\""; }
  },

  Polityka(4, 4.5) {
    public String toString() { return "Tygodnik \"Polityka\""; }
  },

  Gazeta(2, 2.5) {
    public String toString() { return "\"Gazeta Wyborcza\""; }
  };

  Gazety(double wp, double rp) {
    wholesalePrice = wp;
    retailPrice = rp;
  }

  public double getRetailPrice() { return retailPrice; }
  public double getWholesalePrice() { return wholesalePrice; }

  private final double wholesalePrice;
  private final double retailPrice;

}

Wprowadzamy nową enumerację, opisującą transakcje: dostawa (SUPPLY) i sprzedaż (SALE).
package kiosk2;
import java.util.*;

// Dosyć funkcjonalna enumeracja
// opisuje możliwe transakcje (dostawa, sprzedaż gazet)
// a zarazem prowadzi rejestr magazynu i rejestr sprzedanych gazet

public enum Transaction {

   // Elementy wyliczenia (konkretne operacje)
   // Wiązemy z nimi ciała podklas, w których
   // implementujemy metodę perform,
   // która jest zadeklarowana na końcu jako abstrakcyjna
   // Uwaga: w klasach enum elementy wyliczenia winny
   // poprzedzać wszelkie rozszerzenia funkcjonalności

   SUPPLY {
     public void perform(Gazety g, int n) {
       if (volume.containsKey(g)) n += volume.get(g);
       volume.put(g, n);
     }
   },

   SALE {
     public void perform(Gazety g, int n) {
       if (!volume.containsKey(g)) return;
       int k = volume.get(g);
       if (n > k) n = k;
       volume.put(g, k-n);
       sold.put(g, n);
     }
     
   };


   public static int getVolume(Gazety g) {
     if (!volume.containsKey(g)) return 0;
     return volume.get(g);
   }

   public static int getSold(Gazety g) { 
     if (!sold.containsKey(g)) return 0;
     return sold.get(g);
   }

   // Gazety - stan magazynowy   
   private static final EnumMap<Gazety, Integer> volume =
     new  EnumMap<Gazety, Integer>(Gazety.class);
   // Gazety - sprzedane
   private static final EnumMap<Gazety, Integer> sold =
     new  EnumMap<Gazety, Integer>(Gazety.class);


   // Metoda perform musi być zsdeklarowana jako abstrakcyjna
   // ona własnie jest w różny sposób implemetowana w ciałach podklas
   // przypisanych stałym wyliczenia

   public abstract void perform(Gazety g, int n);

}
No i nowa klasa Kiosk, która w pełni korzysta z dobrodziejstw obu enumeracji i w łatwy sposób uzyskuje sporą funkcjonalnośc pod względem obliczeniowym i prezentacyjnym.
package kiosk2;
import static kiosk2.Gazety.*;
import static kiosk2.Transaction.*;
import static java.lang.System.out;


public class Kiosk {

  public Kiosk() {

    // Sprowadzamy gazety
    // możemy użyć dalej zdefiniowanej metody trans (może coś jeszcze będzie robić)
    trans(SUPPLY, Głos, 20);
    trans(SUPPLY, Polityka, 20);
    trans(SUPPLY, Gazeta, 20);

    // ale możemy też pisać tak:
    SUPPLY.perform(Gazeta, 10);
    SUPPLY.perform(Polityka, 10);

    // co jest w kiosku i ile wydano na sprowadzenie gazet:
    out.println("Po dostawie w kiosku są następujące gazety:");
    double cost = 0;
    for (Gazety g : Gazety.values()) {
      int n = getVolume(g);
      cost += g.getWholesalePrice() * n;
      out.println(g + " - liczba egzemplarzy " + n );
    }
    out.println("Wydano: " + cost);

    // Sprzedajemy trochę
    SALE.perform(Polityka, 15);
    SALE.perform(Głos, 10);
    SALE.perform(Gazeta, 20);

    // Co sprzedano i ile uzyskano?
    out.println("Sprzedano gazety: ");
    double income = 0;
    for (Gazety g : Gazety.values()) {
      int n = getSold(g);
      income += g.getRetailPrice() * n;
      out.println(g + " - liczba egzemplarzy " + n );
    }
    out.println("Dochód " + income);
    out.println("Zarobek : " + (income - cost));

    out.println("Zostały do sprzedaży:");
    for(Gazety g : Gazety.values())
      out.println(g + " - liczba egzemplarzy: " + getVolume(g));


  }

  /**
   * Ew. można taką metodę sobie dodac
   *   Transakcja - zakup gazet do kiosku lub ich sprzedaż
   *   @param t - rodzaj transakcji (SUPPLY lub SALE)
   *   @param g - jaka gazeta
   *   @param n - ile sztuk
   */

  public void trans(Transaction t, Gazety g, int n) {
    t.perform(g, n);
  }


  public static void main(String[] args) {
    new Kiosk();
  }

}
Wynik działania programu:


Po dostawie w kiosku są następujące gazety:
Tygodnik "Głos" - liczba egzemplarzy 20
Tygodnik "Polityka" - liczba egzemplarzy 30
"Gazeta Wyborcza" - liczba egzemplarzy 30
Wydano: 195.0
Sprzedano gazety:
Tygodnik "Głos" - liczba egzemplarzy 10
Tygodnik "Polityka" - liczba egzemplarzy 15
"Gazeta Wyborcza" - liczba egzemplarzy 20
Dochód 127.5
Zarobek : -67.5
Zostały do sprzedaży:
Tygodnik "Głos" - liczba egzemplarzy: 10
Tygodnik "Polityka" - liczba egzemplarzy: 15
"Gazeta Wyborcza" - liczba egzemplarzy: 10