13. Beágyazott osztályok

Megadhatunk egy osztályt egy másik osztály tagjaként. Egy ilyen osztályt beágyazott osztálynak hívunk, és a következőképpen néz ki:

class EnclosingClass {
    ...
    class ANestedClass {
        ...
    }
}

A beágyazott osztályokat arra használjuk, hogy kifejezzük és érvényesítsük két osztály között a kapcsolatot. Megadhatunk egy osztályt egy másik osztályon belül, hogyha a beágyazott osztálynak a magába foglaló osztályon belüli környezetben van értelme. Pl. a szövegkurzornak csak a szövegkomponensen belüli környezetben van értelme.

A beágyazó osztály tagjaként a beágyazott osztály kiváltságos helyzetben van. Korlátlan hozzáféréssel rendelkezik a beágyazó osztályok tagjaihoz még akkor is, hogy ha azok privátként vannak deklarálva. Azonban ez a speciális kiváltság nem mindig speciális. A hozzáférést biztosító tagok korlátozzák a hozzáféréseket az olyan osztálytagokhoz, amelyek a beágyazó osztályon kívül esnek. A beágyazott osztály a beágyazó osztályon belül található, ebből kifolyólag hozzáférhet a beágyazó osztály tagjaihoz.

Mint ahogyan más tagokat is, a beágyazott osztályokat is statikusként, avagy nem statikusként lehet deklarálni, ezért ezeket pontosan így is hívják: statikus beágyazott osztály. A nem statikus beágyazott osztályokat belső osztályoknak hívjuk.

class EnclosingClass {
    ...
    static class StaticNestedClass {
        ...
    }
    class InnerClass {
        ...
    }
}

Ahogy a statikus metódusok és változók esetén, amelyeket mi osztálymetódusoknak és változóknak hívunk, a statikus beágyazott osztályt a beágyazó osztályával kapcsoljuk össze. Ahogy az osztálymetódusok, a statikus beágyazott osztályok sem hivatkozhatnak közvetlenül olyan példányváltozókra vagy metódusokra, amely az ő beágyazó osztályában van megadva. A példánymetódusok és változók esetén egy belső osztály az ő beágyazó osztályának a példányával kapcsolódik össze, és közvetlen hozzáférése van annak az objektumnak a példányváltozóihoz és metódusaihoz. Mivel egy belső osztályt egy példánnyal társítanak, ezért önmaga nem definiálhat akármilyen statikus tagot.

Hogy a továbbiakban könnyebb legyen megkülönböztetni a beágyazott osztály és a belső osztály fogalmát, érdemes ezekre a következőféleképpen tekinteni: a beágyazott osztály két osztály közötti szintaktikus kapcsolatot fejez ki, azaz szintaktikailag az egyik osztályban levő kód megtalálható a másikon belül. Ezzel ellentétben a belső osztály olyan objektumok közötti kapcsolatot fejez ki, amelyek két osztálynak a példányai. Tekintsük a következő osztályokat:

class EnclosingClass {
    ...
    class InnerClass {
        ...
    }
}

Az e két osztály közötti kapcsolatnál nem az az érdekes, hogy az InnerClass szintaktikusan definiálva van az EnclosingClass-on belül, hanem az, hogy az InnerClass-nak a példánya csak az EnclosingClass-nak a példáján belül létezhet, és közvetlen hozzáférése van a példányváltozók és a beágyazó osztály példánymetódusaihoz.

A beágyazott osztályok mindkét fajtájával találkozhatunk a Java API-n belül, és kell is használni őket. Azonban a legtöbb beágyazott osztály, amelyeket használunk, valószínűleg belső osztály lesz.

13.1. Belső osztályok

Ahhoz, hogy megértsük, mi a belső osztály és mire jó, tekintsük át újra a Stack osztályt. Tegyük fel, hogy ehhez az osztályhoz hozzá szeretnénk adni egy olyan tulajdonságot, amely lehetővé teszi egy másik osztály számára, hogy kilistázza a veremben lévő elemeket vagy tagokat java.util.Iterator interfész használatával. Ez az interfész három metódus deklarációt tartalmaz:

public boolean hasNext();
public Object next();
public void remove();

Az Iterator definiálja azt az interfészt, ami végigmegy egyszer az elemeken egy megadott sorrend szerint, amelyet a következőképpen adunk meg:

while (hasNext()) {
    next();
}

A Stack osztály önmaga nem tudja végrehajtani az Iterator interfészt, mert az Iterator interfész API-ja bizonyos korlátokat szab: két különböző objektum nem listázhatja kis egyszerre a veremben lévő elemeket. Ugyanis nincs lehetőség arra, hogy megtudjuk, ki hívja meg a következő metódust, és a listázást nem lehet újraindítani, mert az Iterator interfésznek nincsen olyan metódusa, amely támogatná ezt. Így a kilistázást csak egyszer lehet végrehajtani, mert az Iterator interfésznek nincs arra metódusa, hogy az elejéhez visszamenjen a bejárás. Ehelyett egy belső osztály van, amely elvégzi a verem számára ezt a munkát. A belső osztály hozzáférhet a verem elemeihez, és már csak azért is hozzá kell, hogy tudjon férni, mert a veremnek a publikus interfésze csak LIFO hozzáférést támogat. Itt lép be a képbe a belső osztály. Itt látható egy verem implementáció, ami definiál egy belső osztályt, amelyet StackIterator-nak hívunk, és kilistázza a verem elemeit.

public class Stack {
    private Object[] items;
    public Iterator iterator() {
        return new StackIterator();
    }
    class StackIterator implements Iterator {
        int currentItem = items.size() - 1;
        public boolean hasNext() {
            ...
        }
        public Object next() {
            ...
        }
        public void remove() {
            ...
        }
    }
}

Figyeljük meg, hogy a StackIterator osztály közvetlenül hivatkozik a veremben levő elemek példányainak változóira. A belső osztályokat elsősorban arra használjuk, hogy implementáljuk a segítő osztályokat, úgy, mint itt, ebben a példában is láthatjuk. Ha GUI eseményeket akarunk használni, akkor ismernünk kell a belső osztályok használatának a szabályait, mert az eseménykezelés mechanizmusa elég erőteljesen használja ezeket.

Deklarálhatunk egy belső osztályt anélkül, hogy nevet adnánk neki. Az ilyen osztályokat névtelen osztályoknak hívjuk. Itt látható még egy változata a Stack osztálynak, ebben az esetben névtelen osztályt használunk az Iterator számára.

public class Stack {
    private Object[] items;
    public Iterator iterator() {
        return new Iterator() {
            int currentItem = items.size() - 1;
            public boolean hasNext() {
                ...
            }
            public Object next() {
                ...
            }
            public void remove() {
                ...
            }
        }
    }
}

A névtelen osztályok nehézkessé tehetik a kód olvasását, ezért csak olyan osztályokhoz érdemes használnunk, amelyek nagyon kicsik (nem több mint egy-két metódus), és amelyek használata eléggé egyértelmű, mint például az eseménykezelő osztály.

13.2. Néhány további érdekesség

ás osztályokhoz hasonlóan a beágyazott osztályokat is lehet absztraktnak vagy véglegesnek deklarálni. Ennek a két módosítónak a jelentése a beágyazott osztály esetén is ugyanaz, mint más osztályoknál.

Ugyanígy használhatjuk a hozzáférés módosítókat – mint például a private, public és protected –, hogy korlátozzuk a beágyazott osztályokhoz való hozzáférést, mint ahogy minden más osztálytag esetén tehetjük azt. Nem csak a névtelen, hanem bármilyen beágyazott osztály deklarálható bármilyen kódblokkon belül. A beágyazott osztály, amely egy metóduson vagy egy másik kódblokkon belül van deklarálva, hozzáférhet bármilyen hatókörön belüli végleges lokális változóhoz.

13.3. Ellenőrző kérdések

A következő kódban melyik nem kerülhet az XX helyére?

public class MyClass1 {
    public static void main(String argv[]){ }
    XX class MyInner {}
}
  • public
  • private
  • static
  • friend