12. Öröklődés

A java.lang csomagban definiált Object osztály meghatározza és megvalósítja azokat a metódusokat, amelyek minden osztály számára szükségesek. A következő ábrán látható, hogy sok osztály ered az Object-ből, majd sok további osztály származik az előbbi osztályokból, és így tovább, létrehozva ezzel az osztályok hierarchiáját.

Object leszármazottak

A hierarchia csúcsán álló Object az osztályok legáltalánosabbja. A hierarchia alján található osztályok sokkal specializáltabb viselkedést eredményeznek. Egy leszármazott osztály valamely osztályból származik. A superclass kifejezés (továbbiakban szülőosztály vagy ősosztály) egy osztály közvetlen ősére/elődjére, vagy annak bármely felmenő osztályára utal. Minden osztálynak csak és kizárólag egyetlen közvetlen szülőosztálya van.

Egy leszármazott osztály a változóit és metódusait a szülőosztályától örökli. A leszármazott osztály számára azonban lehet, hogy nem elérhető egy öröklött változó vagy függvény. Például, egy leszármazott osztály számára nem érhető el egy private tag, ami a felsőbb osztálytól öröklődött. Mondhatnánk persze, hogy akkor az a tag egyáltalán nem is öröklődött. De igenis öröklődött. Akkor válik ez fontossá, amikor egy olyan belső osztályt használunk, aminek van hozzáférése a mellékelt osztályok private tagjaihoz. Ne feledjük, hogy a konstruktorok nem metódusok, tehát az leszármazott osztályok nem örökölhetik azokat.

12.1. Metódusok felülírása és elrejtése

Ha egy leszármazott osztálybeli metódus, melynek ugyanaz a szignatúrája és visszatérési értéke, mint a szülőosztály metódusának, akkor a leszármazott osztály felülírja a szülőosztály metódusát. (Megjegyzendő, hogy egy metódus szignatúrája a nevéből, valamint paramétereinek számából és típusából áll.)

Egy leszármazott osztály felülíró képessége lehetővé teszi, hogy egy osztály örököljön egy olyan szülőosztálytól, melynek viselkedése elég közeli, majd szükség szerint változtasson a viselkedésen. Például az Object osztály tartalmaz egy toString nevű metódust, amelynek a visszaadja az objektumpéldány szöveges reprezentációját. Minden osztály megörökli ezt a metódust. Az Object metódusának végrehajtása általában nem túl hasznos a leszármazott osztályok számára, ezért a metódus felülírása célszerű, hogy jobb információt nyújthasson az objektum saját magáról. Ez különösen hasznos például nyomkövetés esetén. A következő kód egy példa a toString felülírására:

public class MyClass {
    private int anInt = 4;
    public String toString() {
        return "Instance of MyClass. anInt = " + anInt;
    }
}

A felülíró metódusának neve, valamint paramétereinek száma és típusa, valamint visszatérési értéke megegyezik azzal a metódussal, amelyet felülír. (Valójában a leszármazott osztálybeli metódus visszatérési típusa lehet a szülőosztály visszatérő típusának leszármazottja is a Java 5 óta.)

A felülíró metódusnak lehet az őstől eltérő throws záradéka, ha nem ad meg olyan típusokat, melyek nincsenek a felülírt metódus záradékában előírva. Másrészt, a felülíró metódus láthatósága lehet bővebb, mint a felülírt metódusé, de szűkebb nem. Például a szülő osztály protected metódusa a leszármazott osztályban publikussá (public) tehető, de priváttá (private) nem.

Megjegyzés: Érdemes átgondolni e szabályok hátterét. Egy leszármazott osztály objektuma bárhol használható, ahol egy ősosztálybeli objektum is. Éppen ezért a leszármazott semelyik tagjának láthatósága nem szűkülhet, hiszen akkor az ilyen használat lehetetlen lenne. Ugyanígy egy felülírt metódus által dobott újfajta kivétel kezelése nem lenne biztosított.

Egy leszármazott osztály nem tudja felülírni az olyan metódusokat, melyek az ősosztályban végleges (final) minősítésű (a definíció szerint a végleges metódusok nem felülírhatók). Ha mégis megpróbálunk felülírni egy végleges metódust, a fordító hibaüzenetet küld.

