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)
相關閱讀