Java SE 6 技術手冊

9.2 package 與 import

隨著程式架構越來越大,類別個數越來越多,您會發現管理程式中維護類別名稱也會是一件麻煩的事,尤其是一些同名問題的發生,例如在程式中,您也許會定義一個 Point 類別,但另一個與您合作開發程式的開發人員並不曉得已經有這個類別名稱的存在,他可能也定義了一個 Point 類別,於是編譯過後他的 Point 類別檔案會覆蓋您的 Point 類別檔案,問題就這麼發生了。

9.2.1 設定套件(package)

Java 提供「套件」(package)來管理類別,套件被設計與檔案系統結構相對應,如果您的套件設定為onlyfun.caterpillar,則該類別應該在 Classpath 可以存取到的路徑下的 onlyfun 目錄下之 caterpillar 目錄找到,沒有設定套件管理的類別會歸為「預設套件」(default package)。

為了要能建立與套件相對應的檔案系統結構,您在編譯時可以加入 "-d" 參數,並指定產生的類別檔案要儲存在哪一個目錄之下,實際使用範例來進行說明,在 Java 中定義套件時,要使用關鍵字 "package",使用方法如範例 9.4。

範例 9.4 PackageDemo.java

package onlyfun.caterpillar;

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

在編譯時您使用以下的指令指定編譯過後的類別檔案儲存目錄,'.'表示建立在目前的工作位置:

javac -d . UsePackage.java 

編譯完成之後,在目前的工作位置中會出現 onlyfun 目錄,而 onlyfun 目錄下會有個 caterpillar 目錄,而 caterpillar 目錄下則有一個 PackageDemo.class 檔案,在編譯完成之後,"package" 的設定會成為類別名稱的一部份,也就是完整的類別名稱是 onlyfun.caterpillar.PackageDemo,所以在執行時要這麼下指令以指定類別名稱:

java onlyfun.caterpillar.PackageDemo

執行結果會出現"Hello! World!"。再來舉個例子,假設您如範例 9.5 建立了onlyfun.caterpillar.Point2D 類別。

範例 9.5 Point2D.java

package onlyfun.caterpillar; 

public class Point2D { 
    private int x;
    private int y;

    public Point2D() {
    } 

    public Point2D(int x, int y) {
        this.x = x; 
        this.y = y;
    } 

    public int getX() { return x; } 
    public int getY() { return y; } 
}

先用以下的指令來編譯 Point2D 類別:

javac -d . Point2D.java

在編譯過後,在 onlyfun 目錄的 caterpillar 目錄下會有 Point2D.class 檔案,而 "package" 所設定的名稱就成為類別名稱的一部份,也就是完整的類別名稱是 onlyfun.caterpillar.Point2D,除非您改變套件名稱並重新編譯類別,否則的話無法改變這個名稱,為了要使用 onlyfun.caterpillar.Point2D 類別,方法之一是使用「完全描述」(Fully qualified)名稱,也就是完整的指出「套件加類別」名稱,如範例 9.6 所示。

範例 9.6 Point2DDemo.java

public class Point2DDemo { 
    public static void main(String[] args) { 
        onlyfun.caterpillar.Point2D p1 = new 
            onlyfun.caterpillar.Point2D(10, 20);

        System.out.printf("p1: (x, y) = (%d, %d)%n",
            p1.getX(), p1.getY()); 
    } 
}

執行結果如下所示:

p1: (x, y) = (10, 20)

設定了套件名稱的類別,必須放置在對應的目錄中,例如若 Point2D 的套件設定為 onlyfun.caterpillar,則最後編譯完成的 .class 檔案必須放在 onlyfun 目錄的 caterpillar 目錄下,否則編譯時會有以下的錯誤發生:

bad class file: .\Point2D.class
class file contains wrong class: onlyfun.caterpillar.Point2D
Please remove or make sure it appears in the correct subdirectory of the classpath.
        Point2D p1 = new Point2D(10, 20);
        ^
1 error

如果您在編譯時沒有使用 "-d" 並指定 .class 檔案產生的目標目錄,則編譯完成之後,您也要自己建立對應於套件的目錄結構,然後將 .class 放入對應的目錄之下。

良葛格的話匣子 在命名套件時,可以使用組織的網域名稱來作為開頭命名,通常是倒過來命名,例如網域如果是 openhome.cc,則命名套件時可以用 cc.openhome,之後再加上您自己設定的套件名稱,這麼一來同名的衝突機會可以更少。

9.2.2 import 的意義

如果您有使用 "package" 來為您的類別設定套件管理,則編譯過後 "package" 所設定的名稱就成為類別名稱的一部份,在範例 9.6中,您使用「完全描述」(Fully qualified)名稱來指定使用的類別,當然這個方法要打一長串的文字,因而使用上不是很方便,您可以使用 "import" 關鍵字,告知編譯器您所要使用的類別是位於哪一個套件,如此您可以少打一些字,讓編譯器多作一些事,例如範例 9.6 可以改寫為範 例9.7。

範例 9.7 Point2DDemo2.java

import onlyfun.caterpillar.Point2D;

public class Point2DDemo2 { 
    public static void main(String[] args) { 
        Point2D p1 = new Point2D(10, 20);

        System.out.printf("p1: (x, y) = (%d, %d)%n",
            p1.getX(), p1.getY()); 
    } 
}

雖然在範例 9.7 中新建 onlyfun.caterpillar.Point2D 物件時,只指定了 Point2D 名稱,但編譯器從一開頭的 "import" 設定得知,完整的類別名稱應該是 onlyfun.caterpillar.Point2D,因而可以順利編譯,執行結果與範例 9.6 是相同的。

在使用 "import" 時可以指定類別的完整描述,如果您會使用到某個套件下的許多類別,在使用 "import" 指定時,可以於套件指定後加上 '*',這表示您會使用到該套件下的某些類別,編譯器會自己試著找出類別,例如範例 9.7 還可以再改為範例 9.8 的寫法。

範例 9.8 Point2DDemo3.java

import onlyfun.caterpillar.*;

public class Point2DDemo3 { 
    public static void main(String[] args) { 
        Point2D p1 = new Point2D(10, 20);

        System.out.printf("p1: (x, y) = (%d, %d)%n",
            p1.getX(), p1.getY()); 
    } 
}

編譯器在處理這個程式時,會先試著在現行工作路徑下找有無 Point2D 類別,如果找不到的話,編譯器會試著組合 "import" 上的設定來找尋 Point2D 類別,就範例 9.8 而言,會將 onlyfun.caterpillar 與 Point2D 組合在一起,然後試著找到 onlyfun.caterpillar.Point2D 類別。

您也許會發現無法編譯範例 9.8,可能出現以下的錯誤訊息:

bad class file: .\Point2D.java
file does not contain class Point2D
Please remove or make sure it appears in the correct subdirectory of the classpath.

這不是程式撰寫有誤,而是因為您 "import" 時使用了 '*',並且您的 Point2D.java 原始檔案也在同一個目錄,照之前編譯器尋找類別順序的說明,編譯器會先找到 Point2D.java,但發現當中有設定套件,而 Point2D.java 沒有在對應的目錄(onlyfun/caterpillar)下,所以編譯器認定這是個錯誤。

將原始碼與編譯完成的類別檔放在一起容易發生這類的問題,事實上將原始碼與編譯完成的檔案放在一起並不是一個好的管理方式,您可以建一個專門放原始碼 .java 檔案的目錄 src,並建一個專門放 .class 檔案的目錄 classes,編譯時這麼下指令:

javac -d ./classes ./src/*.java

這麼一來產生的 .class 就會儲存在 classes 目錄下,可以至 classes 目錄下直接執行程式:

java Point2DDemo3

或者直接在執行編譯時的工作目錄下,以指定 Classpath 的方式如下執行程式:

java -cp ./classes Point2DDemo3

但要注意的是,如果您使用 "import" 之後,出現類別名稱有同名衝突時,編譯器就不知道如何處理了,例如:

import java.util.Arrays;
import onlyfun.caterpillar.Arrays;
public class SomeClass {
    ....
}

在這個例子中,編譯器從 "import" 上發現有兩個可能的 Arrays 類別,它不確定若遇到 Arrays 時您要使用的是 java.util.Arrays,或是 onlyfun.caterpillar.Arrays,編譯器只好回報錯誤訊息:

java.util.Arrays is already defined in a single-type import
import onlyfun.caterpillar.Arrays;
^
1 error

這個時候您就要考慮換一下類別名稱了(如果您有權更動那些類別的話),或者是不使用 "import",直接使用完整描述;在 "import" 時儘量不因為貪圖方便而使用 '*',也可以減少這種情況的發生。

良葛格的話匣子 使用 "import" 就是在告知編譯器您的類別位於哪一個套件下,而編譯器尋找類別最先是根據 Classpath 的設定,所以您也要瞭解 Classpath 的設定,建議您也看看官方網站上的 Classpath 設定文章,您對套件的瞭解會更深入:

Java SE 平台的 .class 檔案是儲存在 JRE 安裝目錄的 /lib 下的 rt.jar 中,而額外的第三方(Third- party)元件可以放 /lib/ext 中,以及您自己設定的 Classpath。

9.2.3 public 與套件

一個類別在定義時可以使用 "public" 加以修飾,一個 .java 檔案中可以定義數個類別,但只能有一個被宣告為 "public",沒有被宣告為 "public" 的類別只能被同一個套件中的類別之實例呼叫使用,例如將範例 9.5 中 onlyfun.caterpillar.Point2D 類別上的 "public" 拿掉並重新編譯,接著再編譯 Point2DDemo.java 檔案時,會出現以下的錯誤,因為 Point2DDemo(預設套件)與 onlyfun.caterpillar.Point2D 不在同一個套件:

Point2DDemo.java:3: onlyfun.caterpillar.Point2D is not public in onlyfun.caterpillar;     cannot be accessed from outside package
        onlyfun.caterpillar.Point2D p1 = new

類別成員也可以宣告為 "public",宣告為 "public" 的類別成員可以被其它物件呼叫使用,如果宣告類別時不使用 "public"、"protected" 或 "private" 設定權限,則預設為「套件存取範圍」,只有同一個套件中的類別可以呼叫這些類別成員,例如範例 9.5 中將 getX()、getY() 上的 "public" 拿掉並重新編譯,接著再編譯 Point2DDemo.java 時,會出現以下的錯誤,因為 Point2DDemo(預設套件)與 onlyfun.caterpillar.Point2D 不在同一個套件:

Point2DDemo.java:7: getX() is not public in onlyfun.caterpillar.Point2D; cannot be     accessed from outside package
           p1.getX(), p1.getY());
              ^

類別上的權限設定會約束類別成員上的權限設定,所以如果類別上不宣告 "public",而類別成員上設定了 "public",則類別成員同樣的也只能被同一個套件的類別存取,也就是說如果您這麼撰寫程式:

package onlyfun.caterpillar;
class SomeClass {
    // ...
    public void someMethod() {
        // ....
    }
}

其效果等同於:

package onlyfun.caterpillar;
class SomeClass {
    // ...
    void someMethod() {
        // ....
    }
}

由這邊的討論,可以再來看看預設建構方法的權限。首先要知道的是,當您在 Java 中定義一個類別,但沒有定義建構方法時,編譯器會自動幫您產生一個預設建構方法,也就是說,如果您這麼寫:

package onlyfun.caterpillar;
public class Test {
    ....
}

則編譯器會自動加上預設建構方法,也就是相當於這麼寫:

package onlyfun.caterpillar;
public class Test {
    public Test() {
    }
    ....
}

如果您自行定義建構方法,則編譯器就不會幫您加上預設建構方法,所以當您這麼定義時:

package onlyfun.caterpillar;
public class Test {
    public Test(int i) {
        ...
    }
    ....
}

則在建構時,就必須指明使用哪個建構方法,簡單的說,您就不能使用以下的方式來建構:

Test test = new Test();

有時會建議即使沒有用到,在定義自己的建構方法的同時,也加上個沒有參數的建構方法,例如:

package onlyfun.caterpillar;
public class Test {
    public Test() { // 即使沒用到,也先建立一個空的建構方法
    }

    public Test(int i) {
        ...
    }
    ....
}

要注意的是,在繼承時,如果您沒有使用 super() 指定要使用父類別的哪個建構方法,則預設會尋找父類別中無參數的建構方法。

預設建構方法的存取權限是跟隨著類別的存取權限而設定,例如:

package onlyfun.caterpillar;
public class Test {
}

由於類別宣告為 public,所以預設建構方法存取權限為 public。如果是以下的話:

package onlyfun.caterpillar;
class Test {
}

則預設建構方法存取權限為套件存取權限,也就是編譯器會自動為您擴展為:

package onlyfun.caterpillar;
class Test {
    Test() {
    }
}

在這邊整理一下 private、protected、public 與 default 與類別及套件的存取關係:

表 9.1 權限設定為套件的關係

存取修飾 同一類別 同一套件 子類別 全域
private OK
(default) OK OK
protected OK OK OK
public OK OK OK OK

9.2.4 import 靜態成員

在 J2SE 5.0 後新增了 "import static" 語法,它的作用與 "import" 類似,都是為了讓您可以省一些打字功夫,讓編譯器多作一點事而存在的。"import static" 是使用時的語法,原文的文章或原文書中介紹這個功能時,大都用 "static import" 描述這個功能,編譯器訊息也這麼寫,但為了比較彰顯這個功能的作用,這邊稱之為「import 靜態成員」。

使用 "import static" 語法可以讓您 "import" 類別或介面中的靜態成員,一個實際的例子如範例 9.9 所示。

範例 9.9 HelloWorld.java

import static java.lang.System.out;

public class HelloWorld {
    public static void main(String[] args) {
        out.println("Hello! World!");
    }
}

在範例中您將 java.lang.System 類別中的 out 靜態成員 "import" 至程式中,編譯時編譯器遇到 out 名稱,可以從 "import static" 上知道 out 是 System 中的靜態成員,因而自動展開為 System.out 並加以編譯。

再來看一個例子,Arrays 類別中有很多的靜態方法,為了使用方便,您可以使用 "import static" 將這些靜態方法 "import" 至程式中,如範例 9.10 所示。

範例 9.10 ImportStaticDemo.java

import static java.lang.System.out;
import static java.util.Arrays.sort;

public class ImportStaticDemo {
    public static void main(String[] args) {
        int[] array = {2, 5, 3, 1, 7, 6, 8};

        sort(array);

        for(int i : array) {
            out.print(i + " ");
        }
    }
}

編譯器遇到範例 9.10 中的 sort() 方法,可以從 "import static" 上知道 sort() 方法是 java.util.Arrays 上的靜態成員,執行結果如下:

1 2 3 5 6 7 8

如果您想要 "import" 類別下所有的靜態成員,也可以使用 '*' 字元,例如將範例 9.10 中的 "import static" 改為以下也是可行的:

`import static java.util.Arrays.*;

"import static" 語法可以讓您少打一些字,但是您要注意名稱衝突問題,對於名稱衝突編譯器可能透過以下的幾個方法來解決:

  • 成員覆蓋

    如果類別中有同名的資料成員或方法,則優先選用它們。

  • 區域變數覆蓋

如果方法中有同名的變數名或參數名,則選用它們。

  • 重載(Overload)方法上的比對

    嘗試使用重載機制判斷,也就是透過方法名稱及參數列的比對來選擇適當的方法。

如果編譯器無法判斷,則會回報錯誤,例如若您定義了如下的類別:

package onlyfun.caterpillar;
public class Arrays {
    public static void sort(int[] arr) {
        // ....
    }
}

然後如下撰寫程式:

import static java.lang.System.out;
import static java.util.Arrays.sort;
import static onlyfun.caterpillar.Arrays.sort;
public class ImportStaticDemo2 {
    public static void main(String[] args) {
        int[] array = {2, 5, 3, 1, 7, 6, 8};
        sort(array);
        for(int i : array) {
            out.print(i + " ");
        }
    }
}

由於從 java.util.Arrays.sort 與 onlyfun.caterpillar.Arrays.sort 的兩行 "import static" 上都可以找到 sort,編譯器無法辦別要使用哪一個 sort() 方法,因而編譯時會出現以下的錯誤:

ImportStaticDemo2.java:9: reference to sort is ambiguous, both method sort(int[]) in     onlyfun.caterpillar.Arrays and method sort(int[]) in java.util.Arrays match
     sort(array);
        ^
1 error