天天看點

Java向前引用容易出錯的地方

     所謂向前引用,就是在定義類、接口、方法、變量之前使用它們,例如,

class MyClass
{
    void method()
    {
        System.out.println(myvar);
    }
    String myvar = "var value";
}      

      myvar在method方法後定義,但method方法可以先使用該變量。在很多語言,如C++,是需要提前定義的,而Java已經允許了向前引用。不過在使用向前引用時可能會容易犯一些錯誤。例如,下面的代碼。

class MyClass {
     int method() {return n; }
     int m = method();
     int n = 1;
}      

       如果簡單地執行下面的代碼,毫無疑問會輸出1.

System.out.println(new MyClass().method());      

       不過使用下面的代碼輸出變量m,卻得到0。

System.out.println(new MyClass().m);      

    那麼這是真麼回事呢?

   實際上,從java編譯器和runtime的工作原理可以得知。在編譯java源代碼時隻是進行了詞法、文法和語義檢測,如果都通過,會生成.class檔案。不過這時MyClass中的變量并沒有被初始化,編譯器隻是将相應的初始化表達式(method()、1)記錄在.class檔案中。

   當runtime運作MyClass.class時,首先會進行裝載成員字段,而且這種裝載是按順序執行的。并不會因為java支援向前引用,就首先初始化所有可以初始化的值。首先,runtime會先初始化m字段,這時當然會調用method方法,在method方法中利用向前引用技術使用了n。不過這時的n還沒有進行初始化呢。runtime為了實作向前引用,在進行初始化所有字段之前,還需要将所有的字段添加到符号表中。以便在任何地方(但需要滿足java的調用規則)都可以引用這些字段,不過由于還沒有初始化這些字段,是以這時符号表中所有的字段都使用預設的值。int類型的字段預設值自然是0了。是以在初始化int m = method()時,method方法通路的n實際上是在進行正式初始化之前已經被添加到符号表中的字段n,而不是後面的int n = 1執行的結果。但将MyClass改成如下的形式,結果就完全不同了。

class MyClass {
    int method() {return n; }
    int n = 1;
    int m = method();
}      

現在執行下面的代碼,會輸出1。

System.out.println(new MyClass().m);      

    究其原因,是引用初始化m時調用method方法,該方法中使用的n已經是初始化完的了,而不是最初放到符号表中的值。

MyClass myClass = new MyClass();
System.out.println(myClass.n);  //  輸出1
System.out.println(myClass.m);  //  仍然輸出0      
class MyClass {
     static int method() {return n; }
     static int m = method();  //  直接通路m,仍然會輸出0
     static int n = 1;
}