Egy leszármazott osztálynak felül kell írnia azon metódusokat, melyek a felsőbb osztályban absztraktnak (abstract) nyilvánítottak, vagy maga a leszármazott osztály is absztrakt kell, hogy legyen. Emlékezzünk vissza, hogy a Java programnyelv megengedi a metódusok túlterhelését, ha a metódus paramétereinek a számát vagy típusát megváltoztatjuk. Egy ősosztályban is megengedhető a metódusok túlterhelése. Alábbiakban nézzünk egy példát a toString metódus túlterhelésére:

public class MyClass {
    private int anInt = 4;
    public String toString() {
        return "Instance of MyClass. anInt = " + anInt;
    }
    public String toString(String prefix) {
        return prefix + ": " + toString();
    }
}

Amint azt a példa illusztrálja, túlterhelhetünk egy ősosztálybeli metódust, hogy további funkciókkal is szolgálhasson. Amikor egy olyan metódus írunk, mely azonos nevű a felsőbb osztálybeli metódussal, le kell ellenőrizni a paramétereket és a kivétellistát (throws záradék), hogy biztosak lehessünk afelől, hogy a felülírás olyan lett, amilyennek akartuk.

Ha egy leszármazott osztály egy osztálymetódust ugyanazzal az aláírással definiál, mint a felsőbb osztálybeli metódus, akkor a leszármazott osztály metódusa elrejti (másként fogalmazva elfedi) a szülőosztálybelit. Nagy jelentősége van az elrejtés és a felülírás megkülönböztetésének. Nézzük meg egy példán keresztül, hogy miért! E példa két osztályt tartalmaz. Az első az Animal, melyben van egy példánymetódus és egy osztálymetódus:

public class Animal {
    public static void hide() {
        System.out.println("The hide method in Animal.");
    }
    public void override() {
        System.out.println("The override method in Animal.");
    }
}

A második osztály neve Cat, ez az Animal-nak egy leszármazott osztálya:

public class Cat extends Animal {
    public static void hide() {
        System.out.println("The hide method in Cat.");
    }
    public void override() {
        System.out.println("The override method in Cat.");
    }

    public static void main(String[] args) {
        Cat myCat = new Cat();
        Animal myAnimal = (Animal)myCat;
        myAnimal.hide();
        myAnimal.override();
    }
}

A Cat osztály felülírja az override metódust az Animal-ban, és elrejti a hide osztálymetódust az Animal-ban. Ebben az osztályban a main metódus létrehoz egy Cat példányt, beteszi az Animal típusú hivatkozás alá is, majd előhívja mind az elrejtett, mind a felülírt metódust. A program eredménye a következő:

The hide method in Animal.
The override method in Cat.

A szülőosztályból hívjuk meg a rejtett metódust, a leszármazott osztályból pedig a felülírtat. Osztálymetódushoz a futtatórendszer azt a metódust hívja meg, mely a hivatkozás szerkesztési idejű típusában van definiálva, amellyel a metódust elnevezték. A példánkban az myAnimal szerkesztési idejű típusa az Animal. Ekképpen a futtatórendszer az Animal-ban definiált rejtett metódust hívja meg. A példánymetódusnál a futtatórendszer a hivatkozás futásidejű típusában meghatározott metódust hívja meg. A példában az myAnimal futásidejű típusa a Cat. Ekképpen a futtatórendszer a Cat-ban definiált felülíró metódust hívja meg.

Egy példánymetódus nem tud felülírni egy osztálymetódust, és egy osztálymetódus nem tud elrejteni egy példánymetódust. Mindkét esetben fordítási hibát kapunk.

12.2. Tagváltozók elrejtése

Egy osztály változója, ha ugyanazt a nevet viseli, mint a felsőbb osztály egy változója, akkor elrejti a felsőbb osztály változóját, még akkor is, ha különböző a típusuk. Az leszármazott osztályokon belül a felsőbb osztálybeli változóra nem utalhatunk egyszerűen a nevével. Ehelyett a tagváltozót el tudjuk érni az ősosztályon keresztül, amiről majd a következő fejezet fog szólni. Általánosságban véve nem célszerű a tagváltozók elrejtése.

12.3. A super használata

Ha egy metódus felülírja az ősosztálya metódusainak egyikét, akkor a super használatával segítségül hívható a felülírt metódus. A super arra is használható, hogy egy rejtett tag variánsra utaljunk. Ez a szülőosztály:

public class Superclass {
    public boolean aVariable;
    public void aMethod() {
        aVariable = true;
    }
}

Most következzen a Subclass nevű leszármazott osztály, mely felülírja aMethod-ot és aVariable-t:

public class Subclass extends Superclass {
    public boolean aVariable; //hides aVariable in Superclass
    public void aMethod() { //overrides aMethod in Superclass
        aVariable = false;
        super.aMethod();
        System.out.println(aVariable);
        System.out.println(super.aVariable);
    }
}

A leszármazott osztályon belül az aVariable név a SubClass-ban deklaráltra utalt, amely a szülőosztályban deklaráltat elrejti. Hasonlóképpen, az aMethod név a SubClass-ban deklaráltra utalt, amely felsőbb osztályban deklaráltat felülírja. Tehát ha egy a szülőosztályból örökölt aVariable-ra és aMethod-ra szeretnénk utalni, a leszármazott osztálynak egy minősített nevet kell használnia, használva a super-t, mint azt láttuk. A Subclass aMethod metódusa a következőket írja ki:

false
true

Használhatjuk a super-t a konstruktoron belül is az ősosztály konstruktora meghívására. A következő kódpélda bemutatja a Thread osztály egy részét – az osztály lényegében többszálú programfutást tesz lehetővé –, amely végrehajt egy animációt. Az AnimationThread osztály konstruktora beállít néhány kezdeti értékeket, ilyenek például a keretsebesség és a képek száma, majd a végén letölti a képeket:

class AnimationThread extends Thread {
    int framesPerSecond;
    int numImages;
    Image[] images;
    AnimationThread(int fps, int num) {
        super("AnimationThread");
        this.framesPerSecond = fps;
        this.numImages = num;
        this.images = new Image[numImages];
        for (int i = 0; i <= numImages; i++) {
            ...
            // Load all the images.
            ...
        }
    }
    ...
}

A félkövérrel szedett sor a közvetlen szülőosztály konstruktorának explicit meghívása, melyet a Thread nyújt. Ez a Thread konstruktor átvesz egy String-et, és így nevezi a Thread-et. Ha a leszármazott osztály konstruktorában van explicit super konstruktorhívás, akkor annak az elsőnek kell lennie. Ha egy konstruktor nem hív meg explicit módon egy szülőosztálybeli konstruktort, akkor a Java futtatórendszer automatikusan (implicit) a szülőosztály paraméter nélküli konstruktorát hívja meg, még mielőtt a konstruktoron belül bármi utasítást végrehajtana.

Megjegyzés: ha a szülő osztályban nem áll rendelkezésre paraméter nélküli konstruktor, akkor fordítási hibát kapunk. Ilyen esetben kötelesek vagyunk explicit paraméteres konstruktorhívást alkalmazni.

12.4. Az Object osztály metódusai

Az Object osztály minden osztály közös őse, az osztályhierarchia tetején áll. Minden osztály közvetlen vagy közvetett módon utódja az Object osztálynak, így minden osztály rendelkezik az Object osztály metódusaival. Ez az osztály definiálja azt az alapvető működést, mely minden objektumnál rendelkezésre áll.
Az Object osztály által nyújtott legfontosabb metódusok a következők:

  • clone
  • equals és hashCode
  • finalize
  • toString
  • getClass

Ezeket sorra tárgyaljuk a következőkben.

12.4.1. A clone metódus

A clone metódust akkor használjuk, ha létre szeretnénk hozni egy objektumot, egy már meglévő objektumból (másolatot készíteni róla). Az adott osztállyal megegyező típusú új példányt hoz létre:

aCloneableObject.clone();

A metódus a CloneNotSupportedException kivételt dobja, ha a klónozás nem támogatott az osztály számára. A klónozás akkor támogatott, ha az osztály implementálja a Cloneable interfészt. Habár az Object tartalmazza a Clone metódust, nincsen megvalósítva az interfész. Ha az objektum, ahol a clone-ra hivatkoztunk, nem implementálja a cloneable interfészt, egy eredetivel azonos típusú és értékű objektum jön létre. Legegyszerűbb azonban, ha az osztály deklarációban létrehozunk egy implements Cloneable sort.

Bizonyos osztályoknál a helyes működés feltétele a clone felüldefiniálása. Tekintsünk egy Stack osztályt, mely tartalmaz egy tagváltozót, mely az Object-ek tömbjére hivatkozik. Ha a Stack az Object osztály clone metódusára épül, akkor az eredeti és a másolt Stack ugyanazokat az elemeket fogja tartalmazni, mivel az adattag tömb, és másoláskor csak referencia másolás fog történni.

A Stack osztálynak olyan clone implementációra van szüksége, amely lemásolja a Stack objektum adattagjait, ezzel biztosítva a megfelelő tartalom szétválasztást:

public class Stack implements Cloneable {
    private Object[] items;
    private int top;
    ...
    protected Stack clone() {
        try {
            Stack s = (Stack)super.clone(); //clone the stack
            s.items = (Object)items.clone(); //clone the array
            return s; // return the clone
        } catch (CloneNotSupportedException e) {
           //This shouldn't happen because Stack is Cloneable.
            throw new InternalError();
        }
    }
}

Az implementáció viszonylag egyszerű. Először a clone metódus Object implementációja hívódik meg a super.clone segítségével, mely létrehoz és inicializál egy Stack objektumot. Ilyenkor mindkét objektum ugyanazokat az objektumokat tartalmazza. Ezután a metódus lemásolja az objektumokat, és a metódus Stack-el tér vissza.

Megjegyzés: A clone metódus nem a new-t használja a másolat létrehozásánál és nem hív konstruktorokat, helyette a super.clone-t használja, mely létrehozza az objektumot a megfelelő típussal, és engedélyezi a másolást, minek eredményeképpen a kívánt másolatot kapjuk.

Érdemes még azt is megfigyelni, hogy az adattag másolását sem „kézzel” végezte a metódus, hanem a tömb objektum clone metódusával. Ez a metódus egy másik azonos méretű tömböt hoz létre, aminek elemeiről is másolat készül. (A tömbben tárolt tagokról már nem fog másolat készülni, de ez nem is célja egy verem másolásnak.)

12.4.2. Az equals és hashCode metódusok

Az equals metódus két objektumot hasonlít össze és dönti el, hogy egyenlők-e vagy sem (ha egyenlők, true-val tér vissza). Ha önmagával hasonlítunk egy objektumot, true-t ad vissza.

A következő programrészlet összehasonlít két Integer-t:

Integer one = new Integer(1);
Integer anotherOne = new Integer(1);
if (one.equals(anotherOne)) {
    System.out.println("objects are equal");
}

A program kimenete:

objects are equal

Egyenlők, mivel az értékük megegyezik. Ha két objektum egyenlő az equals metódus szerint, akkor a hashCode metódus által szolgáltatott értékeknek is meg kell egyezniük. (Figyelem, fordítva ez nem feltétlenül igaz!)

Ha az equals működése nem megfelelő az osztályunk számára, akkor felül kell írnunk az osztályunkban.

A hashCode metódus állítja elő az objektumok hash kódját, ami például akkor lehet szükséges, ha az objektumot hashtáblában tároljuk. Hash kódként (a metódus visszatérési értékeként) mindig egy int típusú számot kapunk.

Helyes hash függvény írása egyszerű, azonban hatékony függvény írása nehéz lehet, komolyabb munkát igényel. Ez a téma azonban már nem fér bele a jegyzetünkbe.

12.4.3. A finalize metódus

Az Object osztály ugyancsak tartalmazza a finalize metódust. A szemétgyűjtő meghívja, ha már nincs egyetlen hivatkozás sem az objektumra. A finalize metódus automatikusan meghívódik, melyet a legtöbb osztály használ, ezért nem is kell külön meghívni.

A finalize metódussal legtöbbször nem kell törődnünk, az őstől örökölt metódus többnyire megfelelően működik.

12.4.4. A toString metódus

Az objektumot String-ként ábrázolja. Hasznos minden új osztály definíciója során felülírni, hogy a megfelelő értékeket reprezentálhassa. Használhatjuk a toString-et a System.out.println-nel együtt az objektumok szöveges megjelenítésére, pl.:

System.out.println(new Double(Math.PI).toString());

A futás eredménye:

3,14159

Nagyon hasznos ez a metódus akkor, ha a program tesztelési fázisában bizonyos objektumok tartalmát ellenőrizni szeretnénk. Ilyenkor csak ki kell írni a kérdéses objektumot, például a konzolra:

System.out.println(anObject);

12.4.5. A getClass metódus

Visszaadja a futásidejű osztályát az objektumnak. Az Object osztály nem engedi meg a getClass metódus felüldefiniálását (final).

A következő metódus az objektum osztálynevét jeleníti meg:

void PrintClassName(Object obj) {
    System.out.println("The Object's class is "
                       + obj.getClass().getName());
}

