首頁
中文書目錄
原文書目錄
 站內快速搜尋
資源中心
Book Series
Special Interest













■好消息,歐萊禮書籍已重新鋪貨至各大書局及網路書店,歡迎讀者選購       ■歡迎各院校採用歐萊禮書籍,學校團購請洽校園服務團隊

[技術短文]

全世界所有程式員都會犯的錯誤


當年,國際巨星成龍的「龍種」曝光,眾人指責他對不起嬌妻林鳳嬌,逼得他出面召開記者會,向世人自白他犯了「全世界所有男人都會犯的錯誤」。從來沒犯過這種錯誤的我,也因此常常認為自己不是個男人。
雖然沒犯過「全世界所有男人都會犯的錯誤」,但是我倒是曾經犯了「全世界所有程式員都會犯的錯誤」。不管使用何種語言,全世界所有程式員都一定犯過這種錯誤,那就是:太依賴編譯器,卻不知道編譯器做了哪些事。

一般來說,越高階的程式語言,會提供越多語法上的便利,以方便程式撰寫,這就俗稱為 syntax 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 和 counter2 都是 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」的部分刪除。這種作法只有在希望 counter2 的初始值是 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 的 syntax sugar 並不算多,C# 的 syntax sugar 才真的是無所不在,也因此 C# 的初學者更容易犯了「全世界所有程式員都會犯的錯誤」。許多 C# 的書都會一邊介紹 C# 語法,一邊介紹編譯之後 MSIL(.NET 的中間語言,類似 Java 的 Bytecode)的結果,然而 Java 的書卻鮮少這麼做。

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


本文作者:蔡學鏞
文章出處:Sleepless 2.0
張貼日期:03/10/2003

| 首頁 | 聯絡我們 |
© 2009, O'Reilly Media, Inc. Taiwan Branch