1. Czym jest klasa wewnętrzna (inner class)?
W Javie klasę można zadeklarować nie tylko na najwyższym poziomie (w pliku), ale także wewnątrz innej klasy. Taka klasa nazywa się zagnieżdżoną (nested class). Jeśli taka klasa jest zadeklarowana bez modyfikatora static, to nazywa się wewnętrzną (non-static inner class, po prostu inner class).
Klasa wewnętrzna — to klasa zadeklarowana wewnątrz innej klasy i powiązana z egzemplarzem tej klasy zewnętrznej. Może odwoływać się do wszystkich pól i metod klasy zewnętrznej, nawet jeśli są one private. To tak, jakby obiekt miał własnego „tajnego pomocnika”, któremu wolno wszystko!
To trochę jak dom z pokojami. Dom — to klasa zewnętrzna, a pokój — klasa wewnętrzna. Pokój nie może istnieć bez domu, ale ma dostęp do jego zasobów: światła, ogrzewania, mebli. Jeśli dom zniknie, zniknie i pokój. Dokładnie tak działają klasy wewnętrzne w Javie!
Po co są klasy wewnętrzne?
- Logiczne powiązanie: gdy jedna klasa jest potrzebna tylko do współpracy z inną i nie ma sensu poza nią.
- Enkapsulacja: pozwala ukrywać szczegóły implementacji, nie zaśmiecając przestrzeni nazw pakietu.
- Dostęp do prywatnych składowych: klasa wewnętrzna może odwoływać się do prywatnych pól i metod klasy zewnętrznej.
- Kompaktowość: zmniejsza liczbę „śmieciowych” klas na poziomie pakietu.
Składnia deklaracji klasy wewnętrznej
Zadeklarować klasę wewnętrzną jest bardzo prosto: deklaruje się ją wewnątrz ciała innej klasy, bez modyfikatora static.
class Outer {
// pola i metody klasy zewnętrznej
class Inner {
// pola i metody klasy wewnętrznej
void printHello() {
System.out.println("Hello from Inner!");
}
}
}
Tutaj Inner jest klasą wewnętrzną względem Outer.
Ilustracja: wizualizacja
Outer
│
├─ pola/metody
│
└─ Inner (klasa wewnętrzna)
└─ własne pola/metody
2. Jak utworzyć egzemplarz klasy wewnętrznej
Tu kryje się jedna z najczęstszych pułapek dla początkujących! Egzemplarz klasy wewnętrznej jest zawsze powiązany z konkretnym obiektem klasy zewnętrznej.
Przykład:
Outer outer = new Outer(); // tworzymy obiekt klasy zewnętrznej
Outer.Inner inner = outer.new Inner(); // tworzymy klasę wewnętrzną przez obiekt zewnętrzny!
inner.printHello(); // Hello from Inner!
Próba utworzenia po prostu new Inner() zakończy się błędem kompilacji, ponieważ Java nie wie, z którym obiektem klasy zewnętrznej powiązać tę klasę wewnętrzną.
Dlaczego tak?
Klasa wewnętrzna może odwoływać się do pól i metod klasy zewnętrznej. Dlatego musi „wiedzieć”, z którym konkretnie obiektem klasy zewnętrznej pracuje.
3. Przykład użycia klasy wewnętrznej
Przyjrzyjmy się przykładowi, w którym klasa wewnętrzna jest naprawdę przydatna.
Przykład 1: model „Plecak i przedmioty”
Załóżmy, że mamy klasę Backpack, która może przechowywać przedmioty (Item). Chcemy jednak, aby klasa Item była dostępna tylko wewnątrz Backpack, ponieważ poza plecakiem przedmioty nas nie interesują.
public class Backpack {
private String owner;
public Backpack(String owner) {
this.owner = owner;
}
// Klasa wewnętrzna — przedmiot może istnieć tylko w plecaku!
class Item {
private String name;
public Item(String name) {
this.name = name;
}
public void printInfo() {
// Magia! Widzimy prywatne pole klasy zewnętrznej
System.out.println(owner + " ma przedmiot: " + name);
}
}
}
Użycie:
Backpack bp = new Backpack("Wasia");
Backpack.Item item = bp.new Item("Podręcznik Java");
item.printInfo(); // Wasia ma przedmiot: Podręcznik Java
Zwróć uwagę: Item może odwoływać się do pola owner, nawet jeśli jest ono private!
Przykład 2: iterator dla własnej kolekcji
W Javie kolekcje często implementują wewnętrzny iterator. Zróbmy swoją prostą kolekcję z wewnętrzną klasą-iteratorem.
public class IntList {
private int[] data = new int[10];
private int size = 0;
public void add(int value) {
data[size++] = value;
}
// Klasa wewnętrzna — iterator!
class Iterator {
private int index = 0;
public boolean hasNext() {
return index < size;
}
public int next() {
return data[index++];
}
}
}
Użycie:
IntList list = new IntList();
list.add(10);
list.add(20);
IntList.Iterator it = list.new Iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
4. Cechy klas wewnętrznych
Dostęp do składowych klasy zewnętrznej
Klasa wewnętrzna może odwoływać się do dowolnych (nawet private) pól i metod klasy zewnętrznej. To wygodne, ale wymaga ostrożności: jeśli klasa zewnętrzna się zmieni, klasa wewnętrzna może nagle się „zepsuć”.
class Outer {
private int secret = 42;
class Inner {
void showSecret() {
System.out.println("Tajemnica: " + secret);
}
}
}
Klasa wewnętrzna nie może zawierać składowych statycznych
Klasa wewnętrzna (non-static inner) nie może zawierać statycznych pól ani metod, poza stałymi static final (np. public static final int MY_CONST = 123;). Wynika to z faktu, że klasa wewnętrzna zawsze jest „przypięta” do egzemplarza klasy zewnętrznej.
Jeśli potrzebujesz zadeklarować statyczną klasę zagnieżdżoną — użyj modyfikatora static (o tym w następnej lekcji).
Widoczność klasy wewnętrznej
Klasa wewnętrzna może być private, protected, public lub mieć widoczność poziomu pakietu (package-private).
public class Outer {
private class Inner { /* ... */ }
}
5. Przydatne niuanse
Kiedy używać klas wewnętrznych
Klasy wewnętrzne nie są dla ozdoby, lecz do logicznej organizacji kodu. Używaj ich, gdy:
- Klasa jest potrzebna tylko w jednym miejscu (np. pomocniczy iterator, handler, część złożonej struktury).
- Klasa jest ściśle związana z klasą zewnętrzną i nie ma sensu poza nią.
- Chcesz ukryć szczegóły implementacji przed innymi klasami pakietu.
Nie używaj klas wewnętrznych tylko dla mody! Czasem lepiej wynieść klasę na najwyższy poziom, jeśli może się przydać w innych miejscach.
Klasa wewnętrzna a this
Wewnątrz klasy wewnętrznej można odwołać się do pól i metod klasy zewnętrznej za pomocą OuterClassName.this.
class Car {
private String model = "Tesla";
class Engine {
void printModel() {
// Wyraźnie odwołujemy się do obiektu zewnętrznego
System.out.println("Model: " + Car.this.model);
}
}
}
Taka składnia jest szczególnie przydatna, gdy klasa wewnętrzna i zewnętrzna mają pola o tych samych nazwach.
Kiedy NIE warto używać klas wewnętrznych
Nie używaj, jeśli:
- Klasa wewnętrzna nie odwołuje się do pól/metod klasy zewnętrznej
- Klasa może się przydać w innych częściach programu
- Klasa zewnętrzna staje się zbyt duża i skomplikowana
W takich przypadkach lepiej:
- Zrobić z niej statyczną klasę zagnieżdżoną (static class)
- Wynieść klasę do osobnego pliku
- Użyć zwykłych metod zamiast całej klasy
6. Typowe błędy przy pracy z klasami wewnętrznymi
Błąd nr 1: próba utworzenia klasy wewnętrznej bez obiektu klasy zewnętrznej.
Jeśli napiszesz new Inner(), kompilator zgłosi błąd: "No enclosing instance of type Outer is accessible". Zawsze twórz klasę wewnętrzną przez obiekt klasy zewnętrznej: outer.new Inner().
Błąd nr 2: próba zadeklarowania statycznych pól lub metod w klasie wewnętrznej.
Klasa wewnętrzna nie może zawierać składowych statycznych (poza stałymi). Jeśli trzeba — użyj statycznej klasy zagnieżdżonej, tzn. zadeklaruj klasę z modyfikatorem static.
Błąd nr 3: nadużywanie klas wewnętrznych.
Jeśli klasa wewnętrzna nie korzysta z pól/metod klasy zewnętrznej, prawdopodobnie powinna być static albo w ogóle wyniesiona na zewnątrz. Nie twórz klas wewnętrznych „na wszelki wypadek”.
Błąd nr 4: zamieszanie przy odwołaniach do pól klasy zewnętrznej.
Jeśli klasa wewnętrzna i zewnętrzna mają pola o tych samych nazwach, użyj OuterClassName.this.field, aby jednoznacznie odwołać się do pola klasy zewnętrznej.
GO TO FULL VERSION