Java入門記(一):折騰HelloWorld


  HelloWorld,學習每門語言的第一步。有人戲稱,這些年的編程生涯就是學習各種語言的HelloWorld,不知是自謙還是自嘲。目前所在的公司使用Java作為主要開發語言,我進行語言轉換也大半年了,這HelloWorld便是語言轉換的第一關。好在本科的時候學過那么一點,而且在此之前進行了較長時間的C/C++開發,其間有不少的相似之處。這里略去JDK的安裝和環境配置(JDK為1.6.0.45),直接從代碼入手。

  首先看一個最簡單的Java下的HelloWorld:

public class HelloWorld {
  public static void main(String[]agrs)
    {
      System.out.println("HelloWorld!");
    }
}

  一般來說,初學者寫HelloWorld到這里,編譯完運行一下看到結果就可以結束了。下面對這個小程序進行更多的探索,進一步了解和學習Java編程中的特性。

 

1.源碼文件的編碼

  最初為了簡單起見,我是在Win7中用記事本編寫並保存代碼為HelloWorld.java,然后用命令行直接javac編譯。出於在Windows下寫Linux程序的習慣,我在記事本保存時將代碼保存為UTF-8編碼的HelloWorld.java文件。編譯時提示:

  在仔細檢查源代碼確定沒有任何拼寫錯誤后,嘗試將編碼改回Windows默認的ANSI,成功生成了HelloWorld.class並能夠正確運行,看來是編碼不一致惹得禍。接下來,抱着嘗試的心態,使用Unicode和Unicode Big Endian保存源碼,發現也會報錯,只是提示不同,編譯器提示有非法字符。這個問題如果在Eclipse中用默認方式保存文件,則不會出現。

  有趣的是,如果使用Java的I/O方法生成文本文件,應該如何確定文件的編碼,也是一個常見的問題。如果僅僅是涉及Windows/Linux兩個平台之間的編碼差異,而不包括中文編碼,前者使用\r\n,而后者使用\n\r或\n即可。對於漢字編碼,需要在使用到的I/O方法中指定編碼,這里不再做一步的詳述。

 

2.為什么沒有import語句?

  還記得經典的K&R中經典的HelloWorld么?即使極盡精簡,C中仍然避免不了使用#include <stdio.h>來引入頭文件,才能使用printf函數。

  而Java和C/C++不一樣,這個簡單的HelloWorld不需要類似include的import,也不需要使用命名空間,看似更簡單了些。實際上,這是因為Java給每個Java文件都默認導入了java.lang這個包,從而省去了import java.lang;這個語句罷了。這樣,下面進行屏幕輸出直接使用System.out.println()即可。

  java.lang中包括的都是常用的類和方法,具體內容讀者有興趣可以自行查閱。上文提到java.import是“默認導入”,有沒有什么辦法禁止其導入?我搜索了下,目前還沒有查到相關的資料,如果哪位讀者了解,希望能告訴我。(這可能涉及到類加載器的問題,暫未進行研究)

  如果你執意在這段簡單的代碼中使用與import對應的package,可以參考本文第六節

 

3.文件名為什么要與類名一致?類名與修飾符問題

  在實踐中可以看出,編譯結果是HelloWorld.class,但是運行的命令卻是java HelloWorld。如果這個文件還有更多的類,可以看到這些類在編譯時都生成了*.class文件。對於“類名和文件名一致”這個疑問提的並不合理,顯然代碼編寫時,一個文件中可以有很多個類。這涉及到了Java的特性(來自《Java編程思想(第四版中文版)》):

每個編譯單元(文件)只能最多有一個public類;如果有,其名稱必須與含有這個編譯單元的文件名相匹配,包括大小寫。

  如果不遵守這個要求,寫出類似下面的代碼

//ERROR IN CODE
public class HelloWorld {
  public static void main(String[]agrs)
    {
      System.out.println("HelloWorld!");
    }
}

public class HelloWorld2 {
  public static void main(String[]agrs)
    {
      System.out.println("HelloWorld, me too!");
    }
}

  那么編譯器會提示這一點

  如果把HelloWorld2類的public去掉,將使其變成包訪問權限,程序可以正常運行,此時只執行HelloWorld.main(),並不會發生沖突。

  實際上,如果這個文件只有一個HelloWorld類,或者有兩個類,只要這個包括main()的類名與文件名一致,類名前不加public也是可以正常運行的,且調用的是與文件名一致的類的main()方法。但個人認為這不是良好的編程實踐,如下:

class HelloWorld {
  public static void main(String[]agrs)
    {
      System.out.println("HelloWorld!");
    }
}

class HelloWorld2 {
  public static void main(String[]agrs)
    {
      System.out.println("HelloWorld, me too!");
    }
}

   編譯時將生成HelloWorld.class和HelloWorld2.class,分別運行時,結果為兩個類各自的main()方法。

 

