蔡學鏞專欄:全世界所有程式員都會犯的錯誤

來源:互聯網
上載者:User

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

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

一般來說,越高階的程式語言,會提供越多文法上的便利,以方便程式撰寫,這就俗稱為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和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」的部分刪除。這種作法只有在希望
-方法三:將初始化的動作搬到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的書卻鮮少這麼做。

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

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

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.