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:
- zestaw publicznych abstrakcyjnych metod
- i/lub publicznych statycznych stałych
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:
- Interfejs zawiera deklaracje publicznych metod abstrakcyjnych oraz ew. publicznych stałych statycznych.
- Każda klasa implementująca interfejs musi zdefiniować WSZYSTKIE jego metody albo będzie klasą abstrakcyjną.
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:
- Pies jest Zwierzem
- Pies potrafi mówić
- Pies może się poruszać
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ż:
- klasa Pies dziedziczy klasę Zwierz - zatem kuba jest typu Zwierz,
- klasa Pies implementuje interfejs Speakable - zatem kuba jest typu Speakable,
- 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.
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ół") :
- Wymagają jawnego użycia operatora konwersji.
- Są bezpieczne: Java w trakcie wykonania programu wykryje błąd, polegający
na konwersji do niewłaściwego typu (tj . do referencji do obiektu klasy,
która nie dziedziczy - choćby pośrednio - klasy, referencję do obiektu której
poddajemy konwersji).
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:
- Przy przekazywaniu argumentu następuje konwersja: Kot -> Zwierz,.
- Polimorficznie jest wołana metoda getTyp() (z jest Zwierz, ale Java wie, że w tym Zwierzu siedzi Kot).
- Ponieważ w z siedzi Kot, który implementuje interfejs
Speakable, można zrobić konwersję do typu Speakable i odwołać się polimorficznie
do getVoice().
- 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:
- Konwersję Psa do typu Moveable (w górę).
- Polimorficzne wywołanie metody start() na rzecz obiektu oznaczanego przez m (formalnego typu Moveable, który jednak faktycznie jest typu Pies).
- 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?
- Klasy wewnętrzne mogą być ukryte przed innymi klasami pakietu (względy bezpieczeństwa)
- Klasy wewnętrzne pozwalają unikać kolizji nazw (np. klasa wewnętrzna
nazwana Vector nie koliduje nazwą z klasą zewnętrzną o tej samej nazwie)
- Klasy wewnętrzne pozwalają (czasami) na lepszą, bardziej klarowną strukturyzację
kodu, bo można odwoływać się z nich do składowych (nawet prywatnych) klasy
otaczającej, a przy tym zlokalizować pewne działania
- Klasy wewnętrzne (w szczególności anonimowe) są intensywnie używane przy implementacji standardowych interfejsów Javy
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
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:
- NazwaTypu – nazwa nadklasy (klasy dziedziczonej w klasie wewnętrznej) lub implementowanego przez klasę wewnętrzną interfejsu,
- parametry – argumenty przekazywane konstruktorowi nadklasy;
w przypadku gdy Typ jest nazwą interfejsu lista parametrów jest oczywiście
pusta (bo chodzi o implementację interfejsu).
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:
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:
- anonimowe klasy wewnętrzne nie mogą mieć konstruktorów (bo nie mają nazwy),
- za pomocą anonimowej klasy wewnętrznej można stworzyć tylko jeden obiekt, bo definicja klasy podana jest w wyrażeniu new
czyli przy tworzeniu obiektu, a nie mając nazwy klasy nie możemy potem tworzyć
innych obiektów; jeśli jednak to wyrażenie new umieścimy np. w pętli – to
oczywiście stworzone zostanie tyle obiektów ile razy wykona się pętla,
- definiowanie klas wewnętrznych implementujących interfejsy stanowi
jedyny dopuszczalny przypadek użycia nazwy interfejsu w wyrażeniu new (nie
wolno na przykład pisać ActionListener al = new ActionListener();, ale możemy
użyć new ActionListener() wtedy, gdy zaraz potem następuje definicja anonimowej
klasy wewnętrznej implementującej interfejs ActionListener),
- anonimowe klasy wewnętrzne są kompilowane do plików .class o nazwach
automatycznie nadawanych przez kompilator (nazwa składa się z nazwy klasy
otaczającej i jakiegoś automatycznie nadawanego identyfikatora np. Car$1.class)
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.
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