12-2 套件

在您的電腦中會有許多的檔案,對於這些檔案,您會建立一些資料夾來分門別類管理,例如將文件放在 docs 資料夾,將遊戲放到 games 資料夾,將郵件放到 emails 資料夾,您不會將所有的檔案全部放在一個資料夾中,因為在找尋檔案時會很麻煩,而且很容易的您會遇到檔案名稱相同的衝突問題。

您也可以使用類似資料夾的階層管理觀念,來管理每一個您所設計的 Java 類別,這讓您可以有系統的管理 Java 類別,方便找尋所需的類別,並且避免類別名稱經常重複的問題。

• package 關鍵字

您可以使用 package 關鍵字來定義套件以管理類別,將套件的觀念想像成類似檔案系統資料夾管理的觀念,您可以根據套件的階層很快的找到所需的類別,在不同套件下的類別,即使名稱相同也無所謂,它們是不同的類別。

程式碼 12-5 直接示範了如何使用 package 關鍵字:

package com.oracle;

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello! World!");
    }
}
                            
程式碼 12-5 HelloWorld.java

在程式的第一行中,使用了 package 將 HelloWorld 類別歸類於 com.oracle 套件來加以管理,這麼一來完整的類別名稱將是 com.oracle.HelloWorld;在檔案管理方面,套件被設計為與檔案系統相對應,所以在編譯過後,HelloWorld.class 必須放置於 com 資料夾的 oracle 資料下。

圖12-5 套件的階層管理

每次編譯完類別之後,都還要自行將*.class 移動至對應的資料夾下並不方便,您可以在編譯時加入-d 選項,並指定編譯後*.class 的目標資料夾,則編譯器就會在指定的資料夾下依套件設定,自動建立與套件管理對應的資料夾,例如您可以這麼下指令:

圖12-6 編譯時使用-d 引數

在圖 12-6 中使用 . 來指定編譯過後的*.class 將置放於目前的目錄之下,編譯器也會自動依套件設定,建立 com 資料夾與 oracle 資料夾,而 HelloWorld.class 檔案則被置放在 oracle 資料夾之下,在編譯過後,套件名稱會成為完整類別名稱的一部份,所以執行程式時,必須指定套件名加類別名,例如:

圖12-7 執行時必須使用完整的類別名稱

• import 關鍵字

假設您設計了一個 Student 類別,並使用設定 com.oracle 來管理該類別,如程式碼 12-6 所示:

package com.oracle;

public class Student {
    private int number;
    private String name;

    public Student() {
    }
    public Student(int number, String name) {
        this.number = number;
        this.name = name;
    }
    public int getNumber() {
        return number;
    }
    public String getName() {
        return name;
    }
}
                            
程式碼 12-6 Student.java

如果您要在另一個 Java 類別中引用這個類別,則您要使用完全描述(Fully qualified)名稱,也就是指定完整的類別名稱,即套件名加上類別名,例如程式碼 12-7 所示:

public class StudentDemo {
    public static void main(String[] args) {
        com.oracle.Student student =
                new com.oracle.Student(123456, "Duke");


        System.out.println("學號: " +
                student.getNumber());
        System.out.println("姓名: " +
                student.getName());
    }
}
                            
程式碼 12-7 StudentDemo.java

程式;碼中第 3 行的 com.oracle.Student 就是完全描述名稱,再次強調的是,在編譯過後,套件名稱已經成為完整類別名稱的一部份,除非您重新編譯,否則是無法改變這個名稱的程式碼 12-7 的執行結果如下所示:

圖12-8 程式碼 12-7 執行結果

重點提示

如果您的類別與另一個類別是位於同一個套件下,則無需使用完全描述名稱也可以,編譯器會嘗試尋找同一套件下的類別來進行編譯。

然而每次都要指定完全描述名稱在撰寫上過於麻煩,您可以在一開始時使用 import 關鍵字,告訴編譯器如果程式中找不到指定的類別時,可以參考 import 上所指定的完全描述名稱嘗試進行編譯,例如將程式碼 12-7 改為程式碼 12-8。

import com.oracle.Student;

