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.
at AB$B.
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?