package 與 import

May 18, 2022

就如同你會分不同資料夾來放置不同作用的檔案,類別也應該分門別類地放置。

package 套件

舉例來說,一個應用程式中會有多個類別彼此合作,也有可能由多個團隊共同分工,完成應用程式的某些功能塊,再組合在一起,若應用程式是多個團隊共同合作,又不分門別類放置 .class,那麼若 A 部門寫了 Util 類別並編譯為 Util.class,B 部門也寫了 Util 類別並編譯為 Util.class,當他們要將應用程式整合時,就會發生檔案覆蓋的問題,若現在要統一管理原始碼,也許原始碼也會發生彼此覆蓋問題。

你要有個分門別類管理類別的方式,無論是實體檔案上的分類管理,或是類別名稱上的分類管理,有個 package 關鍵字,可以達到這個目的。

請用編輯器開啟〈類別/原始碼路徑〉中 Hello1/src 資料夾的 Console.java,在開頭鍵入 package 該行:

package cc.openhome.util;

public class Console {
    public static void writeLine(String text) {
        System.out.println(text);
    }
}

這表示,Console 類別將放在 cc.openhome.util 套件(package)管理,套件的命名,通常會用組織或單位的網址命名,舉例來說,我的網址是 openhome.cc,套件就會反過來命名為 cc.openhome,由於組織或單位的網址是獨一無二的,這樣的命名方式,比較不會與其他組織或單位的套件發生同名衝突。

接著編輯 Hello1/src 資料夾中的 Main.java,在開頭鍵入 package 該行文字,這表示 Main 類別將放在 cc.openhome 套件,並且將 Console 改為 cc.openhome.util.Console

package cc.openhome;

public class Main {
    public static void main(String[] args) {
        cc.openhome.util.Console.writeLine("Hello, World");
    }
}

當類別原始碼開始使用 package 進行分類時,就會具有四種管理上的意義:

  • 原始碼檔案要放置在與 package 定義名稱階層相同的資料夾階層。

目前計劃將所有原始碼檔案放在 src 中管理,由於 Console 類別使用 package 定義在 cc.openhome.util 套件下,Console.java 就必須放在 src 資料夾中的 cc/openhome/util 資料夾,在沒有工具輔助下,必須手動建立出資料夾,Main 類別使用 package 定義在 cc.openhome 套件下,因此 Main.java 必須放在 src 資料夾中的 cc/openhome 資料夾。

這麼做的好處很明顯,日後若不同組織或單位的原始碼要放置在一起管理,就不容易發生原始碼檔案彼此覆蓋的問題。

  • package 定義名稱與 class 定義名稱,會結合而成類別的完全吻合名稱(Fully qualified name)。

由於 Main 類別是位於 cc.openhome 套件分類中,完全吻合名稱是 cc.openhome.Main,而 Console 類別是位於 cc.openhome.util 分類中,完全吻合名稱為 cc.openhome.util.Console

在原始碼中指定使用某個類別時,如果是相同套件中的類別,就只要使用 class 定義的名稱即可,而不同套件的類別,必須使用完全吻合名稱。由於 MainConsole 類別是位於不同的套件中,在 Main 類別中要使用 Console 類別,就必須使用 cc.openhome.util.Console

這麼做的好處在於,若另一個組織或單位也使用 class 定義了 Console,但套件定義為 com.abc,完全吻合名稱會是 com.abc.Console,也就不會與 cc.openhome.util.Console 發生名稱衝突問題。

  • 位元碼檔案要放置在與 package 定義名稱階層相同的資料夾階層。

目前計劃將位元碼檔案放在 classes 資料夾中管理,由於 Console 類別使用 package 定義在 cc.openhome.util 套件下,編譯出來的 Console.class 就必須放在 classes 資料夾中的 cc/openhome/util 資料夾,Main 類別使用 package 定義在 cc.openhome 套件下,Main.class 就必須放在 classes 資料夾中的 cc/openhome 資料夾。

編譯時並不用手動建立對應套件階層的資料夾,若使用 -d 指定位元碼的存放位置,就會自動建立出對應套件階層的資料夾,並將編譯出來的位元碼檔案放置至對應的位置。例如:

C:\workspace\Hello1>javac -sourcepath src -cp classes -d classes src/cc/openhome/Main.java

C:\workspace\Hello1>java -cp classes cc.openhome.Main
Hello, World

由於 Main 類別位於 cc.openhome 套件,使用 java 執行程式時,必須指定完全吻合名稱,也就是指定 cc.openhome.Main 這個名稱。

  • 要在套件間共用的類別或方法(Method)必須宣告為 public

這牽涉到套件間的權限管理,之後才會討論。

使用 import

使用套件管理,解決了實體檔案與撰寫程式時類別名稱衝突的問題,然而,若每次撰寫程式時,都得鍵入完全吻合名稱,也是件麻煩的事,想想看,有些套件定義的名稱冗長時,單是要鍵入完全吻合名稱得花多少時間。

可以用 import 偷懶一下,例如:

package cc.openhome;

import cc.openhome.util.Console;

public class Main {
    public static void main(String[] args) {
        Console.writeLine("Hello, World");
    }
}

編譯與執行時的指令方式,與方才相同。當編譯器剖析 Main.java 看到 import 宣告時,會先記得 import 的名稱,後續剖析程式時,若看到 Console 名稱,原本會不知道 Console 是什麼東西,但編譯器記得 import 告訴過它,如果遇到不認識的名稱,可以比對一下 import 過的名稱,編譯器試著使用 cc.openhome.util.Console,結果可以在指定的類別路徑中,cc/openhome/util 資料夾下找到 Console.class,於是進行編譯。

import 只是告訴編譯器,遇到不認識的類別名稱,可以嘗試使用 import 過的名稱,import 讓你少打一些字,讓編譯器多為你做一些事。

如果同一套件下會使用到多個類別,你也許會多次使用 import

import cc.openhome.Message;
import cc.openhome.User;
import cc.openhome.Address;

你可以更偷懶一些,使用 *,表示套件中的全部類別:

import cc.openhome.*;

偷懶也是有個限度,如果編譯器無法辨別該使用哪個類別時,就會引發編譯錯誤,遇到這種情況時,就不能偷懶了,要使用哪個類別名稱,就得明確地在程式中使用類別的完全吻合名稱,就像方才一開始必須撰寫 cc.openhome.util.Console.writeLine("Hello, World") 那樣。

標準 API 有許多常用類別,像 System 類別,其實也有使用套件管理,完整名稱是 java.lang.System,在 java.lang 套件的類別由於很常用,不用撰寫 import 也可以直接使用 class 定義的名稱,這也就是不用如下撰寫程式的原因(寫了也沒關係,只是自找麻煩):

java.lang.System.out.println("Hello!World!");

如果類別位於同一套件,彼此使用並不需要 import,當編譯器看到一個沒有套件管理的類別名稱,會先在同一套件尋找類別,如果找到就使用,若沒找到,再試著從 import 陳述進行比對。java.lang 可視為預設就有 import,沒有寫任何 import 陳述時,也會試著比對 java.lang 的組合,看看是否能找到對應類別。

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