封裝的意義

物件導向程式設計的原則之一, 是讓實作和界面分開, 以便讓同一界面但不同的實作的物件能以一致的面貌讓外界存取, 為了達到此目標, java允許設計人員規範類別成員以及類別本身的存取限制。

類別成員的存取

所謂封裝(Encapsulation),是指class A的設計者可以指定其他的class能否存取A的某個member。Java定義了四種存取範圍:

public class EncapsulationExample {
    private int privateVariable;
    int packageVariable;
    protected int protectedVariable;
    public int publicVariable;
    private int privateObjectMethod() {}
    int packageObjectMethod() {}
    protected int protectedObjectMethod();
    public int publicObjectMethod();
    static private int privateClassMethod() {}
    static int packageClassMethod() {}
    static protected int protectedClassMethod();
    static public int publicClassMethod();
}

如果member的前面沒有private,protected,public其中一個修飾字,則該member的存取範圍就是package。從以上的敘述,讀者可以推知這四種存取範圍的大小是public > protected > package > private。

Package的定義

所謂package,可以想成是在設計或實作上相關的一群class。要宣告某class屬於某package,其語法為

package myPackage;
public class MyClass {
}

如果沒有宣告package的話,如下面這兩個class,就會被歸類為「匿名」的package:

// in file ClassA.java
public class ClassA {
    public static void main(String[] argv) {
        ClassB x = new ClassB();
    }
}
// in file ClassB.java
public class ClassB {
}

為了讓JVM在執行期間能夠找到所需要的class,同一個package的class會放在同一個目錄下。不過只知道目錄的名稱還不夠,還需要指定該目錄在檔案系統內的路徑。classpath這個環境變數是由多個以分號隔開的路徑所組成,JVM透過classpath配合package的名稱就可以找到所需要的class。如果我們只用到Java標準的程式庫,則不需要指定classpath。指定classpath環境變數時,要特別注意的是,不要忘了把.(代表目前的工作目錄)放到最前面,否則就找不到「匿名」package裡的class(別忘了絕大部分簡單的範例都沒有宣告package,所以都是匿名的)。

classpath=.;n:\計網中心系統組\project;

classpath裡除了路徑外,也可以指定zip或jar(java archive format)格式的檔案。zip和jar可以把目錄及其子目錄內的檔案都壓縮起來,因此可以透過這類檔案抓到所需的class檔。

classpath=.;c:\mylib.zip;c:\otherlib.jar;n:\計網中心系統組\project;

package的宣告可以用.號構成複雜的package tree。

package mylib.package1
public class A {}
package mylib.package2
public class B {}

屬於mylib.package1的class會放在mylib目錄下的package1目錄內。Java所提供的標準應用程式介面(Application Programming Interface, API)就是一個複雜的package tree。

同一個.java檔裡面, 可以定義好幾個class, 但最多只能有一個宣告為public class。此限制是因為java希望每一個編譯的單元(.java檔)都有唯一的界面宣告。那麼public class和class的區別何在? non public class只能給同一個package內的其他class引用, public class則可以給任何class引用。

Package的引用

假如Class A用到package myPackage裡的Class B, 為了檢查A使用到B的部分是否符合B的原始定義, 諸如方法存不存在, 參數正不正確等問題, Compiler必須引入class B的定義, 以便進行編譯時期的檢查。引入的語法為

import myPackage.B;

這裡要強調的是, import指令告知Compiler在Compiler time所要檢查的類別定義在哪裡。但有時候我們編譯的環境和執行的環境可能不同, 例如編譯時用JDK 1.4, 執行時卻用JDK 1.2, 若程式使用到JDK 1.4才有的API, 那麼會在執行期間產生錯誤。

有時候我們會引用相當多個同屬某package的類別, 如果要一個一個import, 會很煩人, 因此Java允許我們使用萬用字元*來代表某package裡的所有class:

import myPackage.*;
public class A {
    public static void main(String[] argv) {
        B x = new B();
    }
}

眼尖的讀者會發現我們並沒有import String的定義啊, 怎麼都沒有問題? 由於寫程式多多少少都會用到一點系統提供的程式庫, 如果連很簡單的程式都要import一堆class, 也真煩人。因此Java Compiler會自動幫我們引入java.lang.*

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

就等同

import java.lang.*;
public class Hello {
    public static void main(String[] argv) {
        System.out.println("Hello World.");
    }
}

