1. 程式人生 > >java編譯的莫名奇妙問題總結 .

java編譯的莫名奇妙問題總結 .

Java很誘人,但對於剛跨入Java門檻的初學者來說,編譯並執行一個無比簡單的Java程式簡直就是一個惡夢。明明程式沒錯,但各種各樣讓人摸不著頭腦的錯誤資訊真的讓你百思不得其解,許多在Java門口徘徊了很久的初學者就這樣放棄了學習Java的機會,很是可惜。筆者也經歷過這個無比痛苦的階段,感覺到編譯難的問題就出在classpath的設定及對package的理解之上。本文以例項的方式,逐一解決在編譯過程中所出現的各種classpath的設定問題。本文例項執行的環境是在Windows XP + JDK 1.5.0。對其他的環境,讀者應該很容易進行相應的轉換。

1. 下載並安裝JDK1.5.0,並按預設路徑,安裝到C:/Program Files/Java/jdk1.5.0中。

2. 用滑鼠單擊WindowsXP的“開始”->“執行”,在彈出的執行視窗中輸入cmd,按確定或回車,開啟一個命令列視窗。

3. 在命令列中輸入:

java

有一列長長的洋文滾了出來,這是JDK告訴我們java這個命令的使用方法。其中隱含了一個重要資訊,即JDK安裝成功,可以在命令列中使用java此命令了。

4. 在命令列中輸入

javac

螢幕顯示:

'javac' 不是內部或外部命令,也不是可執行的程式或批處理檔案。

這是由於windows找不到javac這個命令的原因。這就不明白了,java與javac都是JDK在同一個子目錄裡面的兩個檔案,為什麼可以直接執行java而不能直接執行javac呢?原來,Sun公司為了方便大家在安裝完JDK後馬上就可以執行Java類檔案,在後臺悄悄地將java命令加入了Path的搜尋路徑中
,因此我們可以直接執行java命令(但我們是看不到它到底是在哪設定的,無論是在使用者的Path或系統的Path設定中均找不到這個java存放的路徑)。但Sun所做的到此為止,其他JDK的命令,一概不管,需要由使用者自己新增到搜尋路徑中。

5. 既然如此,那我們自己新增Path的搜尋路徑吧。對“我的電腦”按右鍵,選“屬性”,在“系統屬性”視窗中選“高階”標籤,再按“環境變數”按鈕,彈出一個“環境變數”的視窗,在使用者變數中新建一個變數,變數名為“Path”,變數值為"C:/Program Files/Java/jdk1.5.0/bin;%PATH%"。最後的%PATH%的意思是說,保留原有的Path設定,且將目前的Path設定新加到其前面。
一路按“確定”退出(共有3次)。關掉原來的命令列視窗,依照第2步,重新開啟一個新的命令列視窗。在此視窗中輸入

javac

長長的洋文又出現了,這回是介紹javac的用法。設定成功。

6. So far so good. 到目前為止,我們已經可以程式設計了。但是,這不是一個好辦法。因為隨著以後我們深入學習Java,我們就會用到JUnit、Ant或NetBeans等應用工具,這些工具在安裝時,都需要一個名為指向JDK路徑的“JAVA_HOME”的環境變數,否則就安裝不了。因此,我們需要改進第5步,為以後作好準備。依照第5步,彈出“環境變數”的視窗,在使用者變數中新建一個變數,變數名為“JAVA_HOME”,變數值為"C:/Program Files/Java/jdk1.5.0"。注意,這裡的變數值只到jdk1.5.0,不能延伸到bin中。確定後,返回“環境變數”的視窗,雙擊我們原先設定的Path變數,將其值修改為“%JAVA_HOME%/bin;%PATH%”。這種效果與第5步是完全一樣的,只不過多了一個JAVA_HOME的變數。這樣,以後當我們需要指向JDK的路徑時,只需要加入“%JAVA_HOME%”就行了。至此,Path路徑全部設定完畢。一路確定退出,開啟新的命令列視窗,輸入

javac

如果長長的洋文出現,Path已經設定正確,一切正常。如果不是,請仔細檢查本步驟是否完全設定正確。

