28 października 2008

SCJP - Statyczne klasy zagnieżdżone

Statyczne klasy zagnieżdżone (ang. static nested classes) to jeden z niewielu tematów, które w książce „SCJP Sun Certified Programmer for Java 5 Study Guide (Exam 310-055)” są mocno niedopowiedziane - w jaskrawym przeciwieństwie do wielu innych, znacznie banalniejszych (choć pewnie ważniejszych) tematów, które są zdecydowanie przegadane, albo wręcz zagadane. Postaram się dziś wypełnić tę lukę, choć mając na uwadze ostateczny cel, tj. przygotowanie do SCJP powstrzymam się od wchodzenia w temat zbyt głęboko. Lubiącym poczytać po angielsku (oraz lubiącym wiedzieć więcej) polecam artykuł „Nested Classes, Part 3”. Przy okazji, przestrzegam czytelników wspomnianej książki o SCJP, że to co napisano we wstępie do rozdziału o klasach zagnieżdżonych jest nieprawdą. Proszę przeczytać więc poniżej zacytowany fragment tejże i zapamiętać, że tak nie jest. Inaczej niż piszą autorzy - klasy zagnieżdżone znajdują się w specjalnym związku z klasami zewnętrznymi (ang. special relationship), o czym za chwilę.

“While an inner class (…) enjoys that special relationship with the outer class (…), a static nested class does not. It is simply a non-inner (also called "top-level") class scoped within another. So with static classes it's really more about name-space resolution than about an implicit relationship between the two classes.”

Czy klasy wewnętrzne (ang. inner classes) i klasy zagnieżdżone (ang. nested classes) to to samo, tylko inaczej nazwane? Jak można się domyślać - nie! Podobnie jak klasę wewnętrzną, klasę zagnieżdżoną definiujemy wewnątrz innej klasy – klasy zewnętrznej – jednak klasy zagnieżdżone, w odróżnieniu od wewnętrznych są to klasy statyczne. Zobaczmy na przykładzie, jak to wygląda.
class Notepad {
private static Date lastCreatedDate;

static class Note {
private Date creationDate;

public Date getCreationDate() {
return creationDate;
}

public Date getLastCreatedDate() {
return lastCreatedDate; // statyczna zmienna prywatna klasy zewnętrznej
}
}

public static Note newNote() {
Note art = new Note();

art.creationDate = new Date(); // zmienna prywatna z klasy wewnętrznej
lastCreatedDate = art.creationDate;

return art;
}
}

Klasa ‘Notepad’ jest w naszym przypadku klasą zewnętrzną a ‘Note’ klasą zagnieżdżoną. Zauważmy, że deklarację klasy ‘Note’ poprzedzono modyfikatorem ‘static’. To właśnie dodając to słówko kluczowe mówimy, że chodzi nam o klasę zagnieżdżoną a nie wewnętrzną. Podobnie jak klasę wewnętrzną, klasę zagnieżdżoną łączy z klasą zewnętrzną pewien szczególny związek, związek oparty na pełnym zaufaniu, a więc taki, który pozwala na dostęp do zmiennych prywatnych swojego partnera. Zaufanie działa w obie strony. Zarówno klasa ‘Note’ ma dostęp do zmiennych prywatnych klasy ‘Notepad’ (zmienna ‘lastCreatedDate’) jak i klasa ‘Notepad’ do zmiennych prywatnych klasy ‘Note’ (zmienna ‘creationDate’). Zwróćmy jednak szczególną uwagę na fakt, że klasy zagnieżdżone to klasy statyczne – a więc nie związane z żadnym obiektem klasy zewnętrznej – stąd mogą one odwoływać się tylko i wyłącznie do zmiennych (i metod) statycznych z tejże klasy zewnętrznej. Jest to sytuacja zupełnie analogiczna do tej w której jesteśmy implementując metodę statyczną, chociażby metodę ‘main(…)’. Możemy wówczas używać jedynie innych statycznych metod bądź statycznych zmiennych. Zmiennych instancyjnych użyć nie możemy, bo przecież nie mamy żadnej instancji!

Zanim przejdziemy dalej zastanówmy się w jakich okolicznościach ta nieco dziwna konstrukcja językowa może się przydać. Szczęśliwie, bardzo dobry przykład jest na wyciągnięcie ręki; zerknijmy na interfejs ‘java.util.Map<K,V>’, a szczególnie na typ wyniku metody ‘entrySet()’. Jest to ‘Set<Map.Entry<K,V>>’. A cóż to takiego ‘Map.Entry<K,V>’? Uwaga, uwaga… statyczny interfejs zagnieżdżony! Tak, zagnieżdżone mogą być także interfejsy i to zarówno w interfejsach jak i w klasach. Generalnie rzecz biorąc, zagnieżdżać można wszystko i we wszystkim, nawet klasy w interfejsach. Interfejs ‘java.util.Map<K,V>’ implementuje klasa ‘java.util.AbstractMap<K,V>’ (która z kolei jest nadklasą klasy ‘java.util.HashMap<K,V>’) a interfejs zagnieżdżony ‘Map.Entry<K,V>’ klasa zagnieżdżona ‘AbstractMap.SimpleEntry<K,V>. Na drzewku dziedziczenia wygląda to tak:

Jak widać z powyższych przykładów, jeśli klasy zagnieżdżonej używamy poza klasą w której została ona zdefiniowana to musimy nazwę tejże poprzedzić nazwą klasy zewnętrznej, zupełnie jakby klasa zewnętrzna była pakietem, w którym zlokalizowano klasę zagnieżdżoną. Pełną nazwą klasy ‘Note’ jest więc ‘Notepad.Note’. Poniżej przykład, pokazujący jak należy identyfikować klasy zagnieżdżone, w zależności od stopnia zagnieżdżenia klasy i umiejscowienia kodu, który tej klasy używa.
class ExternalClass {
void externalTest() {
Notepad.Note.Line line = new Notepad.Note.Line();
}
}

class Notepad {
static class Note {
static class Line {

}

void noteTest() {
Line line = new Line();

// można też tak - za dużo nie ma nigdy
Notepad.Note.Line nextLine = new Notepad.Note.Line();
}
}

void notepadTest() {
Note.Line line = new Note.Line();
}
}

Istnieje też opcja alternatywna do każdorazowego podawania pełnej – poprzedzonej nazwą klasy zewnętrznej – nazwy klasy zagnieżdżonej. Wystarczy klasę zagnieżdżoną zaimportować; tak jak pokazano na poniższym przykładzie.
import my.pckg.Notepad.Note.Line;

class ExternalClass {
void externalTest() {
Line line = new Line();
}
}

class Notepad {
static class Note {
static class Line {

}
}
}

Poza tym co napisano powyżej zagnieżdżone klasy i interfejsy żądzą się tymi samymi regułami co klasy „normalne”, w szczególności, klasy i interfejsy zagnieżdżone mogą być deklarowane jako ‘public’, ‘protected’ czy ‘private’.

9 komentarzy:

MZ pisze...

Do czego się potrafią przydać wewnętrzne klasy statyczne? Jakoś nie wpaść na sposób ich wykorzystania.

Mariusz Lipiński pisze...

Przecież ogromna czesc mojego artykulu poswiecona jest wlasnie temu, zeby pokazac przyklady. Przyklad z java.util.Map cie nie przekonuje?

Wieczorek pisze...
Ten komentarz został usunięty przez autora.
Anonimowy pisze...

Wielu ludzi będących dopiero laikami (jak ja)czyta ten artykuł i powiedzmy sobie szczerze, że Twój przykład z java.util.Map niczego nie wyjaśnia. Wręcz mąci bardziej, zwłaszcza z wnioskiem że zagnieżdżać można wszytko i wszędzie. Dla mniej wyuczonych (jeszcze... :P) programistów wstaw jakiś "lżejszy przykład". Jeśli oczywiście masz chwilę i ochotę. Pozdrawiam serdecznie. Slawek

Anonimowy pisze...

Bardzo fajny artykuł. Zawsze mieszały mi się pojęcia klasy zagnieżdżonej i wewnętrznej. Wreszcie do mnie dotarła ta różnica :). I do tego się jeszcze dowiedziałem czegoś nowego... Wielkie dzięki! (P.S. Nieuki, sami sobie piszczcie proste przykłady....)

Jeszcze raz dzięki serdecznie!

goodAdviceUncle pisze...

"Podobnie jak klasę wewnętrzną, klasę zagnieżdżoną definiujemy wewnątrz innej klasy – klasy zewnętrznej – jednak klasy zagnieżdżone, w odróżnieniu od wewnętrznych są to klasy statyczne"
Niestety nie jest to prawda, w świetle dokumentacji Javy

"Terminology: Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are simply called static nested classes. Non-static nested classes are called inner classes."

Wniosek - klasa zagnieżdżona NIE MUSI być statyczna

Mariusz Lipiński pisze...

To kwestia terminologii. Ja na klasy niestatyczne mówię wewnętrzne a na statyczne zagnieżdżone.

goodAdviceUncle pisze...

Tak, kwestia terminologii. Ale może po prostu lepiej trzymać się terminologii proponowanej przez developerów Javy, po prostu żeby uniknąć nieścisłości.

Mariusz Lipiński pisze...

Inner classes tłumaczę na klasy wewnętrzne a nested classes na klasy zagnieżdżone. Nie widzę tu żadnego "nietrzymania się" przyjętej terminologii. Swoją drogą - jak się kogoś/coś cytuje, to wypadałoby podać źródło.