A következő példa létrehoz az obj típusával megegyező másik objektum példányt:

Object createNewInstanceOf(Object obj) {
    return obj.getClass().newInstance();
}

Ha tudjuk az osztály nevét, kaphatunk egy Class objektumot az osztálynévből. A következő két sor egyaránt ugyanazon végeredményt produkálja (a második változat hatékonyabb):

String.class
Class.forName("String")

12.5. Végleges osztályok és metódusok

Végleges osztályok

A final kulcsszó segítségével deklarált változók értékét az inicializálás után nem lehet megváltoztatni, a leszármazott osztály nem módosíthatja, befolyásolhatja az eredeti működését. Fontos szempont a rendszer biztonságának növelése és az objektum orientált tervezés szempontjából.

Biztonság: Az egyik módszer, amit a hackerek használnak rendszerek feltörésénél, egy származtatott osztály létrehozása egy osztályból, majd helyettesítése az eredetivel. A származtatott osztály a metódushívás szempontjából úgy néz ki, mint az eredeti, de a viselkedése teljesen más is lehet, ami hibás működést eredményezhet. Ennek elkerülése érdekében deklarálhatjuk osztályunkat véglegessé, mely megakadályozza a származtatott osztályok létrehozását. A String osztály is végleges. Ez az osztály nélkülözhetetlen a Java Platform működéséhez. Ez biztosítja, hogy minden String a megfelelő módon működjön.

Ha megpróbáljuk lefordíttatni egy final osztály leszármazott osztályát, hibaüzenetet fogunk kapni.

Tervezés: Az objektumorientált tervezésnél érdemes megállapítani, hogy mely osztályokat szeretnénk véglegessé tenni, és tegyük is az adott osztályokat véglegessé a final módosító segítségével:

final class ChessAlgorithm {
    ...
}

Minden leszármaztatási próbálkozás hibás lesz.

Végleges metódusok

A final kulcsszót használjuk a deklarációban, ha azt akarjuk elérni, hogy ne lehessen a metódust származtatott osztályban felüldefiniálni. Az Object metódusai közül van, amelyik final típusú, és van, amelyik nem.

A következő példában a ChessAlgorithm osztályban a nextMove metódus tesszük véglegessé:

class ChessAlgorithm {
    ...
    final void nextMove(ChessPiece pieceMoved,
                        BoardLocation newLocation) {
        ...
    }
    ...
}

12.6. Ellenőrző kérdések

  • Mit jelent az, hogy egyik osztály leszármazottja a másiknak?
  • Lehet-e egy osztályreferenciát a szülőosztály felé konvertálni?
  • Lehet-e egy osztályreferenciát a leszármazott osztály felé konvertálni?
  • Lehet-e Javában különböző típusú értékek között értékadás? Ha igen, mikor?
  • Ha létrehozunk egy példányt, és egy szülőosztály típusa szerinti referenciával hivatkozunk rá, a szülőosztály vagy a leszármazott osztály szerinti metódus hívódik-e meg?
  • Mit jelent az osztályok újrafelhasználhatósága? Hogyan valósul meg Javában?
  • Az osztály mely tagjait örökli a leszármazott osztály?
  • Mikor lehet egy metódust a leszármazottban elfedni (elrejteni)?
  • Hogyan lehet hivatkozni a leszármazott osztályban az ős elrejtett adattagjára?
  • Mire használható a super kulcsszó?
  • Milyen esetben szükséges az ősosztály konstruktorát explicit meghívni?

Igaz vagy hamis? Indokolja!

  • Egy Java fordítási egységben pontosan egy osztály szerepel.
  • Bármely .class kiterjesztésű állományt lehet közvetlenül futtatni.
  • Lehet-e eltérés az ősben definiált metódus és a leszármazottban felülírt változat láthatóságában?
  • Lehet olyan metódus, amelyet egy leszármazottban nem lehet felülírni?
  • Végleges osztálynak kell-e végleges metódust tartalmazni?
  • A végleges metódust tartalmazó osztály maga is végleges?

Melyik egy publikus, absztrakt metódus helyes deklarációja?

  • public abstract void add();
  • public abstract void add() {}
  • public virtual add();

A leszármazott osztály konstruktorában hova kell írni a szülőosztály konstruktorának hívását?

  • akárhova
  • a konstruktor első sorába
  • a konstruktor utolsó sorába
  • nem kell meghívni