7. 開始程式設計。在C盤的根目錄中新建一個子目錄,名為“JavaTest”,以作為存放Java原始碼的地方。開啟XP中的記事本,先將其儲存到JavaTest資料夾中,在“檔名”文字框中輸入"Hello.java"。注意,在檔名的前後各加上一個雙引號,否則,記事本就會將其存為"Hello.java.txt"的文字檔案。然後輸入以下程式碼:

public class Hello {
   public static void main(String[] args) {
     System.out.println("Hello, world");
   }
}

再次儲存檔案。

8. 在命令列視窗中輸入

cd C:/JavaTest

將當前路徑轉入JavaTest中。然後,輸入

javac Hello.java

JDK就在JavaTest資料夾中編譯生成一個Hello.class的類檔案。如果出現“1 error”或“XX errors”的字樣,說明是原始碼的輸入有誤,請根據出錯提示,仔細地按第7步的程式碼找出並修正錯誤。請讀者注意甄別程式碼輸入有誤的問題與classpath設定有誤的問題。因為本文是關於如何正確設定classpath及package的,因此,這裡假設讀者輸入的程式碼準確無誤。到目前為此,由於我們是在原始碼的當前路徑下編譯,因此,不會出現classpath設定有誤的問題。

9. 在命令列視窗中輸入

java Hello

螢幕出現了

Hello world

成功了,我們已經順利地編譯及運行了第一個Java程式。
但是,第8步及第9步是不完美的,因為我們是在JavaTest這個存放原始碼的資料夾中進行編譯及執行的,因此,一些非常重要的問題並沒有暴露出來。實際上,第8步的“javac Hello.java”及第9步的“java Hello”涉及到兩個問題,一是作業系統如何尋找“javac”及“java”等命令,二是作業系統如何尋找“Hello.java”及“Hello.class”這些使用者自己建立的檔案。對於“javac”及“java”等命令,由於它們均是可執行檔案,作業系統就會依據我們在第6步中設定好的Path路徑中去尋找。而對於“Hello.java”及“Hello.class”這些檔案,Path的設定不起作用。由於我們是在當前工作路徑中工作,java及javac會在當前工作路徑中尋找相應的java檔案(class檔案的尋找比較特殊,詳見第11步),
因此一切正常。下面我們開始人為地將問題複雜化,在非當前工作路徑中編譯及執行,看看結果如何。

10. 在命令列視窗中輸入

cd C:
轉入到C盤根目錄上,當前路徑離開了存放原始碼的工作區。輸入

javac Hello.java

螢幕出現:

error: cannot read: Hello.java
1 error

找不到Hello.java了。我們要給它指定一個路徑,告訴它到C:/JavaTest去找Hello.java檔案。輸入

javac C:/JavaTest/Hello.java

OK,這回不報錯了,編譯成功。

11. 輸入

java C:/JavaTest/Hello

這回螢幕出現:

Exception in thread "main" java.lang.NoClassDefFoundError: C:/JavaTest/Hello

意思為在“C:/JavaTest/Hello”找不到類的定義。明明C:/JavaTest/Hello是一個.class檔案,為什麼就找不到呢?原來,Java對待.java檔案與.class檔案是有區別的。對.java檔案可以直接指定路徑給它,而java命令所需的.class檔案不能出現副檔名,也不能指定額外的路徑給它。

那麼,如何指定路徑呢?對於Java所需的.class檔案,必須通過classpath來指定。

12. 依照第5步,彈出“環境變數”視窗,在使用者變數中新建一個變數,變數名為“classpath”,變數值為"C:/JavaTest"。一路按“確定”退出。關閉原命令列視窗,開啟新的命令列視窗,輸入

java Hello

“Hello world”出來了。由此可見,在“環境變數”視窗中設定classpath的目的就是告訴JDK,到哪裡去尋找.class檔案。這種方法一旦設定好,以後每次執行java或javac時,在需要呼叫.class檔案時,JDK都會自動地來到這裡尋找。因此,這是一個全域性性的設定。

13. 除了這種在環境變數”視窗中設定classpath的方法之外,還有另一種方法,即在java命令後面加上一個選項classpath,緊跟著不帶副檔名的class檔名。例如,

java -classpath C:/JavaTest Hello

