自己收集的,全世界所有程序員都會犯的錯誤

當年,國際巨星成龍的「龍種」曝光,衆人指責他對不起嬌妻林鳳嬌,逼得他出面召開記
者會,向世人自白他犯了「全世界所有男人都會犯的錯誤」。從來沒犯過這種錯誤的我,
也因此常常認爲自己不是個男人。 

雖然沒犯過「全世界所有男人都會犯的錯誤」,但是我倒是曾經犯了「全世界所有程序員
都會犯的錯誤」。不管使用何種語言,全世界所有程序員都一定犯過這種錯誤,那就是:
太依賴編譯器,卻不知道編譯器做了哪些事。 

一般來說,越高階的程序語言,會提供越多語法上的便利,以方便程序撰寫,這就俗稱爲
syntactic sugar,我稱其爲「語法上的甜頭」。雖說是甜頭,但是如果你未能瞭解該語法
的實質內涵,很可能會未嘗甜頭,卻吃盡苦頭。 

不久前,我收到一個電子郵件,讀者列出下面的Java程序,向我求救。看過這個程序之後
,我確定這又是一個「全世界所有程序員都會犯的錯誤」。 

// 程序1 

class Singleton { 
private static Singleton obj = new Singleton();
 public static int counter1; 
public static int counter2 = 0;
 private Singleton()
{  
 counter1++;  
 counter2++; 
}
 public static Singleton getInstance()
{  
return obj;
 }
 }

 // 程序2 
public class MyMain {
 public static void main(String[] args) {   
Singleton obj = Singleton.getInstance();  
 System.out.println("obj.counter1=="+obj.counter1);  
 System.out.println("obj.counter2=="+obj.counter2); 
}
 } 

執行結果是: 
obj.counter1==1 
obj.counter2==0 

你有沒有被此結果嚇一跳?乍看程序代碼,你很可能會認爲counter1和counter2的值一定
會相等,但執行結果顯然不是如此。其實,程序1被編譯後的程序應該等同於下面的程序3
: 

// 程序3  class Singleton

private static Singleton obj;
 public static int counter1; 
public static int counter2; 
static {
 // 這就是class constructor   
// 在進入此class constructor之前,class已經被JVM 
  // 配置好內存,所有的static field都會被先設定爲0,  
 // 所以此時counter1和counter2都已經是0,且singleton爲null  
 obj = new Singleton();
// 問題皆由此行程序產生  
 // counter1不會在此被設定爲0   
counter2 = 0;
// counter2再被設定一次0(其實是多此一舉)
  }
 private Singleton()
{
// 這是instance constructor    counter1++;  
 counter2++; 

public static Singleton getInstance()
 {    return obj;  } 

這是因爲:當class具有static field,且直接在宣告處透過「=...」的方式設定其值時,
編譯器會自動將這些敘述依序搬到class constructor內。同樣地,當class具有instance
field,且直接在宣告處透過「=...」的方式設定其值時,編譯器會自動將這些敘述依序
搬到instance constructor內。 

此程序在class constructor內,還未將static field初始化時(這時候,counter1和cou
nter2都是0),就呼叫instance constructor,而instance constructor竟然還會去更動
static field的值,使得counter1和counter2都變成1。然後instance constructor執行完
,回到class constructor,再把counter2的值設爲0(但是 
counter1維持不變)。最後的結果:counter1等於1,counter2等於0。 

欲改正程序1,方法有三: 

-方法一:將singleton field的宣告調到counter1與counter2 field之後。 
         這是最好的作法。 
-方法二:將counter2=0的宣告中,「=0」的部分刪除。這種作法只有在希望 
-方法三:將初始化的動作搬到class constructors內,自行撰寫,而不依賴 
         編譯器產生。這是最保險的作法。 

如何避免犯下「全世界所有程序員都會犯的錯誤」,我給各位Java程序員 
的建議是: 
-熟讀Java Language Specification 
-在有疑問時,使用J2SDK所提供的javap來反組譯Java Bytecode,直接觀察 
編譯後的結果。 

下面是我用javap來反組譯程序1的示範: 

C:/>javap -c -classpath . Singleton  Compiled from MyMain.java  class Singleton extends java.lang.Object {    public static int counter1;    public static int counter2;    public static Singleton getInstance();    static {};  }  Method Singleton()   0 aload_0   1 invokespecial #1 <Method java.lang.Object()>   4 getstatic #2 <Field int counter1>   7 iconst_1   8 iadd   9 putstatic #2 <Field int counter1>  12 getstatic #3 <Field int counter2>  15 iconst_1  16 iadd  17 putstatic #3 <Field int counter2>  20 return  Method Singleton getInstance()   0 getstatic #4 <Field Singleton obj>   3 areturn  Method static {}   0 new #5 <Class Singleton>   3 dup   4 invokespecial #6 <Method Singleton()>   7 putstatic #4 <Field Singleton obj>  10 iconst_0  11 putstatic #3 <Field int counter2>  14 return 
其實Java的syntactic sugar並不算多,C#的syntactic sugar才真的是無所不在, 
也因此C#的初學者更容易犯了「全世界所有程序員都會犯的錯誤」。許多C#的書都會一邊
介紹C#語法,一邊介紹編譯之後MSIL(.NET的中間語言,類似Java的Bytecode)的結果,
然而Java的書卻鮮少這麼做。 

雖說是「全世界所有程序員都會犯的錯誤」,但是這不代表你犯了此錯誤之後,仍可以同
愛借錢的曹啓泰一般地「擡頭挺胸、理直氣壯」。只要有心,其實這一類的錯誤仍是可以
避免的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章