類別/原始碼路徑
May 19, 2022在〈Hello, Java〉編譯 HelloWorld.java 後,同一資料夾會出現 HelloWorld.class,直接在同一資料夾中執行 java HellWorld
就可以顯示文件。
JVM 與 CLASSPATH
接下來,請試著切換至 C:\,想想看,如何執行 HelloWorld
?以下幾個方式都是行不通的:
C:\workspace>cd ..
C:\>java HelloWorld
Error: Could not find or load main class HelloWorld
Caused by: java.lang.ClassNotFoundException: HelloWorld
C:\>java c:\workspace\HelloWorld
Error: Could not find or load main class c:\workspace\HelloWorld
Caused by: java.lang.ClassNotFoundException: c:\workspace\HelloWorld
C:\>
執行 java
指令是為了啟動 JVM,之後接著類別名稱,表示要求JVM執行指定的可執行檔(.class)。
實體作業系統下執行某個指令時,會依 PATH
的路徑資訊,試圖找到可執行檔案;JVM 是 Java 程式唯一認得的作業系統,對 JVM 來說,可執行檔就是副檔名為 .class 的檔案。想在JVM中執行某個可執行檔(.class),就要告訴 JVM 這個虛擬作業系統到哪些路徑下尋找檔案,方式是透過 CLASSPATH
指定其可執行檔(.class)的路徑資訊。
PATH
與 CLASSPATH
是不同層次的環境變數,實體作業系統搜尋可執行檔是看 PATH
,JVM 搜尋可執行檔(.class)只看 CLASSPATH
。
如何在啟動 JVM 時告知可執行檔(.class)的位置?可以使用 -classpath
或簡寫 -cp
引數來指定:
C:\>java -classpath c:\workspace HelloWorld
Hello, Java
C:\>java -cp c:\workspace HelloWorld
Hello, Java
C:\>
如果有多個路徑資訊,可以用分號區隔。例如:
java -cp C:\workspace;C:\classes HelloWorld
JVM 會依 CLASSPATH
路徑順序,搜尋是否有對應的類別檔案,先找到先載入。如果在 JVM 的 CLASSPATH
路徑資訊中都找不到指定的類別檔案,就會出現 java.lang.NoClassDefFoundError
訊息。
JVM 預設的 CLASSPATH
會讀取目前資料夾中的 .class,如果自行指定 CLASSPATH
,就以指定的為主。例如:
C:\>cd \workspace
C:\workspace>java -cp c:\xyz HelloWorld
Error: Could not find or load main class HelloWorld
Caused by: java.lang.ClassNotFoundException: HelloWorld
C:\workspace>
如上所示,雖然工作路徑是在 C:\workspace(其中有 HelloWorld.class),啟動 JVM 時指定到 C:\xyz 中搜尋類別檔案,JVM 就老實地到指定的 C:\xyz 中找尋,結果當然就是找不到而顯示錯誤訊息。有的時候,希望也從目前資料夾開始尋找類別檔案,則可以使用 .
指定。例如:
C:\workspace>java -cp .;c:\xyz HelloWorld
Hello, Java
C:\workspace>
如果使用 Java 開發了程式庫,這些程式庫中的類別檔案,會封裝為 JAR(Java Archive)檔案,也就是副檔名為 .jar 的檔案。JAR 檔案實際使用 ZIP 格式壓縮,當中包含一堆 .class 檔案,那麼,如果有個 JAR 檔案,就是將 JAR 檔案當作特別的資料夾,例如,有 abc.jar 與 xyz.jar 放在 C:\lib 底下,執行時若要使用 JAR 檔案中的類別檔案,可以如下:
java -cp C:\workspace;C:\lib\abc.jar;C:\lib\xyz.jar SomeApp
如果有些類別路徑很常使用,可以透過環境變數設定。例如:
SET CLASSPATH=C:\classes;C:\lib\abc.jar;C:\lib\xyz.jar
在啟動 JVM 時,也就是執行 java
時,若沒使用 -cp
或 -classpath
指定,就會讀取 CLASSPATH
環境變數,若有指定,會使用 -cp
或 -classpath
的指定。
如果某資料夾中有許多 .jar 檔案,可以使用 *
表示使用資料夾中所有 .jar檔案(也適用在系統環境變數的設定)。例如指定使用 C:\jars 下所有 JAR 檔案:
java –cp .;C:\jars\* cc.openhome.JNotePad
編譯器與 CLASSPATH
在 classes 有個已編譯的Console.class,請將 classes 複製至C:\workspace,然後在 C:\workspace 開個 Main.java,如下使用 Console
類別:
public class Main {
public static void main(String[] args) {
Console.writeLine("Hello, World");
}
}
如果如下編譯,會出現錯誤訊息:
C:\workspace>javac Main.java
Main.java:3: error: cannot find symbol
Console.writeLine("Hello, World");
^
symbol: variable Console
location: class Main
1 error
C:\workspace>
編譯器在抱怨,它找不到 Console
類別在哪裡(cannot find symbol),在使用 javac
編譯器時,如果要使用到其他類別程式庫時,也必須指定 CLASSPATH
,告訴 javac
編譯器到哪邊尋找 .class 檔案。例如:
C:\workspace>java Main
Exception in thread "main" java.lang.NoClassDefFoundError: Console
at Main.main(Main.java:3)
Caused by: java.lang.ClassNotFoundException: Console
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
... 1 more
C:\workspace>
這一次編譯成功了,但無法執行,原因是執行時找不到 Console
類別,因為你執行時忘了跟 JVM 指定 CLASSPATH
,如果如下執行就可以了:
C:\workspace>java -cp .;classes Main
Hello, World
C:\workspace>
別忘了,如果執行 JVM 時指定了 CLASSPATH
,就只會在指定的 CLASSPATH
中尋找使用到的類別,因此方才指定 .;classes
,注意一開始的 .
,這表示目前資料夾,這樣才可以找到目前資料夾下的 Main.class,以及 classes 的 Console.class。
管理原始碼與位元碼
來觀察一下目前的 C:\workspace,原始碼檔案與位元碼檔案都放在一起,想像一下,如果程式規模稍大,一堆 .java 與 .class 檔案還放在一起,會有多麼混亂,你需要有效率地管理原始碼與位元碼檔案。
請將 Hello1 資料夾複製至 C:\workspace,Hello1 資料夾有 src 與 classes 資料夾,src 資料夾中有 Console.java 與 Main.java,其中Console.java 就是方才 Console
類別的原始碼。
src 資料夾用來放置原始碼檔案,而編譯好的位元碼檔案,希望能指定存放至 classes 資料夾。可以在文字模式下,切換至 Hello1 資料夾,然後如下進行編譯:
C:\workspace\cd Hello1
C:\workspace\Hello1\javac -sourcepath src -d classes src/Main.java
在編譯 src/Main.java 時,由於程式碼中要使用到 Console
類別,必須告訴編譯器,Console
類別的原始碼檔案存放位置,這邊使用 -sourcepath
指定從 src 資料夾中尋找原始碼檔案,而 -d
指定了編譯完成的位元碼存放資料夾,編譯器會將使用到的相關類別原始碼也一併進行編譯,編譯完成後,會在 classes 資料夾中看到 Console.class 與 Main.class 檔案。你可以如下執行程式:
C:\workspace\Hello1\java -cp classes Main
Hello, World
在編譯時,會先搜尋 -sourcepath
指定的資料夾,看看是否有使用到的類別原始碼,然後會搜尋是否有已編譯的類別位元碼,預設搜尋位元碼的路徑會包括 JDK 資料夾中的 lib\modules,以及目前的工作路徑。
確認原始碼與位元碼搜尋路徑之後,接著檢查類別位元碼的搜尋路徑中,是否已經有編譯完成的類別,如果存在且從上次編譯後,類別的原始碼並沒有改變,無需重新編譯,若不存在,重新編譯類別。
就上例而言,類別位元碼的搜尋路徑中,找不到 Main.java 與 Console.java 編譯出的類別位元碼,因此會重新編譯出 Main.class 與 Console.class 並存放至 classes。
實際專案中會有數以萬計的 .java,如果每次都重新將 .java 編譯為 .class,會是非常費時的工作,也沒有必要,因此編譯時可以指定類別路徑,若存在編譯後的類別位元碼,而且上次編譯後原始碼並沒有修改,就不會重新編譯。
就上例而言,可以指定 -cp
為 classes。例如:
C:\workspace\Hello1\javac -sourcepath src -cp classes -d classes src/Main.java
C:\workspace\Hello1\
這次指定了 -sourcepath
為 src,而 -cp
為 classes,因此會在 src 搜尋位原始碼檔案,除了 JDK 資料夾的 lib\modules 檔案,現在也會在 classes 搜尋位元碼檔案,由於類別位元碼的搜尋路徑包括 classes 資料夾,可以找到 Console
類別位元碼,無需重新編譯出 Console.class,只會將 javac
指定的 Main.java 重新編譯為 Main.class。
使用 javac
編譯時若加上 -verbose
引數,可以看到編譯過程中搜尋原始碼及類別位元碼的過程。