JDK遇到這種情況時,先根據命令列中的classpath選項中指定的路徑去尋找.class檔案,找不到時再到全域性的classpath環境變數中去尋找。這種情況下,即使是沒有設定全域性的classpath環境變數,由於已經在命令列中正確地指定類路徑,也可以執行。

為了在下面的例子中更好地演示classpath的問題,我們先將全域性的classpath環境變數刪除,而在必要時代之以命令列選項-classpath。彈出“環境變數”視窗,選中“classpath”的變數名,按“刪除”鍵。

此外,java命令中還可以用cp,即classpath的縮寫來代替classpath,如java -cp C:/JavaTest Hello。特別注意的是,JDK 1.5.0之前,javac命令不能用cp來代替classpath,而只能用classpath。而在JDK 1.5.0中,java及javac都可以使用cp及classpath。因此,為保持一致,建議一概使用classpath作為選項名稱。

14. 我們再次人為地複雜化問題。關閉正在編輯Hello.java的記事本,然後將JavaTest資料夾名稱改為帶空格的“Java Test”。在命令列中輸入

javac C:/Java Test/Hello.java

長長的洋文又出來了,但這回卻是報錯了:

javac: invalid flag: C:/Java

JDK將帶有空格的C:/Java Test分隔為兩部分"C:/Java"及"Test/Hello.java",並將C:/Java視作為一個無效的選項了。這種情況下,我們需要將整個路徑都加上雙引號,即

javac "C:/Java Test/Hello.java"

這回JDK知道,引號裡面的是一個完整的路徑,因此就不會報錯了。同樣,對java命令也需要如此,即

java -classpath "C:/Java Test" Hello

對於長檔名及中文的資料夾,XP下面可以不加雙引號。但一般來說,加雙引號不容易出錯,也容易理解,因此,建議在classpath選項中使用雙引號。

15. 我們再來看.java檔案使用了其他類的情況。在C:/Java Test中新建一個Person.java檔案,內容如下:

public class Person {
   private String name;

   public Person(String name) {
     this.name = name;
   }

   public String getName() {
     return name;
   }
}

然後,修改Hello.java,內容如下:

public class Hello {
   public static void main(String[] args) {
     Person person = new Person("Mike");
     System.out.println(person.getName());
   }
}

在命令列輸入

javac "C:/Java Test/Hello.java"

錯誤來了:

C:/Java Test/Hello.java:3: cannot find symbol
symbol: class Person

JDK提示找不到Person類。為什麼javac "C:/Java Test/Hello.java"在第14步中可行,而在這裡卻不行了呢?第14步中的Hello.java檔案並沒有用來其他類,因此,JDK不需要去尋找其他類,而到了這裡,我們修改了Hello.java,讓其使用了一個Person類。根據第11步,我們需要告訴JDK,到哪裡去找所用到的類,即使這個被使用的類就與Hello.java一起,同在C:/Java Test下面!輸入

javac -classpath "C:/Java Test" "C:/Java Test/Hello.java"

編譯通過,JDK在C:/Java Test資料夾下同時生成了Hello.class及Person.class兩個檔案。實際上,由於Hello.java使用了Person.java類,JDK先編譯生成了Person.class,然後再編譯生成Hello.class。因此,不管Hello.java這個主類使用了多少個其他類,只要編譯這個類,JDK就會自動編譯其他類,很方便。輸入

java -classpath "C:/Java Test" Hello

螢幕出現了

Mike

成功。

16. 第15步說明了在Hello.java中如何使用一個我們自己建立的Person.java,而且這個類與Hello.java是同在一個資料夾下。在這一步中,我們將考查Person.java如果放在不同資料夾下面的情況。

先將C:/Java Test資料夾下的Person.class檔案刪除,然後在C:/Java Test資料夾下新建一個名為DF的資料夾,並將C:/Java Test資料夾下的Person.java移動到其下面。在命令列輸入

javac -classpath "C:/Java Test/DF" "C:/Java Test/Hello.java"

編譯通過。這時javac命令沒有什麼不同,只需將classpath改成C:/Java Test/DF就行了。

在命令列輸入

java -classpath "C:/Java Test" Hello

這時由於Java需要找在不同資料夾下的兩個.class檔案,而命令列中只告訴JDK一個路徑,即C:/Java Test,在此資料夾下,只能找到Hello.class,找不到Person.class檔案,因此,錯誤是可以預料得到的:

Exception in thread "main" java.lang.NoClassDefFoundError: Person
         at Hello.main(Hello.java:3)

果真找不到Person.class。在設定兩個以上的classpath時,先將每個路徑以雙引號引起來,再將這些路徑以“;”號隔開,並且每個路徑與“;”之間不能帶有空格。因此,我們在命令列重新輸入:

java -classpath "C:/Java Test";"C:/Java Test/DF" Hello

編譯成功。但也暴露出一個問題,如果我們需要用到許多分處於不同資料夾下的類,那這個classpath的設定豈不是很長!有沒有辦法,對於一個資料夾下的所有.class檔案,只指定這個資料夾的classpath,然後讓JDK自動搜尋此資料夾下面所有相應的路徑?有,只要使用package。

17. package簡介。Java中引入package的概念,主要是為了解決命名衝突的問題。比如說,在我們的例子中,我們設計了一個很簡單的Person類,如果某人開發了一個類庫,其中恰巧也有一個Person類,當我們使用這個類庫時,兩個Person類出現了命名衝突,JDK不知道我們到底要使用哪個Person類。更有甚者,當我們也開發了一個很龐大的類庫,無可避免地,我們的類庫中與其他人開發的類庫中命名衝突的情況就會越來越多。總不能為了避免自己的類名與其他人開發的類名相同,而讓每個程式設計人員都絞盡腦汁地將一個本應叫Writer的類強行改名為SarkuyaWriter,MikeWriter, SmithWriter吧?

現實生活中也是如此。假如你名叫張三,又假如與你同一單位的人中有好幾個都叫張三,那你的問題就來了。某天單位領導在會上宣佈,張三被任命為辦公室主任,你簡直不知道是該哭還是該笑。但如果你的單位中只有你叫張三,你才不會在乎全國叫張三的人有多少個,因為其他張三都分佈在全國各地、其他城市,你看不見他們,摸不著他們,自然不會擔心。

Sun從這個“張三問題”受到了很大的啟發,為解決命名衝突問題,就採取了“眼不見心不煩”的策略:將每個類都歸屬到一個特定的區域中,在同一個區域中的所有類,都不允許同名;而不同區域的類,由於相互看不到,則允許有同名的類存在。這樣,就解決了命名衝突的問題,正如北京的張三與上海的張三畢竟不是同一人。這個區域在Java中就叫package。由於package在Java中非常重要,如果你沒有定義自己的package,JDK將會你的類都歸到一個預設的無名package中。

自定義package的名稱可以由各個程式設計師自由建立。作為避免命名衝突的手段,package的名稱最好足以與其他程式設計師的區別開來。在網際網路上,每個域名都是唯一的,因此,Sun推薦將你自己的域名倒寫後作為package的名稱。如果你沒有自己的域名,很可能只是因為囊中羞澀而不去申請罷了,並不見得你假想的域名與其他域名發生衝突。例如,筆者假想的域名是sarkuya.com,目前就是唯一的,因此我的package就可以定名為com.sarkuya。謝謝Java給了我們一個免費使用我們自己域名的機會,唯一的前提是倒著寫。當然,每個package下面還可以帶有不同的子package,如com.sarkuya.util,com.sarkuya.swing,等等。

定義package的方式是在相應的.java檔案的第一行加上“package packagename;”的字樣,而且每個.java檔案只能有一個package。實際上,Java中的package的實現是與計算機檔案系統相結合的,即你有什麼樣的package,在硬碟上就有什麼樣的存放路徑。例如,某個類的package名為com.sarkuya.util,那麼,這個類就應該必須存放在com/sarkuya/util的路徑下面。至於這個com/sarkuya/util又是哪個資料夾的子路徑,第18步會談到。

package除了有避免命名衝突的問題外,還引申出一個保護當前package下所有類檔案的功能,主要通過為類定義幾種可視度不同的修飾符來實現:public, protected, private, 另外加上一個並不真實存在的friendly型別。

