類別/原始碼路徑

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)的路徑資訊。

PATHCLASSPATH 是不同層次的環境變數,實體作業系統搜尋可執行檔是看 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 引數,可以看到編譯過程中搜尋原始碼及類別位元碼的過程。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter