Czasem struktura danych naszej aplikacji daleko odbiega od idealnej, kiedy wszystko jest oddzielnym obiektem z jasnymi powiązaniami. Istnieją takie sytuacje, w których wykorzystanie danej klasy ma sens jedynie w połączeniu z inną. Poznajmy więc klasy wewnętrzne.
Mianem “klas wewnętrznych” określa się wszystkie typy klas (zarówno interfejsy, enumy jak i “normalne” klasy) zdefiniowanych wewnątrz innej klasy. Klasy wewnętrzne dzielą się na cztery główne rodzaje:
- standardowe
- statyczne
- lokalne
- anonimowe
Standardowe klasy wewnętrzne
Standardowe klasy wewnętrzne to po prostu zwykłe klasy zdefiniowane wewnątrz innej.
public class Outer { public class Inner { } }
W tym wypadku mamy klasę zewnętrzną Outer i klasę wewnętrzną Inner. Modyfikatory dostępu w przypadku klas wewnętrznych działają tak samo jak w przypadku dziedziczenia. Istotną kwestią natomiast jest fakt, że klasa wewnętrzna ma dostęp do wszystkich atrybutów i metod klasy zewnętrznej.
Aby stworzyć instancję klasy wewnętrznej należy odwołać się do klasy zewnętrznej, a następnie wywołać konstruktor klasy wewnętrznej. Ważne jest, że instancja klasy zewnętrznej musi już być utworzona.
... Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); Outer.Inner inner2 = new Outer.Inner(); // <-- BŁĄD! ...
Statyczne klasy wewnętrzne
W Javie istnieją także statyczne klas wewnętrzne. Są to klasy wewnętrzne poprzedzone modyfikatorem static i do istnienia nie potrzebują instancji klasy zewnętrznej.
public class Outer { public static class Inner { } }
W odróżnieniu od standardowych klas wewnętrznych, klasy statyczne nie potrzebują instancji klasy zewnętrznej do sworzenia własnej instancji. Do stworzenia tejże wystarczy odwołanie się do pełnego typu klasy wewnętrznej (zawierającego ścieżkę złożoną z klasy zewnętrznej), w tym wypadku Outer.Inner.
... Outer outer = new Outer(); Outer.Inner inner1 = outer.new Inner(); Outer.Inner inner2 = new Outer.Inner(); ...
Tym samym pokazaliśmy, że inicjalizacja obiektu klasy wewnętrznej pokazana w akapicie o standardowych klasach wewnętrznych jest prawidłowa – ale tylko dla klas wewnętrznych statycznych.
Lokalne klasy wewnętrzne
Lokalne klasy wewnętrzne są normalnymi klasami wewnętrznymi, jednakże zdefiniowanymi wewnątrz bloku. Podobnie jak lokalne zmienne – ich zasięg ograniczają nawiasy klamrowe bloku, w którym zostały zapisane, i wewnątrz tego bloku zachowują się jak standardowe klasy wewnętrzne.
private static void main(String[] args){ class LocalInner{ @Override public String toString(){ return "inner local class"; } } LocalInner local = new LocalInner(); System.out.println(local); }
Klasy anonimowe
Klasy anonimowe to szczególny rodzaj klas wewnętrznych. Są to klasy definiowane w kodzie, które mają dokładnie jedną instancję. Tworzenie instancji klasy anonimowej powiązane jest z zapisaniem jej definicji.
Rozważmy interfejs LoginInterface oraz jego użycie w klasie aplikacji:
public interface LoginInterface { void login(String user, String pass); void logout(); }
... LoginInterface loginInterface = new LoginInterface() { @Override public void login(String user, String pass){ userService.authUser(user, pass); } @Override public void logout(){ sessionService.dropSession(); } } ...
Widzimy definicję interfejsu umożliwiającego zalogowanie się i wylogowanie użytkownika. Następnie w aplikacji interfejs jest używany do stworzenia klasy anonimowej. Konstrukcja new TYP() { } pozwala na stworzenie klasy anonimowej, w tym wypadku – implementującej interfejs LoginInterface. Można więc przyjąć, że powyższy kod to skrócenie następującego zapisu:
public interface LoginInterface { void login(String user, String pass); void logout(); } public class LoginImpl implements LoginInterface { @Override public void login(String user, String pass) { userService.authUser(user, pass); } @Override public void logout(){ sessionService.dropSession(); } } ... LoginInterface login = new LoginImpl(); ...
Wewnątrz definicji klasy anonimowej możemy definiować normalne pola i metody. Najczęściej jednak sprowadza się to do zaimplementowania metod interfejsu, dla którego tworzymy klasę anonimową.
Istotną kwestią jest, że klasa anonimowa nie tworzy instancji interfejsu, choć tak może się początkowo wydawać. Kompilator tak naprawdę tworzy nową klasę, która implementuje podany interfejs. Klasa stworzona w ten sposób ma swoją nazwę, która zazwyczaj wygląda według wzoru:
NazwaKlasyZewnętrznej$NumerKlasyAnonimowej it.gniado.posts.classes.anonymous.ApplicationMainClass$1
Co to za nazwa? Nazwa klasy wewnętrznej oddzielona jest od klasy zewnętrznej znakiem $. Z racji na to, że LoginInterface jest pierwszą klasą anonimową w danej klasie – jej numer wynosi 1. Każda kolejna klasa anonimowa będzie mieć kolejny numer , np. ApplicationMainClass$2, ApplicationMainClass$3 itd.
Po co używać klas wewnętrznych?
Teoretycznie używanie klas wewnętrznych nie jest wymagane. Wszystko można osiągnąć nadkładając niedużą część kodu i tworząc standardowe klasy. Po coś jednak klasy wewnętrzne zostały wymyślone i wprowadzone. Ich zdecydowaną zaletą jest swobodny dostęp do pól i metod klasy zewnętrznej. Pozwala to na przykład na zastosowanie prywatnych pól i swobodne korzystanie z nich w jakimś konkretnym przypadku bez użycia getterów. Może to mieć zastosowanie chociażby w przypadku klasy, która przechowuje dane w swojej własnej strukturze, a obiekty składające się na tę strukturę są typu, który: nigdzie indziej nie będzie używany, jest nierozerwalnie związany z klasą zarządzającą oraz potrzebuje dostępu do wewnątrz tejże klasy. Takie rozwiązanie z pewnością umożliwia zastosowanie klasy wewnęrznej, czego przykładem może być Map.Entry<K, V>.