public class StudentDemo2 {
    public static void main(String[] args) {
        Student student =
                new Student(123456, "Duke");
        System.out.println("學號: " +
                student.getNumber());
        System.out.println("姓名: " +
                student.getName());
    }
}
                            
程式碼 12-8 StudentDemo2.java

當編譯器在程式中的第 5 行找不到 Student 類別時,它會參考程式中的第 1 行,嘗試 com.oracle.Student 是不是目標類別並進行編譯,所以使用 import 可以讓您在撰寫類別名稱的時候少鍵入一些字,這個程式的執行結果與圖 12-8 是相同的。

如果您大量使用到某個套件管理下的類別,則一行一行的使用 import 來指定完全描述名稱也並不是很方便,此時您可以在使用 import 時,只指定套件名,而最後一個類別名的部份改為*號,例如將程式碼 12-8 改為程式碼 12-9。

import com.oracle.*;

public class StudentDemo3 {
    public static void main(String[] args) {
        Student student =
                new Student(123456, "Duke");
        System.out.println("學號: " +
                student.getNumber());
        System.out.println("姓名: " +
                student.getName());
    }
}
                            
程式碼 12-9 StudentDemo3.java

編譯器在遇到第 5 行時找不到對應的 Student 類別,於是會嘗試將第一行中 com.oracle 套件名與 Student 類別名組合在一起,看看 com.oracle.Student 是不是可以進行編譯;程式碼 12-9的執行結果與圖 12-8 是相同的。

在這邊要注意一個初學者很常遇到的問題,由於初學者經常會在當前的工作目錄下直接編輯*.java 檔案,然後使用 javac 進行編譯,如果您在目前的工作目錄下編輯程式碼 12-6 的Student.java,並使用 javac –d . Student.java 編譯程式,之後您再目前工作目錄下又編輯程式碼 12-9,在進行編譯時,您就會遇到以下的錯誤:

StudentDemo3.java:5: cannot access Student
bad class file: .\Student.java
file does not contain class Student
Please remove or make sure it appears in the correct
subdirectory of the classpath.
        Student student =
        ^
1 error
                            

圖12-9 套件管理下的名稱衝突

您可以在編譯時使用-verbose 選項,看看編譯時的一些訊息,就可以發現錯誤出在哪邊:

                                略...
[checking StudentDemo3]
[loading .\Student.java]
[parsing started .\Student.java]
[parsing completed 0ms]
StudentDemo3.java:5: cannot access Student
bad class file: .\Student.java
file does not contain class Student
Please remove or make sure it appears in the correct
subdirectory of the classpath.
Student student =
^
[total 266ms]
1 error
                            

原因在於編譯器先載入 Student.java 檔案,並發現它並沒有在對應的 com/oracle/ 資料中,要記得編譯器會先以-sourcepath 的路徑設定找尋*.java 檔案來進行編譯,接著才是找尋-classpath 的路徑中之*.class 檔案,而-sourcepath 預設與-classpath 相同的,這是之所以編譯會發生錯誤的原因。

解決的方法之一是建議您在檔案管理時,採用與套件相同的檔案結構的管理 *.java ,例如直接將 Student.java 放 在 com/oracle 資料夾之中。

另一個建議的方法是設置一個 src 目錄,將*.java 檔案放置到該目錄下管理,例如:

圖12-10 使用 src 目錄管理*.java 檔案

在編譯的時候,您可以這麼下指令,就可以正確的通過編譯: javac -d . src/StudentDemo3.java

總而言之,套件管理是為了管理類別檔案的一種機制,可以避免名稱衝突的情況發生,而 import 的語法則是方便您撰寫程式,可以讓您不用指定類別的完全描述名稱,由編譯器自動尋找可編譯的類別,如果一旦名稱衝突發生(例如在 import 時使用*號),則您最好考慮回過頭來指定完全名稱以解決問題。

重點提示

java.lang 套件下的類別是 Java 的常用類別,使用時已預設有 import,無需自行指定,例如 String、Math、和 Integer 您都可以直接使用,而無需在程式開始時使用import 再行指定。

• public 與套件

在定義類別時,可以於類別關鍵字 class 之前加上 public 等權限修飾,如果不加上任何的權限修飾,則預設為套件內可存取的範圍,例如:

