《java深度历险》学习笔记--package与import机制
1、請注意,以上說明只是為了強調path 環境變數的使用--用于指明java与javac命令的搜索路径。事實上,幾乎大多
數版本的JDK 都會於安裝時主動在<Windows 安裝目錄>\system32 底下複製
一份java.exe,而<Windows 安裝目錄>\system32 通常又是Windows 預設
path 環境變數中的其中一個路徑,所以一般的情況下,都會發生可以執行
java.exe,卻不能執行javac.exe 的情形。
?
2、在此推薦您一個非常好用的選項: -verbose 。您可以在javac.exe 或java.exe
中使用此選項。他可以讓您更了解編譯和執行過程中JVM 所做的每件事情。
?
3、,編譯器總是先到 A.java 本身所在的路徑中尋找
B.java,雖然編譯器找到了B.java,可是比對過其package 宣告之後,認為
它應該位於edu\nctu 目錄下,不該在此目錄下,因此產生錯誤訊息。
?
4、結論一:
如果您的類別屬於某個package,那麼您就應該將它至於該package 所對應
的相對路徑之下。舉例來說,如果您有個類別叫做C,屬於xyz.pqr.abc 套件,
那麼您就必須建立一個三層的目錄 xyz\pqr\abr,然後將C.java 或是C.class
放置到這個目錄下,才能讓javac.exe 或是java.exe 順利執行。
其實這裡少說了一個重點,就是這個新建的目錄應該從哪裡開始? 一定要從
d:\my 底下開始建立嗎? 請大家將這個問題保留在心裡,我們將在底下的討論
之中為大家說明。
?
5、結論二:
當您使用javac.exe 編譯的時候,類別原始碼的位置一定要根據結論一所說來
放置,如果該原始碼出現在不該出現的地方(如上述測試中B.java 同時存在
d:\my 與d:\my\edu\nctu 之下),除了很容易造成混淆不清,而且有時候抓
不出編譯為何發生錯誤,因為javac.exe 輸出的錯誤訊息根本無法改善問題。
?
6、結論三:
編譯時,如果程式裡用到其他的類別,不需要該類別的原始碼也一樣能夠通過編
譯。
?
7、結論四:
當您使用javac.exe 編譯程式卻又沒有該類別的原始碼時,類別檔放置的位置
應該根據結論一所說的方式放置。如果類別檔出現在不該出現的地方(如上述測
試中B.class 同時存在d:\my 與d:\my\edu\nctu 之下),有很大的可能性會
造成難以的編譯錯誤。雖然上述的測試中,使用java.exe 執行Java 程式時,
類別檔亂放不會造成執行錯誤,但是還是建議您儘量不要這樣做,除了沒有意義
之外,這種做法像是一顆不定時炸彈,隨時都有可能造成您的困擾
?
8、結論五:
結論一中我們提到,如果您有個類別叫做C,屬於xyz.pqr.abc 套件,那麼您
就必須建立一個三層的目錄 xyz\pqr\abr,然後將C.java 或是C.class 放置到
這個目錄下,才能讓javac.exe 或是java.exe 順利執行。但是這個新建的目錄
應該從哪裡開始呢? 答案是:”可以建立在任何地方”。但是您必須告訴java.exe
與javac.exe 到哪裡去找才行,告訴的方式就是利用它們的-classpath 選項。
在此測試中,我們把edu\nctu 這個目錄移到d:\之下,所以我們必須使用指令:
javac –classpath d:\ A.java
與
java –classpath d:\;. A
告訴java.exe 與javac.exe 說:”如果你要找尋類別檔,請到-classpath 選項後
面指定的路徑去找,如果找不到就算了”。
不過這裡又衍生出兩個問題,首先第一個問題是,那麼為什麼之前的指令都不需
要額外指定-classpath 選項呢?這是因為當您執行java.exe 和javac.exe 時,
其實他已經自動幫您加了參數,所以不管是討論一或討論二裡所使用的指令,其
實執行時的樣子如下:
javac –classpath . A.java
或
java –classpath . A
意思就是說,他們都把當時您所在的目錄當作是-classpath 選項的預設參數。
那麼您可以發現,如果是javac.exe 執行時,當時我們所在的路徑是d:\my,
所以很自然地,他會自動到d:\my 底下的edu\nctu 目錄尋找需要的檔案,
java.exe 也是一樣的道理。
但是,一旦我們搬移了 edu\nctu 目錄之後,這個預設的參數使得
javac.exe 要尋找d:\my\edu\nctu 底下的B.java 或B.class 來進行編譯時,
發生連d:\my\edu\nctu 目錄都找不到的情況(因為被我們移走了),所以自然
發生編譯錯誤。因此我們必須使用在指令中加上–classpath d:\,讓javac.exe
到d:\下尋找相對路徑d:\edu\nctu。執行java.exe 時之所以也要在指令中加
上–classpath d:\,也是一樣的道理。
如果您仔細比較兩個修改過的指令,您還是會發現些許的差異,於是引發了
第二個問題:為什麼javac.exe 用的是–classpath d:\,而java.exe 如果不
用–classpath d:\;.而只用–classpath d:\卻無法執行呢? 我們在第二章曾經提過,
-classpath 是用來指引AppClassLoader 到何處載入我們所需要的類別。在執
行時期,主程式A.class 也是一個類別,所以如果不是預設的情況(預設指向目
前所在目錄,即”.”),我們一定要向AppClassLoader 交代清楚所有相關類別
檔的所在位置。但是對編譯器來說,A.java 就單純是個檔案,只要在目前目錄
之下,編譯器就能找到,基本上和AppClassLoader 毫無關聯,需要在
-classpath 指定路徑,只是為了在編譯時期AppClassLoader 可以幫我們載入
B.class 或指引編譯器找到B.java,所以必須這樣指定。因此,對javac.exe
來說,-classpath 有上述兩種作用;然而對於java.exe 來說,就只有傳遞給
AppClassLoader 一種作用而已。
?
9、請注意,環境變數
CLASSPATH 與–classpath 選項分別所指定的路徑並不會有加成效果。
?
10、結論六:
使用ZIP 檔的效果和單純的目錄相同,如果您在–classpath 選項指定了目錄,就
是告訴java.exe 和javac.exe 到該目錄尋找類別檔;如果您在–classpath 選項
指定了ZIP 檔的檔名,那麼就是請java.exe 和javac.exe 到該壓縮檔中尋找類
別檔。請注意,即使是在壓縮檔中,但是該有的相對路徑還是要有,否則java.exe
和javac.exe 仍然無法找到他們所需要的類別檔。
當然,在環境變數 CLASSPATH 中也可以指定ZIP 檔作為其內容。
?
11、其實JAR
檔就是ZIP 檔。為何大家喜歡使用JAR 檔的封裝方式呢?這是因為一般別人所開
發的套件總是會附帶許多類別檔和資源檔(例如圖形檔),這麼多檔案全部放在目
錄下很容易讓人感到雜亂不堪,如果將這些檔案全部封裝在一個壓所檔之中,不
但可以減少檔案大小,也可以讓您的硬碟看起來更精簡。這也是為何每次我們下
載了某人所開發的套件時,如果只有一個JAR 檔,我們就必須在環境變數
CLASSPATH 或–classpath 選項中加上這個JAR 檔的檔名,因為唯有如此,我們
才能讓java.exe 和javac.exe找到我們的程式中所使用到別人套件中類別的類
別檔,並成功執行。
?
12、之所以要特別提醒大家這個問題,是因為常常在寫程式的時候不小心在兩個
地方都有相同的套件(這也是筆者本身的切身之痛),一個比較舊,但指定
classpath 的時候該路徑較前,導致雖然另外一個路徑裡有新版的程式,但是執
行或編譯時卻總是跑出舊版程式的執行結果,讓人丈二金剛摸不著腦袋。所以最
後還是要提醒您特別注意正視這個問題。
?
13、
1. 將 JAVA 檔和類別檔確實安置在其所歸屬之package 所對應的相對路
徑下。
2. 不管使用 java.exe 或javac.exe,最好明確指定–classpath 選項,如果
怕麻煩,使用環境變數CLASSPATH 可以省去您輸入額外指令的時間。
請注意在他們所指定的路徑或JAR 檔中是否存有package 名稱和類別
名稱相同的類別,因為名稱上的衝突很容易引起混淆。
?
14、如果没有利用加载器的make机制来编译和运行代码,则要一个个的编译和运行,如:
A.java在d:\my\edu\nctu\mot\目录下
代码为:
package edu.nctu.mot ;public class A{ public static void main(String[] args) { System.out.println("I'm A") ; }}
?
在d:\my目录下
编译用
javac d:\my\edu\nctu\mot\A.java
执行用
java –classpath . edu.nctu.mot.A
?
15、,不管你有沒有使用import 指令,存在目前
目錄下的類別都會被編譯器優先採用,只要它不屬於任何package。這是因為
編譯器總是先假設您所輸入的類別名稱就是該類別的全名(不屬於任何
package),然後從-classpath 所指定的路徑中搜尋屬於該類別的.java 檔
或.class 檔
?
16、這個錯誤訊息說明了編譯器產生疑惑,因為 com 與edu 這兩個package 底
下都有名為CA 的類別,他不知道該用哪一個。解決的辦法就是明確地告訴編譯
器我們要哪個package 底下的CA,解決方法有兩種:
1. 在 import 處明確宣告,也就是把Main.java 改成
src\comtime\Main.java
import com.CA;
import edu.*;
public class Main
{
??? public static void main(String args[])
?? {
??????? CA ca = new CA() ;
?? }
}
或者
2. 引用時詳細指定該類別的全名(即套件名稱.類別名稱的組合)
src\comtime\Main.java
import com.*;
import edu.*;
public class Main
{
???? public static void main(String args[])
??? {
???????? com.CA ca = new com.CA() ;
????}
}
?
17、使用javac –classpath .. Main.java 编译Main.java 时,默认以窗口所在路径为根路径(一般为Main.java所在的路径),然后编译Main时,会利用make机制先去加载编译Main所需要的其他类,此时要在classpath中把所有要加载的类的路径都列出来才行。
?
18、以上面這張圖來說,這是假設我們用的指令是:
javac –classpath d:\test;c:\p.jar test.java
所以使得類別路徑參考表之中有兩筆資料,接著,假設我們的test.java 之中有
import A.B.* ;
import C.D ;
那麼編譯器就會先看看d:\test 目錄底下使否存在有A\B 目錄,也就是說編譯
器會檢查d:\test\A\B 究竟是否存在,如果找不到,它會試著再找看看c:\p.jar
之中是否存在A\B 這個目錄,如果都沒找到,就會發出package A.B does not
exist 的訊息。同理,編譯器也會試著尋找是否存在d:\test\C\D.class 或
d:\test\C\D.java(如果找不到就進入c:\p.jar 裡頭找),並確定他們所屬的
package 名稱無誤,也確保他們為public(如此才能讓其他package 的類別
所存取,且檔名一定與類別名稱相同),如果沒找到就會發出cannot resolve
symbol 的錯誤訊息。
?
19、從上面所討論的 Java 編譯器運作邏輯,亦對兩個一般人常常有的誤解做了解釋:
1. import 和C/C++裡的include 同意義。
2. 在 import 裡使用萬用字元,如import x.y.* ,會讓編譯器效率減低。
其中,第一個誤解也常讓很多程式設計師覺得不解,為什麼在Java 之中無法使
用
import java.*.*
這種有一個以上萬用字元的語法?
由上面的討論你可以知道,在Java 裡頭,import 應該和C/C++裡的
namespace 等價才對,而非一般所說的include。
假設你有兩個類別,分別是 X.A 與X.B.C,你必須分別
import X.* ;
import X.B.* ;
才行,因為這兩個指令會讓編譯器分別在類別路徑參考表之中建立兩筆資料,如
此一來當編譯器將類別名稱與類別路徑參考表裡的資料作合成的時候,才能讓你
的程式:
C c1 = new C();
通過編譯,如果你只有
import X.* ;
則編譯器回發出錯誤訊息,說它無法解析類別C。也就是說,類別路徑參考表裡
的資料,只是單純拿來合成,讓編譯器可以很快地定位到類別檔或原始碼的位
置,並不會遞迴式地幫我們自動搜尋該路徑底下其他的類別,所以不管在import
處使用完整的類別名稱或萬用字元,即使原始碼會使得類別路徑參考表和相對類
別參考表很大很大,但是javac.exe 內部在處理這些資料表格時也是採用Hash
Table 的設計,所以對編譯速度的影響幾乎微乎其微。
?
20、當編譯器找到該類別的類別檔或原始碼之後,都會加以驗證他們使否真的屬於這個
package,如果有問題,編譯器一樣會發出錯誤訊息;但是如果並非透過
make(自動解析名稱,自動編譯所需類別)機制來編譯程式,是不會做這項驗證
工作的。
?
21、檢視
利用相同的邏輯,我們回頭來解釋一下,在本章前半部之中談到package
與import 機制那個段落的最後一個範例,我們所使用的檔案是:
A.java
package edu.nctu.mot ;
public class A
{
public static void main(String[] args)
{
System.out.println("I'm A") ;
}
}
編譯用的指令有以下幾種,我們分別解釋當時其成功與失敗之原因:
1 javac edu.nctu.mot.A.java
類 別 路 徑 參 考 表 中 只 有 一 個 ”.” , 以此為起始點, 找不到名為
edu.nctu.mot.A.java 的檔案。也就是說,在編譯時期,不會自動把命
令列之中所指定的原始檔之中的”.”轉換成”/”。我們指定什麼檔案,編譯
器一律認為是單純的檔名。
2 javac -classpath .\my\edu\nctu\mot A.java
類別路徑參考表中只有一個”.\edu\nctu\mot”,以此為起始點,其實
可以找到名為A.java 的檔案,可是問題是,A.java 並非透過make 機
制來編譯,而是我們手動打指令編譯,所以根本不會用到類別路徑參考
表。因此編譯器根本找不到A.java。
3 javac -classpath d:\my\edu\nctu\mot A.java
類別路徑參考表中只有一個”d:\my\edu\nctu\mot”,以此為起始點,
其實可以找到名為A.java 的檔案,可是問題是,A.java 並非透過make
機制來編譯,而是我們手動打指令編譯,所以根本不會用到類別路徑參
考表。因此編譯器根本找不到A.java。
4 javac d:\my\edu\nctu\mot\A.java
在指定的路徑之下可以找到該檔案,可以編譯成功。
執行用的指令有以下幾種,我們分別解釋當時其成功與失敗之原因:
1 java edu.nctu.mot.A
類別路徑參考表中只有一個”.”,以此為起始點,系統會自動把”.”轉換
成”/”,因此會找到”.\edu\nctu\mot”底下的A.class。
2 java -classpath .\edu\nctu\mot A
類別路徑參考表中只有一個”.\edu\nctu\mot”,以此為起始點,系統
會A.class,可是在檢查類別檔內部資訊的時候,發現A.class 屬於
edu.nctu.mot 這個package , 所以應該放置
在”.\edu\nctu\mot\edu\nctu\mot”這個目錄之下,因此出現錯誤
訊息。
3 java -classpath d:\my\edu\nctu\mot A
類別路徑參考表中只有一個”d:\my\edu\nctu\mot”,以此為起始點,
系統會A.class,可是在檢查類別檔內部資訊的時候,發現A.class 屬於
edu.nctu.mot 這個package , 所以應該放置
在”.\edu\nctu\mot\edu\nctu\mot”這個目錄之下,因此出現錯誤
訊息。
4 java d:\my\edu\nctu\mot\A
類別路徑參考表中只有一個”.”。但是系統根本無法接受這樣子的命令。