對於冠以public的類、類屬變數及方法,包內及包外的任何類均可以訪問;
protected的類、類屬變數及方法,包內的任何類,及包外的那些繼承了此類的子類才能訪問;
private的類、類屬變數及方法,包內包外的任何類均不能訪問;
如果一個類、類屬變數及方法不以這三種修飾符來修飾,它就是friendly型別的,那麼包內的任何類都可以訪問它,而包外的任何類都不能訪問它(包括包外繼承了此類的子類),因此,這種類、類屬變數及方法對包內的其他類是友好的,開放的,而對包外的其他類是關閉的。

前面說過,package主要是為了解決命名衝突的問題,因此,處在不同的包裡面的類根本不用擔心與其他包的類名發生衝突,因為JDK在預設情況下只使用本包下面的類,對於其他包,JDK一概視而不見:“眼不見心不煩”。如果要引用其他包的類,就必須通過import來引入其他包中相應的類。只有在這時,JDK才會進行進一步的審查,即根據其他包中的這些類、類屬變數及方法的可視度來審查是否符合使用要求。如果此審查通不過,編譯就此卡住,直至你放棄使用這些類、類屬變數及方法,或者將被引入的類、類屬變數及方法的修飾符改為符合要求為止。如果此審查通過,JDK最後進行命名是否衝突的審查。如果發現命名衝突,你可以通過在程式碼中引用全名的方式來顯式地引用相應的類,如使用

java.util.Date = new java.util.Date()

或是

java.sql.Date = new java.sql.Date()。

package的第三大作用是簡化classpath的設定。還記得第16步中的障礙嗎?這裡重新引用其java命令:

java -classpath "C:/Java Test";"C:/Java Test/DF" Hello

我們必須將所有的.class檔案的路徑一一告訴JDK,而不管DF其實就是C:/Java Test的子目錄。如果要用到100個不同路徑的.class檔案,我們就得將classpath設定為一個特別長的字串,很累。package的引入,很好地解決了這個問題。package的與classpath相結合,通過import指令為中介,將原來必須由classpath完成的類路徑搜尋功能,很巧妙地轉移到import的身上,從而使classpath的設定簡潔明瞭。我們先看下面的例子。

18. 先在Hello.java中匯入DF.Person。程式碼修改如下:

import DF.Person;

public class Hello {
   public static void main(String[] args) {
     Person person = new Person("Mike");
     System.out.println(person.getName());
   }
}

再將DF子資料夾中的Person.java設定一個DF包。程式碼修改如下:

package DF;

public class Person {
   private String name;
   public Person(String name) {
     this.name = name;
   }

   public String getName() {
     return name;
   }
}

好了,神奇的命令列出現了:

javac -classpath "C:/Java Test" "C:/Java Test/Hello.java"
java -classpath "C:/Java Test" Hello

儘管這次我們只設置了C:/Java Test的classpath,但編譯及執行居然都通過了!事實上,Java在搜尋.class檔案時,共有三種方法:
一是全域性性的設定,詳見第12步,其優點是一次設定,每次使用;
二是在每次的javac及java命令列中自行設定classpath,這也是本文使用最多的一種方式,其優點是不加重系統環境變數的負擔;
三是根據import指令,將其內容在後臺轉換為classpath。JDK將讀取全域性的環境變數classpath及命令列中的classpath選項資訊,然後將每條classpath與經過轉換為路徑形式的import的內容相合並,從而形成最終的classpath. 在我們的例子中,JDK讀取全域性的環境變數classpath及命令列中的classpath選項資訊,得到C:/Java Test。接著,將import DF.Person中的內容,即DF.Person轉換為DF/Person, 然後將C:/Java Test與其合併,成為C:/Java Test/DF/Person,這就是我們所需要的Person.class的路徑。在Hello.java中有多少條import語句,就自動進行多少次這樣的轉換。而我們在命令列中只需告訴JDK最頂層的classpath就行了,剩下的則由各個類中的import指令代為操勞了。這種移花接木的作法為我們在命令列中手工地設定classpath提供了極大的便利。

應注意的一點是,import指令是與package配套使用的,只有在某類通過“package pacakgename;”設定了包名後,才能給其他類通過import指令匯入。如果import試圖匯入一個尚未設定包的類,JVM就會報錯。

