Citat:
Nu är jag som vanligt sen i diskussionen, men ifall du ville ha mer åsikter och förtydligande vad som händer, så har jag följande bidrag.Sitter med ett litet problem när jag ska skapa en "sentinel" i mitt binära träd. Istället för att löven ska ha null-pekare så ska löven peka på en "sentinel". Men jag är lite osäker på om ett objekt kan/ska refererera tillbaks till sig själv på detta sätt redan i konstruktorn. Det verkar fungera men skulle vilja ha lite åsikter på min lösning.
TL;DR-svaret på din fråga är att du är garanterad att tilldelningen av left och right blir null för just det fall du undrar över.
Det längre svaret är att framförallt 2 regler i Java är intressanta för vad som händer här:
R1. Definite assignment rule
R2. Instansvariabler är aldrig oinitierade (vänta med att protestera)
För att förtydliga och resonera runt detta så har jag klippt ned din kod med bara de relevanta delarna och lagt till lite så man kan testköra och se vad som händer också. En rejäl fulmanöver har jag gjort också med klassen S nedan, något som jag aldrig skulle göra annat än i labbkod, men det hjälper för att påvisa vad som sker när man testkör.
Kod:
class S { // Just for inspection purpose! S() { // Never ever EVER code like this! System.out.println("1. nil: " + ((Tree)this).nil); } } public class Tree extends S { Node nil = new Node(); // T1 Tree() { // T2 System.out.println("3. nil: " + nil); } class Node { // Note: Inner class, not nested! Node n; Node() { System.out.println("2. nil: " + nil + "\n (this: " + this + ", outer: " + Tree.this + ")"); n = nil; // T3 } } public static void main(String[] args) { new Tree(); // Node n; // T4 // System.out.println("4. n: " + n); // Violates definite assignment rule. } }
T1: Detta är en instansvaribel till Tree med en sk "instance initializer expression". Det senare innebär att initieringsuttrycket kommer utföras som första steg i varje existerande (även eventuell implicit) konstruktor för Tree före dess kodkropp, dvs vid T2 i exemplet.
Notera att initieringsuttrycket körs i sin helhet först och sen sker tilldelningen till vänsterledet. Det får ju effekten i ditt fall att Node-konstruktorn vid T3 refererar till Trees instansvariabel (via outer this) nil innan denna tilldelning (instance initializer) har skett.
Det känns ju som R1 borde träda in här då, som förbjuder all evaluering av variabler som inte är garanterat initierade. Kompilatorn måste enligt Javas språkdefinition söka av alla möjliga exekveringsvägar för att utesluta att detta inte kan ske på något sätt. Kan den inte utesluta detta så måste denna slå ned på det med kompileringsfel.
Men, R2 to the rescue i detta fall. Instansvariabler får alltid ett värde, även före initieringsuttryck i konstruktorer och initializers, deras sk defaultvärden. Det är i princip värdet 0 men i dess motsvarande form för icke taltyper, dvs false för boolean och null för objektreferenser.
R2 garanterar alltså att variabeln nil har värdet null vid T3 och kompilatorn ser att R1 är uppfyllt.
För att se lite tydligare när initializer expression och initieringen till nil sker, så gjorde jag en fulmanöver med klassen S som Tree ärver från samt några numrerade utskrifter av nil i olika skeden av initieringen.
En testkörning ger:
Kod:
1. nil: null 2. nil: null (this: Tree$Node@15db9742, outer: Tree@6d06d69c) 3. nil: Tree$Node@15db9742
Som synes körs superklassens konstruktor först (oavsett implicit eller explicit super()-anrop) där man kan se (genom grotesk kodmissbruk) att nil har värdet null (sitt default-state). Sedan sker "initializer expression" och vi ser därmed utskriften från Node-konstruktorn och nil är som väntat null fortfarande. Efter det att hela initializer expression är klar (med tilldelningen också) körs kodkroppen i Trees konstruktor och vi får den tredje utskriften där vi ser att nil nu refererar till ett Node-objekt. Utskriften av this i Node-konstruktorn bekräftar också att det är samma objekt som skapades vid det tillfället.
Om någon då undrar när R1 slår till så är enklaste exemplet stackvariabler. Se T4 och de två bortkommenterade raderna där. Ta bort kommentarerna så ser man att kompilatorn aldrig kommer släppa igenom användandet av n så länge den inte är initierad.
Sen ett litet sidospår som jag inte vet om du känner till eller inte. Var det meningen att Node skulle vara en "inner class" och inte en "nested class"? Det är omöjligt att avgöra på det lilla du postat vad intentionen är, men eftersom det är så vanligt att många nya i Java av misstag använder inner när de ville ha nested, så frågar jag. I just det lilla exempel du har så kommer du förvisso inte åt NIL om den är nested, men om Node påtvingas typkännedom till Tree endast för att lösa åtkomsten till en specifik instans av Node i ditt system, så tycker jag det finns bättre lösningar. Min egen preferens är att alltid minimera typberoenden i alla system, oavsett storlek. Det betalar sig nästan alltid med tiden.