package com.oracle;
class Some {
    // ...
}
                            

在這樣的設定之下,Some 類別就只能被同是在 com.oracle 套件管理下的類別使用,如果非 com.oracle 套件的類別嘗試使用Some 類別,則編譯時會發生以下的錯誤:

com.oracle.Some is not public in com.oracle; cannot be accessed from outside package
                            

在類別中定義方法時,可以使用 public、protected、private 等關鍵字來修飾方法的權限,如果不指定任何權限修飾,則預設為套件可呼叫權限,也就是在同一個套件管理下的類別才可以呼叫該方法,例如:

package com.oracle;
public class Other {
    void someMethod() {
        // ...
    }
}
                            

在這樣的設定之下,只有同樣是 com.oracle 套件管理下的類別才可以呼叫 someMethod()方法,如果非 com.oracle 套件管理下的類別試圖呼叫 someMethod()方法,則編譯時會出現以下的錯誤:

someMethod() is not public in com.oracle.Other; cannot be accessed from outside package
                            

如果有一個類別在定義時沒有使用任何的權限修飾字,而在定義方法時使用 public 來加以修飾,則該方法仍是套件可存取範圍,例如:

package com.oracle;
class Another {
    public void someMethod() {
        // ...
    }
}
                            

在這樣的設定之下,someMethod()與沒有使用 public 來修飾相同,它仍是只能被同在 com.oracle 套件管理下的類別呼叫。

• import static

import static 語法是 JDK 5.0 所新增的語法,在作用上與 import 類似,import static 可以於程式一開始指定靜態成員的完整名稱,之後在撰寫靜態成員時,可以省略靜態成員之前的套件名與類別名稱等。程式碼 12-10 以一個簡單的顯示"Hello!Java!"程式來作示範:

import static java.lang.System.out;

public class HelloJava {
    public static void main() {
        out.println("Hello!Java!");
    }
}
                            
程式碼 12-10 HelloJava.java

out 物件是 System 類別下的一個公開靜態成員,在未使用import static 語法前,您只能用 System.out 的方式來操作它,在程式碼 12-10 的第 1 行中使用 import static 告訴編譯器,若遇到 out 名稱,可以參考 java.lang.System.out 這個靜態成員,所以在程式碼第 5 行的 out 名稱前無須再撰寫System 類別。

import static 語法不僅可用於類別的靜態資料成員上,也可以應用於類別的靜態方法成員上,例如:

import static java.lang.Double.parseDouble;
import static java.lang.System.out;

public class SimpleCalc {
    public static void main(String[] args) {
        if (args.length == 0) {
            out.println("請提供兩個數字..");
        }
        else {
            double num1 = parseDouble(args[0]);
            double num2 = parseDouble(args[1]);
            out.println(num1 + " + " + num2
                    + " = " + (num1 + num2));
            out.println(num1 + " - " + num2
                    + " = " + (num1 - num2));
            out.println(num1 + " * " + num2
                    + " = " + (num1 * num2));
            out.println(num1 + " / " + num2
                    + " = " + (num1 / num2));
        }
    }
}
                            
程式碼 12-11 SimpleCalc.java

在程式碼的第 2 行中,使用 import static 語法告知編譯器,在遇到 parseDouble 名稱時可以參考 java.lang.Double.parseDouble,故而在第 10 行、第 11 行可以直接撰寫 parseDouble()方法而不用指定 Double 類別名稱,執行結果如下所示:

圖12-11 程式碼 12-11 的執行結果

import static 語法上也可以使用*號,例如您可以告訴這麼撰寫:

import static java.lang.System.*;
                            

如此一來,System 下的所有靜態成員都可以直接在程式中撰寫,而不用再加上 System 類別名稱。同樣的,如果發生名稱衝突的情況,建議您還是回歸原來的完全名稱指定方式,以解決名稱衝突問題。

相關資料

下列參考資料能夠對本章節所討論的話題提供 更詳細的說明:

  • Oracle 官方教材中介紹如何建立與使用介面的文件:Interfaces
  • Oracle 官方教材中介紹如何建立與使用套件的文件:Packages