19. 我們接下來看,當使用JDK類庫時,classpath如何設定。

20. 修改Hello.java,內容如下:

import DF.Person;
import java.util.Date;

public class Hello {
   public static void main(String[] args) {
     Date date = new Date();
     System.out.println(date);

     Person person = new Person("Mike");
     System.out.println(person.getName());
   }
}

21. JDK類庫存放於C:/Program Files/Java/jdk1.5.0/jre/lib/rt.jar檔案中。關於jar檔案的介紹,已經超出了本文的範圍,感興趣的讀者可以閱讀Horstmann寫的Core Java一書。

jar檔案可以用WinRar開啟。用WinRar開啟後,可以看到裡面有一些資料夾,雙擊其中的java資料夾,再雙擊util的資料夾,可以在看到Date.class檔案就在其中。如果你看過Data.java或其他JDK類庫的原始碼(在C:/Program Files/Java/jdk1.5.0/src.zip檔案中),你就會發現,像java、util這些資料夾均是package。這也是Hello.java第2行中使用了import指令的原因。

我們可以通過WinRar的查詢功能來定位某個類所在的包。在“查詢檔案”的視窗中的“要查詢的檔名”文字框中輸入Date.class,就會查找出在rt.jar檔案中存在兩個Date.class檔案,一個是java/sql/Date.class,另一個是java/util/Date.class。其中,sql下面的Date.class檔案與資料庫有關,並非我們這裡所需,java/util/Date.class才是我們所要的。

rt.jar檔案就像本文中的C:/Java Test中一樣,是JDK類庫的唯一入口。我們可以在命令列的classpath選項指定.jar檔案。需要注意,.jar檔案的classpath設定有些特珠。在以前的例子中,我們設定classpath時都是設定了路徑就行了,而對於.jar檔案,我們必須將.jar檔名直接加到classpath中。

22. 在命令列輸入

javac -classpath "C:/Program Files/Java/jdk1.5.0/jre/lib/rt.jar";"C:/Java Test" "C:/Java Test/Hello.java"
java -classpath "C:/Program Files/Java/jdk1.5.0/jre/lib/rt.jar";"C:/Java Test" Hello

這樣當然沒有問題,因為我們指定了rt.jar檔案及C:/Java Test兩個classpath。但且慢,在命令列輸入:

javac -classpath "C:/Java Test" "C:/Java Test/Hello.java"
java -classpath "C:/Java Test" Hello

不可思議的是,編譯及執行成功了!令人驚訝的是在我們將classspath只設置為C:/Java Test的情況下,JDK如何得出java.util.Date的classpath?

原因在於,就像java的Path路徑已經悄悄在後臺設定好一樣,rt.jar的classpath路徑也悄悄地在後臺設定了。因此,我們不必多此一舉手工設定其classpath了。

23. 最後一點需要談到的是,如果主類恰好也在一個package中(在大型的開發中,其實這才是一種最常見的現象),那麼java命令列的類名前面就必須加上包名。

在C:/Java Test下面新建一個資料夾,名為NF。將C:/Java Test下面的Hello.class刪除,將Hello.java移到NF資料夾下。開啟NF資料夾下的Hello.java,為其設定package屬性。

package NF;

import DF.Person;
import java.util.Date;

public class Hello {
   public static void main(String[] args) {
     Date date = new Date();
     System.out.println(date);

     Person person = new Person("Mike");
     System.out.println(person.getName());
   }
}

編譯與以前沒啥區別,只不過是修正一下改過之後的路徑。

javac -classpath "C:/Java Test" "C:/Java Test/NF/Hello.java"

而java命令列卻有了變化

java -classpath "C:/Java Test" NF.Hello

上面命令列語句中,NF.Hello告訴JDK,Hello.class在NF的package下面。

至此,本文有關classpath及package的問題的討論已經全部結束。由此可見,Java的入門的確非常不易。如果初學Java的程式設計師一見到Java的編譯竟是如此的複雜,多半就會抽身而退。因此,筆者認為,Sun在J2SE的Tutorial中故意將編譯的問題儘量簡單化,以吸引更多的Java初學者。一旦品嚐了Java的香醇可口的美味後,就不用擔心他們退出了,因為咖啡是非常容易讓人上癮的。