4.main()函數的參數表和修飾符

  在C中,對於main()的修飾符和參數表有着很多細節要注意(可以參考五花八門的main())。對於Java,這里對main()的寫法也進行簡單的探究。

  先來看參數表String args[]。雖說編譯器要求必須是這種形式,但如果不用標准形式而用其他形式如int x、String s作為參數表,編譯是可以通過的,但是在執行時則會拋出異常,無論是否提供了參數:

  NoSuchMethodError表名,期望的是參數為String args[]的main()方法。雖然提供了同名方法,由於方法的重載機制,並不能代替期望的main(String args[])方法。

  接下來看修飾符public。在第3條已經提到了public對於類名的修飾有所說明,而對於main()這個與文件同名的類的成員方法,為了能被調用,只能用public修飾。不使用修飾符(包訪問權限)、使用private或protected都會提示:

  對於修飾符static,表明這個方法是在存儲在靜態存儲區的,不需要實例化對象就可以調用。去掉static后,可以編譯通過,運行時提示

為了進一步驗證這一點,可以編寫構造方法來驗證。(構造方法是在類的對象在實例化時會被調用的方法)

public class HelloWorld {
    HelloWrold
    {
        System.out.println("Constructor");
    }
    public static void main(String[]agrs)
    {
      System.out.println("HelloWorld!");
    }
}

  編譯運行時,可以看到構造方法並沒有運行。

  對於修飾符void,也是必須的。改成int等並加上對應的return語句同樣會提示“NoSuchMethodError: main”。在《Java虛擬機規范(JavaSE7)》(周志明等譯)中介紹到

Java虛擬機的啟動是通過引導類加載器(Bootstrap Class Loader §5.3.1)創建一個初始類(Initial Class)來完成,這個類是由虛擬機的具體實現指定。緊接着,Java虛擬機鏈接這個初始類,初始化並調用它的public void main(String[])方法。之后的整個執行過程都是由對此方法的調用開始。

  可見,void返回值也是被要求的,其他形式是不允許的。

  經過進一步的測試可知,args[0]是第一個參數;而在C中,argv[0]是執行的程序名。 

 

5.既然main()方法是類方法……

  既然main()方法是類方法,那么在實例化這個類的對象時,自然可以再次調用這個方法。對HelloWorld源代碼加對應的兩行,如下所示

public class HelloWorld {
  public static void main(String[] args)
    {
        HelloWorld h = new HelloWorld();
        System.out.println("HelloWorld!");
        h.main(args);
    }
}

運行結果為

HelloWorld!

HelloWorld!

HelloWorld!

... ...
HelloWorld!
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.ext.DoubleByteEncoder.encodeLoop(Unknown Source)
at java.nio.charset.CharsetEncoder.encode(Unknown Source)
at sun.nio.cs.StreamEncoder.implWrite(Unknown Source)
at sun.nio.cs.StreamEncoder.write(Unknown Source)
at java.io.OutputStreamWriter.write(Unknown Source)
at java.io.BufferedWriter.flushBuffer(Unknown Source)
at java.io.PrintStream.write(Unknown Source)
at java.io.PrintStream.print(Unknown Source)
at java.io.PrintStream.println(Unknown Source)
at main.HelloWorld.main(HelloWorld.java:15)
at main.HelloWorld.main(HelloWorld.java:16)
at main.HelloWorld.main(HelloWorld.java:16)
at main.HelloWorld.main(HelloWorld.java:16)
... ...

  可見HelloWorld被玩壞了,這個無限遞歸創建對象的過程導致了內存溢出。

 

6.試試package

  當然,使用java更多的時候往往要處理多個文件。為了組織同一命名空間下的文件,需要使用包來進行。對應於import,為了指定當前文件在哪個包,需要加上package語句。隨便加上一個包名,最初的代碼變成了

package test;

public class HelloWorld {
  public static void main(String[]agrs)
    {
      System.out.println("HelloWorld!");
    }
}

編譯后,卻無法運行,如下圖所示

  其實,包名是隱含目錄結構的。為了運行,需要把HelloWorld.class移入這個路徑的test文件夾,按照下面的方式運行才可以:

   (2015.10.6更新)如果引用了三方jar包,可以在運行javac和java命令的時候使用-cp指定jar包所在相對路徑,或者直接把jar包放在該class文件所在目錄或環境變量CLASSPATH指定的目錄下。

小結

  可見,對於一個小小的HelloWorld,還是有不少東西可以發掘,只是限於篇幅和本人水平,本文僅僅進行了簡要的介紹。以下是本文提出的可以在后續學習中繼續深入的主題,僅供參考:

1.I/O方法編碼方式的選擇

2.包和代碼組織

3.Java虛擬機(JVM)

 

相關閱讀

    深入理解Java HelloWorld

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM