Java konstruktor hívási sorrend

Rég írtam, valóban. Hogy újra rávezessem magam a kedvenc munkán kívüli elfoglaltságomhoz, most egy egyszerűbb témát vetek fel, ami mégis okozott pár kobak-koppanást az asztalon. Nevezetesen arról szándékozom írni, hogy a Java hogyan sorrendezi egy objektum inicializálásában résztvevő kódokat. Mert bizony több lehet, és nem egyértelmű a lefutási sorrend, ahogy azt a követkető kódrészlet szemlélteti:


public class AB{
public static abstract class A{
public A(){
doSomething();
}
protected abstract void doSomething();
}

public static class B extends A{
final String s = "1234";
final Object o = "1234";
public B(){
super();
}
@Override
protected void doSomething(){
System.out.println(s.getClass());
System.out.println(o.getClass());
}
}

public static void main(String[] args){
new B();
}
}

A példát igyekeztem egyszerűnek tartani, de talán megérdemel egy rövid magyarázatot: Két osztályt definiáltam, az egyik leszármazottja a másiknak. Érdekesség még, hogy az ős konstruktora kódot hív meg a leszármazottból absztrakt metóduson keresztűl. Egy “B” típusú objektum létrehozásához láthatóan három helyről kell kódot hívni: Az “A” és a “B” konstruktora, továbbá a “B” osztályban lévő, konstruktoron kívüli inicializálás. A probléma ezen három kódrészlet lefutásának sorrendje.

Első ránézésre a kóddal semmi probléma nincs azon kívül, hogy teljesen haszontalan. Mindkét mező final, így gondolnánk a konstruktor elött inicializálódnak, tehát nem okozhat problémát. Azonban a kód futtatásakor egy csinos NullPointerException kacsint vissza ránk. A futás kimenete:


class java.lang.String
Exception in thread "main" java.lang.NullPointerException
at AB$B.doSomething(AB.java:22)
at AB$A.(AB.java:8)
at AB$B.
(AB.java:17)
at AB.main(AB.java:27)

Láthatóan a B.doSomething() első sora lefut hiba nélkül, a probléma utána keletkezik. Tehát az “s” változó már létezik, az “o” viszont nem. A konstruktorok lefutásának sorrendje tehát: s=..,A(),o=..,B(). A két mező között csupán az a különbség, hogy az “s” mező típusa megegyezik a futásidejű értékének típusával. Ez meghatározza, hogy a mező a szülő osztály konstruktora előtt vagy után kap értéket. Érdemes kipróbálni, ha az “s” mező elöl kivesszük a final kulcsszót, az azt okozza, hogy az is a szülő konstruktor hívása után kap értéket. Vajon hogy határozódik meg a pontos sorrend, mitől függ, hogy hova ütemezi be a java a mezők értékadását?

Nyílván valahogy igyekszik meghatározni, hogy az adott értékadás kiértékelhető-e az osztály örökölt részének inicializálása nélkül vagy sem. Ennek tesztelésére tettem még egy kísérletet, kicsit módosítva a példát:


public class AB{
public static abstract class A{
protected String a;
public A(){
a = "abc";
doSomething();
}
protected abstract void doSomething();
}

public static class B extends A{
final String s = "1234"+a;
final Object o = "1234";
public B(){
super();
}
@Override
protected void doSomething(){
System.out.println(s.getClass());
System.out.println(o.getClass());
}
}

public static void main(String[] args){
new B();
}
}

Ez a kód is szépen elszáll, méghozzá a B.doSomething() első sorában! Azaz, az explicit hivatkozás egy szülő objektum-beli elemre azt eredményezi, hogy a mező értékadását a szülő konstruktor utánra ütemezi. Elég intelligensnek tűnik a dolog, de mégis beleütközik az ember, mert másra számít.

Persze elkerülhető a dolog ha a konstruktorból hívott absztrakt metódusokat anti-pattern-nek kiáltjuk ki, bár gyakran jól jön. Mégis pontosan mi határozza meg a sorrendet, és lehet-e befolyásolni valahogyan? Van valaki aki nálam sikeresebben guglizott?

4 thoughts on “Java konstruktor hívási sorrend”

  1. A második példád elvileg is lehetetlen, hogy lefusson – kivéve, ha a Java megszakítaná a konstruktor futását.

    Ugyanis az s változó értékét nem tudja kiszámolni, amíg az ősosztály konstruktora nem futott le (abban kap értéket), míg az absztrakt hívás használná (ami az A konstruktorában kap helyet).

    Az első példádnál meg szerintem az történik, hogy az s változó értékét behelyettesíti a konstans “1234” stringgel, és azt helyettesíti be minden használatnál – efféle optimalizációról konkrétan hallottam.

    Mindenesetre követtem debuggerrel a program futását, és úgy egy módosított lefutási sorrenddel találkoztam (kikapcsolva az absztrakt hívást):
    belépés B konstruktorába -> super hívás -> belépés A konstruktorába -> a lokális változóinak inicializálása (felvettem) -> A konstruktorának lefutása -> B lokális változóinak inicializálása -> B konstruktorának futtatása

    Ezt én úgy értelmezem, hogy ez a normál futtatási sorrend (annak tűnik számomra), és az, hogy az első példádnál mégsem az első, hanem csak a második hívásra szállt el, az az én szememben compiler optimalizációnak néz ki…

  2. Valóban, ez megmagyarázza a jelenséget. Őszintén, eszembe se jutott, hogy a ritka esetek, mikor a dolog működik az csak a compiler optimalizáció mellékhatása.

  3. Utólag villant be a kérdés: miért van szükség erre az absztrakt metódusra?

    Tudjuk, hogy egyszerűen az ősosztály konstruktora után meghívódik a leszármazotté, és akkor lehet kezelni a megfelelően specifikus dolgokat.

    Vagy ha tényleg roppantmód indokolt az absztrakt metódus, akkor pedig gondoskodni kell róla, hogy a metódus inicializálja a használt dolgait…

  4. nos, mondhatjuk, hogy roppantmód indokolt a dolog, illetve így egyszerűbb. A meghívott metódus visszatérési értéke kell az ősosztály konstruktorának.

    Igen, odafigyeléssel elkerülhető a dolog, de megtévesztő a szintaxis.

Leave a Reply