物件導向程式設計的原則之一, 是讓實作和界面分開, 以便讓同一界面但不同的實作的物件能以一致的面貌讓外界存取, 為了達到此目標, 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,可以想成是在設計或實作上相關的一群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引用。
假如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(); } }
傳統程式開發的流程是Compile個別Source Code,然後Link所有的Object Code成為執行檔。對大型的應用程式來說,常見的問題之一是如何確定Link時所需的Object Code是由最新的Source編譯而來?尤其模組間存在相依性,如模組A可能用到模組B裡的函數,如果B的函數有修改參數,則A模組也要重新編譯。換句話說單看source code和object code的產生時間是不行的。最簡單的方法就是在Link前將所有的Source Code重新編譯一次,但這樣做有以下幾個問題:
由於這些問題的存在,某些原始碼管理系統便因應而生,例如UNIX上的SCCS(Source Code Control System)。Java Compiler具有下面兩個功能,可以在沒有原始碼管理系統的情況下,也能解決上述問題:
如果應用軟體只有單一的進入點,例如class A的public static void main(String[] argv),則只要編譯A.java就會自動編譯其他需要重新編譯的.java檔。如果應用軟體有兩個以上的進入點,如網路程式的client端和server端的進入點會不一樣,只要寫個批次檔編譯相關進入點的.java檔即可。
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; } }
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"); } }
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; } }