由於class是放在類似樹狀結構的package tree裡面, 因此引用的class應該加上完整的package路徑才是全名, 例如

public class Hello {
    public static void main(java.lang.String[] argv) {
        java.lang.System.out.println("Hello World.");
    }
}

只要不會造成混淆, 一般我們都使用省略package路徑的class簡稱。但是如果我們import P1和P2兩個package, 而這兩個package碰巧都定義了同名的class A, 則用到A的地方就比需以P1.A和P2.A來區別了。

下面的程式碼哪裡有錯?

package p1;
public class Access {
    private static void f1() {}
    static void f2() {}
    protected static void f3() {}
    public static void f4() {
        Access.f1();
    }
}
package p1;
public class Example1 {
    public static void main(String[] argv) {
        Access.f1();
        Access.f2();
        Access.f3();
        Access.f4();
    }
}
package p2;
import p1.*;
public class Example2 {
    public static void main(String[] argv) {
        Access.f1();
        Access.f2();
        Access.f3();
        Access.f4();
    }
}
package p1;
public class Example3 extends Access {
    public static void main(String[] argv) {
        Access.f1();
        Access.f2();
        Access.f3();
        Access.f4();
    }
}
package p2;
import p1.*;
public class Example4 extends Access {
    public static void main(String[] argv) {
        Access.f1();
        Access.f2();
        Access.f3();
        Access.f4();
    }
}

Java檔和Class檔的相依性

傳統程式開發的流程是Compile個別Source Code,然後Link所有的Object Code成為執行檔。對大型的應用程式來說,常見的問題之一是如何確定Link時所需的Object Code是由最新的Source編譯而來?尤其模組間存在相依性,如模組A可能用到模組B裡的函數,如果B的函數有修改參數,則A模組也要重新編譯。換句話說單看source code和object code的產生時間是不行的。最簡單的方法就是在Link前將所有的Source Code重新編譯一次,但這樣做有以下幾個問題:

  1. 重新編譯大型專案全部的程式碼可能會浪費不少時間
  2. 要對每個Source File下達編譯指令,不但費時,而且容易遺漏。即使寫個批次程式,也要隨時記得納入新的原始檔

由於這些問題的存在,某些原始碼管理系統便因應而生,例如UNIX上的SCCS(Source Code Control System)。Java Compiler具有下面兩個功能,可以在沒有原始碼管理系統的情況下,也能解決上述問題:

  1. 可使用javac *.java來編譯目前目錄下的所有java檔案
  2. 編譯A.java時,會自動檢查A所用到的其他class B,比較B.java和B.class的產生時間,如果B.java比較新則B.java就會被重新編譯

如果應用軟體只有單一的進入點,例如class A的public static void main(String[] argv),則只要編譯A.java就會自動編譯其他需要重新編譯的.java檔。如果應用軟體有兩個以上的進入點,如網路程式的client端和server端的進入點會不一樣,只要寫個批次檔編譯相關進入點的.java檔即可。

用Link List實作Stack

Link List Stack 示意圖

Link List Stack 示意圖(點圖放大)

class Node {
    Object data;
    Node next;
}
public class Stack {
    private Node head;
    private int size;
    public void push(Object s) {
        Node tmp = new Node();
        tmp.next = head;
        tmp.data = s;
        size++;
        head = tmp;
    }
    public Object pop() {
        Object tmp = head.data;
        head = head.next;
        size--;
        return tmp;
    }
}

Link List Stack Push Step1

Link List Stack Push Step 1(點圖放大)

Link List Stack Push Step2

Link List Stack Push Step 2(點圖放大)

Link List Stack Push Step3

Link List Stack Push Step 3(點圖放大)

Link List Stack Pop

Link List Stack Pop(點圖放大)


public class Example {
    public static void main(String[] argv) {
        Stack s1 = new Stack();
        Stack s2 = new Stack();
        s1.push("abc");
        s1.push("def");
        s2.push("123");
        s2.push("456");
    }
}

用Link List實作Queue

class Node {
    Object data;
    Node next;
}
public class Queue {
    private Node head, tail;
    private int size;
    public void put(Object s) {
        Node tmp = new Node();
        tmp.data = s;
        if (tail != null) {
            tail.next = tmp;
        } else {
            head = tmp;
        }
        tail = tmp;
        size++;
    }
    public Object get() {
        Object tmp = head.data;
        head = head.next;
        if (head == null) {
            tail = null;
        }
        size--;
        return tmp;
    }
}