程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 7.JAVA編程思想筆記隱藏實施過程

7.JAVA編程思想筆記隱藏實施過程

編輯:JAVA綜合教程

7.JAVA編程思想筆記隱藏實施過程


“進行面向對象的設計時,一項基本的考慮是:如何將發生變化的東西與保持不變的東西分隔開。”

Java 推出了“訪問指示符”的概念,允許庫創建者聲明哪些東西是客戶程序員可以使用的,哪些是不可使用的。這種訪問控制的級別在“最大訪問”和“最小訪問”的范圍之間,分別包括:public,“友好的”(無關鍵字),protected以及private。根據前一段的描述,大家或許已總結出作為一名庫設計者,應將所有東西都盡可能保持為“private”(私有),並只展示出那些想讓客戶程序員使用的方法。這種思路是完全正確的,盡管它有點兒違背那些用其他語言(特別是 C)編程的人的直覺,那些人習慣於在沒有任何限制的情況下訪問所有東西。

組件庫以及控制誰能訪問那個庫的組件的概念現在仍不是完整的。仍存在這樣一個問題:如何將組件綁定到單獨一個統一的庫單元裡。這是通過Java 的package(打包)關鍵字來實現的,而且訪問指示符要受到類在相同的包還是在不同的包裡的影響。

庫組件如何置入包裡? 這樣才能理解訪問指示符的完整含義。

1 包:庫單元

用import 關鍵字導入一個完整的庫時,就會獲得“包”(Package)。例如:

import java.util.*;

它的作用是導入完整的實用工具(Utility)庫,該庫屬於標准Java 開發工具包的一部分。

由於Vector 位於java.util 裡,所以現在要麼指定完整名稱“java.util.Vector”(可省略 import 語句),要麼簡單地指定一個“Vector”(因為import是默認的)。 若想導入單獨一個類,可在import語句裡指定那個類的名字: importjava.util.Vector; 現在,可以自由地使用Vector。然而,java.util 中的其他任何類仍是不可使用的。

之所以要進行這樣的導入,是為了提供一種特殊的機制,以便管理“命名空間”(NameSpace)。我們所有類成員的名字相互間都會隔離起來。位於類A 內的一個方法f()不會與位於類B 內的、擁有相同“簽名”(自變量列表)的f()發生沖突。但類名會不會沖突呢?假設創建一個stack 類,將它安裝到已有一個stack

類(由其他人編寫)的機器上,這時會出現什麼情況呢?對於因特網中的 Java 應用,這種情況會在用戶毫不知曉的時候發生,因為類會在運行一個Java 程序的時候自動下載。

正是由於存在名字潛在的沖突,所以特別有必要對 Java 中的命名空間進行完整的控制,而且需要創建一個完全獨一無二的名字,無論因特網存在什麼樣的限制。

若計劃創建一個“對因特網友好”或者說“適合在因特網使用”的程序,必須考慮如何防止類名的重復。

為Java 創建一個源碼文件的時候,它通常叫作一個“編輯單元”(有時也叫作“翻譯單元”)。每個編譯單元都必須有一個以.java結尾的名字。而且在編譯單元的內部,可以有一個公共(public)類,它必須擁有與文件相同的名字(包括大小寫形式,但排除.java文件擴展名)。如果不這樣做,編譯器就會報告出錯。

每個編譯單元內都只能有一個public 類(同樣地,否則編譯器會報告出錯)。那個編譯單元剩下的類(如果有的話)可在那個包外面的世界面前隱藏起來,因為它們並非“公共”的(非public),而且它們由用於主public類的“支撐”類組成。

編譯一個.java 文件時,我們會獲得一個名字完全相同的輸出文件;但對於.java 文件中的每個類,它們都有一個.class 擴展名。因此,我們最終從少量的.java 文件裡有可能獲得數量眾多的.class 文件。

如以前用一種匯編語言寫過程序,那麼可能已習慣編譯器先分割出一種過渡形式(通常是一個.obj 文件),再用一個鏈接器將其與其他東西封裝到一起(生成一個可執行文件),或者與一個庫封裝到一起(生成一個庫)。但那並不是Java 的工作方式。一個有效的程序就是一系列.class 文件,它們可以封裝和壓縮到一個 JAR文件裡

“庫”也由一系列類文件構成。每個文件都有一個 public類(並沒強迫使用一個 public 類,但這種情況最很典型的),所以每個文件都有一個組件。如果想將所有這些組件(它們在各自獨立的.java 和.class文件裡)都歸納到一起,那麼package 關鍵字就可以發揮作用)。

若在一個文件的開頭使用下述代碼:

package mypackage;

那麼package語句必須作為文件的第一個非注釋語句出現。該語句的作用是指出這個編譯單元屬於名為mypackage 的一個庫的一部分。或者換句話說,它表明這個編譯單元內的public類名位於mypackage這個名字的下面。如果其他人想使用這個名字,要麼指出完整的名字,要麼與mypackage聯合使用import 關鍵字(使用前面給出的選項)。注意根據Java 包(封裝)的約定,名字內的所有字母都應小寫,甚至那些中間單詞亦要如此。

作為一名庫設計者,一定要記住package和import關鍵字允許我們做的事情就是分割單個全局命名空間,保證我們不會遇到名字的沖突——無論有多少人使用因特網,也無論多少人用Java 編寫自己的類。

 

1.1 創建獨一無二的包名

 

由於一個包永遠不會真的“封裝”到單獨一個文件裡面,它可由多個.class 文件構成,所以局面可能稍微有些混亂。為避免這個問題,最合理的一種做法就是將某個特定包使用的所有.class文件都置入單個目錄裡。這正是 Java 所采取的方法。

同時也解決了另兩個問題:創建獨一無二的包名以及找出那些可能深藏於目錄結構某處的類。

但根據約定,編譯器強迫package名的第一部分是類創建者的因特網域名。由於因特網域名肯定是獨一無二的(由InterNIC保證,它控制著域名的分配),所以假如按這一約定行事,package 的名稱就肯定不會重復,所以永遠不會遇到名稱沖突的問題。換句話說,除非將自己的域名轉讓給其他人,而且對方也按照相同的路徑名編寫Java 代碼,否則名字的沖突是永遠不會出現的。當然,如果你沒有自己的域名,那麼必須創造一個非常生僻的包名(例如自己的英文姓名),以便盡最大可能創建一個獨一無二的包名。如決定發行自己的Java代碼,那麼強烈推薦去申請自己的域名,它所需的費用是非常低廉的。

這個技巧的另一部分是將package 名解析成自己機器上的一個目錄。這樣一來,Java 程序運行並需要裝載.class 文件的時候(這是動態進行的,在程序需要創建屬於那個類的一個對象,或者首次訪問那個類的一個static 成員時),它就可以找到.class 文件駐留的那個目錄。

Java 解釋器的工作程序如下:首先,它找到環境變量CLASSPATH(將Java 或者具有Java 解釋能力的工具——如浏覽器——安裝到機器中時,通過操作系統進行設定)。CLASSPATH包含了一個或多個目錄,它們作為一種特殊的“根”使用,從這裡展開對.class文件的搜索。從那個根開始,解釋器會尋找包名,並將每個點號(句點)替換成一個斜槓,從而生成從CLASSPATH 根開始的一個路徑名(所以package foo.bar.baz 會變

成foo\bar\baz或者foo/bar/baz;具體是正斜槓還是反斜槓由操作系統決定)。隨後將它們連接到一起,成為CLASSPATH內的各個條目(入口)。以後搜索.class文件時,就可從這些地方開始查找與准備創建的類名對應的名字。此外,它也會搜索一些標准目錄——這些目錄與 Java 解釋器駐留的地方有關。

自Java 1.2 以來,整個包名都是小寫的. CLASSPATH 裡能包含大量備用的搜索路徑.

1.1.1自動編譯

為導入的類首次創建一個對象時(或者訪問一個類的static 成員時),編譯器會在適當的目錄裡尋找同名的.class 文件(所以如果創建類 X的一個對象,就應該是 X.class)。若只發現X.class,它就是必須使用的那一個類。然而,如果它在相同的目錄中還發現了一個X.java,編譯器就會比較兩個文件的日期標記。如果X.java 比X.class 新,就會自動編譯X.java,生成一個最新的X.class。

對於一個特定的類,或在與它同名的.java 文件中沒有找到它,就會對那個類采取上述的處理。

1.1.2沖突

若通過*導入了兩個庫,而且它們包括相同的名字.例如,假定一個程序使用了下述

導入語句:

import com.bruceeckel.util.*;

import java.util.*;

由於java.util.*也包含了一個Vector 類,所以這會造成潛在的沖突。然而,只要沖突並不真的發生,那麼就不會產生任何問題——這當然是最理想的情況,因為否則的話,就需要進行大量編程工作,防范那些可能可能永遠也不會發生的沖突。

如現在試著生成一個Vector。如下所示:

Vector v = new Vector();

它引用的到底是哪個Vector類呢?編譯器對這個問題沒有答案,讀者也不可能知道。所以編譯器會報告一個錯誤,強迫我們進行明確的說明。例如,假設我想使用標准的 Java Vector,那麼必須象下面這樣編程:

java.util.Vector v = newjava.util.Vector();

由於它(與CLASSPATH 一起)完整指定了那個Vector的位置,所以不再需要import java.util.*語句,除非還想使用來自java.util 的其他東西。

蛤蟆對JAVA的這塊設計不得不點個贊,相當NICE.

 

1.2 自定義工具庫

創建自己的工具庫,以便減少或者完全消除重復的代碼。

可為System.out.println()創建一個別名,減少重復鍵入的代碼量。

package com.toad;

 

publicclass P {

publicstaticvoid rint(Object obj) {

System.out.print(obj);

}

 

publicstaticvoid rint(String s) {

System.out.print(s);

}

 

publicstaticvoid rint(char[]s) {

System.out.print(s);

}

 

publicstaticvoid rint(charc) {

System.out.print(c);

}

 

publicstaticvoid rint(inti) {

System.out.print(i);

}

 

publicstaticvoid rint(longl) {

System.out.print(l);

}

 

publicstaticvoid rint(floatf) {

System.out.print(f);

}

 

publicstaticvoid rint(doubled) {

System.out.print(d);

}

 

publicstaticvoid rint(booleanb) {

System.out.print(b);

}

 

publicstaticvoid rintln() {

System.out.println();

}

 

publicstaticvoid rintln(Object obj) {

System.out.println(obj);

}

 

publicstaticvoid rintln(String s) {

System.out.println(s);

}

 

publicstaticvoid rintln(char[]s) {

System.out.println(s);

}

 

publicstaticvoid rintln(charc) {

System.out.println(c);

}

 

publicstaticvoid rintln(inti) {

System.out.println(i);

}

 

publicstaticvoid rintln(longl) {

System.out.println(l);

}

 

publicstaticvoid rintln(floatf) {

System.out.println(f);

}

 

publicstaticvoid rintln(doubled) {

System.out.println(d);

}

 

publicstaticvoid rintln(booleanb) {

System.out.println(b);

}

} // /:~

不同的數據類型現在都可以在一個新行輸出(P.rintln()),或者不在一個新行輸出(P.rint())。

這個文件所在的目錄必須從某個CLASSPATH位置開始。編譯完畢後,利用一個import 語句,即可在自己系統的任何地方使用P.class文件。

測試這個文件

import com.toad.*;

 

publicclass testtool {

publicstaticvoid main(String[] args) {

P.rintln("Available from now on!");

}

} ///:~

目錄如下圖:

 

輸出如下:

Availablefrom now on!

後續只要做出了一個有用的新工具,就可將其加入toad 目錄即可。

不過請務必保證對於類路徑的每個地方,每個名字都僅存在一個類。

 

1.3 利用導入改變行為

Java 已取消的一種特性是C 的“條件編譯”,它允許我們改變參數,獲得不同的行為,同時不改變其他任何代碼。Java 之所以拋棄了這一特性,可能是由於該特性經常在 C裡用於解決跨平台問題:代碼的不同部分根據具體的平台進行編譯,否則不能在特定的平台上運行。由於 Java 的設計思想是成為一種自動跨平台的語言,所以這種特性是沒有必要的。

然而,條件編譯還有另一些非常有價值的用途。一種很常見的用途就是調試代碼。調試特性可在開發過程中使用,但在發行的產品中卻無此功能。Alen Holub(www.holub.com)提出了利用包(package)來模仿條件編譯的概念。根據這一概念,它創建了C“斷定機制”一個非常有用的Java版本。之所以叫作“斷定機制”,是由於我們可以說“它應該為真”或者“它應該為假”。如果語句不同意你的斷定,就可以發現相關的情況。這種工具在調試過程中是特別有用的。

創建類如下:

package com.toad;

publicclass Assert {

privatestaticvoid perr(String msg) {

System.err.println(msg);

}

 

publicfinalstaticvoid is_true(booleanexp) {

if (!exp)

perr("Assertionfailed");

}

 

publicfinalstaticvoid is_false(booleanexp) {

if (exp)

perr("Assertionfailed");

}

 

publicfinalstaticvoid is_true(booleanexp, Stringmsg) {

if (!exp)

perr("Assertionfailed: " +msg);

}

 

publicfinalstaticvoid is_false(booleanexp, Stringmsg) {

if (exp)

perr("Assertionfailed: " +msg);

}

} // /:~

 

在不同路徑下創建Assert.java如下:

package com.debug;

 

publicclass Assert {

publicfinalstaticvoid is_true(booleanexp){}

publicfinalstaticvoid is_false(booleanexp){}

publicfinalstaticvoid

is_true(booleanexp, Stringmsg) {}

publicfinalstaticvoid

is_false(booleanexp, Stringmsg) {}

} ///:~

 

使用示例如下:

importcom.debug.*;

//import com.toad.*;

publicclass TestAssert {

public static void main(String[] args) {

Assert.is_true((2 + 2) == 5);

Assert.is_false((1 + 1) == 2);

Assert.is_true((2 + 2) == 5, "2 + 2 ==5");

Assert.is_false((1 + 1) == 2, "1 +1 !=2");

}

}///:~

通過改變導入的package,我們可將自己的代碼從調試版本變成最終的發行版本。這種技術可應用於任何種類的條件代碼。

1.4 包的停用

每次創建一個包後,都在為包取名時間接地指定了一個目錄結構。這個包必須存在(駐留)於由它的名字規定的目錄內。而且這個目錄必須能從CLASSPATH 開始搜索並發現。最開始的時候,package關鍵字的運用可能會令人迷惑,因為除非堅持遵守根據目錄路徑指定包名的規則,否則就會在運行期獲得大量莫名其妙的消息,指出找不到一個特定的類——即使那個類明明就在相同的目錄中。請試著將package 語句作為注釋標記出去。如果這樣做行得通,就可知道問題到底出在哪兒。

 

2 Java 訪問指示符

針對類內每個成員的每個定義,Java 訪問指示符 poublic,protected 以及private都置於它們的最前面——無論它們是一個數據成員,還是一個方法。每個訪問指示符都只控制著對那個特定定義的訪問。這與C++存在著顯著不同。在C++中,訪問指示符控制著它後面的所有定義,直到又一個訪問指示符加入為止。

 

2.1 “友好的”

如果根本不指定訪問指示符,默認的訪問沒有關鍵字,但它通常稱為“友好”(Friendly)訪問。這意味著當前包內的其他所有類都能訪問“友好的”成員,

但對包外的所有類來說,這些成員卻是“私有”(Private)的,外界不得訪問。由於一個編譯單元(一個文件)只能從屬於單個包,所以單個編譯單元內的所有類相互間都是自動“友好”的。因此,我們也說友好元素擁有“包訪問”權限。

友好訪問允許我們將相關的類都組合到一個包裡,使它們相互間方便地進行溝通。將類組合到一個包內以後,便“擁有”了那個包內的代碼。只有我們已經擁有的代碼才能友好地訪問自己擁有的其他代碼。我們可認為友好訪問使類在一個包內的組合顯得有意義,或者說前者是後者的原因。在許多語言中,我們在文件內組織定義的方式往往顯得有些牽強。但在Java 中,卻強制用一種頗有意義的形式進行組織。除此以外,我們有時可能想排除一些類,不想讓它們訪問當前包內定義的類。

對於任何關系,一個非常重要的問題是“誰能訪問我們的‘私有’或private代碼”。類控制著哪些代碼能夠訪問自己的成員。沒有任何秘訣可以“闖入”。另一個包內推薦可以聲明一個新類,然後說:“嗨,我是Bob的朋友!”,並指望看到Bob的“protected”(受到保護的)、友好的以及“private”(私有)的成員。為獲得對一個訪問權限,唯一的方法就是:

(1) 使成員成為“public”(公共的)。這樣所有人從任何地方都可以訪問它。

(2) 變成一個“友好”成員,方法是捨棄所有訪問指示符,並將其類置於相同的包內。這樣一來,其他類就可以訪問成員。

(3) 正如以後引入“繼承”概念後大家會知道的那樣,一個繼承的類既可以訪問一個 protected 成員,也可以訪問一個 public成員(但不可訪問 private成員)。只有在兩個類位於相同的包內時,它才可以訪問友好成員。但現在不必關心這方面的問題。

(4) 提供“訪問器/變化器”方法(亦稱為“獲取/設置”方法),以便讀取和修改值。

 

2.2 public:接口訪問

使用public關鍵字時,它意味著緊隨在public 後面的成員聲明適用於所有人,特別是適用於使用庫的客戶程序員。

創建類如下:

publicclass Dinner {

public Dinner() {

System.out.println("Dinner constructor");

}

publicstaticvoid main(String[] args) {

Cookie x =new Cookie();

//!x.foo(); // Can't access

}

} ///:~

創建類如下:

publicclass Cookie {

public Cookie() {

System.out.println("Cookieconstructor");

}

void foo() { System.out.println("foo"); }

} ///:~

創建一個Cookie對象,因為它的構建器是public的,而且類也是public的。然而,foo()成員不可在 Dinner.java 內訪問,因為foo()只有在包內才是“友好”的。

2.2.1默認包

可能會驚訝地發現下面這些代碼得以順利編譯。

 

classCake {

public static void main(String[] args) {

Pie x = new Pie();

x.f();

}

}///:~

 

在位於相同目錄的第二個文件裡:

 

//:Pie.java

//The other class

 

classPie {

void f() {System.out.println("Pie.f()"); }

}///:~

 

Cake 能創建一個 Pie對象,並能調用它的f()方法!通常的想法會認為Pie和f()是“友好的”,所以不適用於Cake。它們確實是友好的——這部分結論非常正確。但它們之所以仍能在Cake.java 中使用,是由於它們位於相同的目錄中,而且沒有明確的包名。Java 把象這樣

的文件看作那個目錄“默認包”的一部分,所以它們對於目錄內的其他文件來說是“友好”的。

 

2.3 private

private關鍵字意味著除非那個特定的類,而且從那個類的方法裡,否則沒有人能訪問那個成員。同一個包內的其他成員不能訪問 private成員,這使其顯得似乎將類與我們自己都隔離起來。另一方面,也不能由幾個合作的人創建一個包。所以private 允許我們自由地改變那個成員,同時毋需關心它是否會影響同一個包內的另一個類。默認的“友好”包訪問通常已經是一種適當的隱藏方法;請記住,對於包的用戶來說,是不能訪問一個“友好”成員的。這種效果往往能令人滿意,因為默認訪問是我們通常采用的方法。對於希望變成public(公共)的成員,我們通常明確地指出,令其可由客戶程序員自由調用。而且作為一個結果,最開始的時候通常會認為自己不必頻繁使用private關鍵字,因為完全可以在不用它的前提下發布自己的代碼(這與C++是個鮮明的對比)。private有非常重要的用途,特別是在涉及多線程處理的時候。

下面是應用了private 的一個例子:

package com.main;

class Sundae {

private Sundae() {System.out.println("privateSundate");}

static Sundae makeASundae() {

returnnew Sundae();

}

}

 

publicclass IceCream {

publicstaticvoid main(String[] args) {

//!Sundaex = new Sundae();

Sundae x =Sundae.makeASundae();

}

} ///:~

這個例子向我們證明了使用private的方便:有時可能想控制對象的創建方式,並防止有人直接訪問一個特定的構建器(或者所有構建器)。在上面的例子中,我們不可通過它的構建器創建一個Sundae 對象;相反,必須調用makeASundae()方法來實現。此時還會產生另一個影響:由於默認構建器是唯一獲得定義的,而且它的屬性是 private,所以可防止對這個類的繼承。

若確定一個類只有一個“助手”方法,那麼對於任何方法來說,都可以把它們設為private,從而保證自己不會誤在包內其他地方使用它,防止自己更改或刪除方法。將一個方法的屬性設為private後,可保證自己一直保持這一選項(然而,若一個句柄被設為 private,並不表明其他對象不能擁有指向同一個對象的public句柄。

2.4 protected

protected 關鍵字為我們引入了一種名為“繼承”的概念,它以現有的類為基礎,並在其中加入新的成員,同時不會對現有的類產生影響——我們將這種現有的類稱為“基礎類”或者“基本類”(Base Class)。亦可改變那個類現有成員的行為。對於從一個現有類的繼承,我們說自己的新類“擴展”(extends)了那個現有的類。如下所示:

class Foo extends Bar {

類定義剩余的部分看起來是完全相同的。

若新建一個包,並從另一個包內的某個類裡繼承,則唯一能夠訪問的成員就是原來那個包的public成員。當然,如果在相同的包裡進行繼承,那麼繼承獲得的包能夠訪問所有“友好”的成員。有些時候,基礎類的創建者喜歡提供一個特殊的成員,並允許訪問衍生類。這正是protected的工作。

擴展之前的cookie類如下:

package com.main;

publicclass ChocolateChipextends Cookie {

public ChocolateChip() {

System.out.println(

"ChocolateChipconstructor");

}

publicstaticvoid main(String[] args) {

ChocolateChip x =new ChocolateChip();

//!x.foo(); // Can't accessfoo

}

} ///:~

對於繼承,值得注意的一件有趣的事情是倘若方法 foo()存在於類Cookie 中,那麼它也會存在於從Cookie繼承的所有類中。但由於foo()在外部的包裡是“友好”的,所以我們不能使用它。當然,亦可將其變成public。但這樣一來,由於所有人都能自由訪問它,所以可能並非我們所希望的局面。

若象下面這樣修改類Cookie:

public class Cookie {

public Cookie() {

System.out.println("Cookie constructor");

}

protected void foo() {

System.out.println("foo");

}

}

那麼仍然能在包dessert裡“友好”地訪問 foo(),但從Cookie 繼承的其他東西亦可自由地訪問它。然而,它並非公共的(public)。

 

2.5 接口與實現

我們通常認為訪問控制是“隱藏實施細節”的一種方式。將數據和方法封裝到類內後,可生成一種數據類型,它具有自己的特征與行為。但由於兩方面重要的原因,訪問為那個數據類型加上了自己的邊界。

第一個原因是規定客戶程序員哪些能夠使用,哪些不能。我們可在結構裡構建自己的內部機制,不用擔心客戶程序員將其當作接口的一部分,從而自由地使用或者“濫用”。

這個原因直接導致了第二個原因:我們需要將接口同實施細節分離開。若結構在一系列程序中使用,但用戶除了將消息發給public接口之外,不能做其他任何事情,我們就可以改變不屬於 public 的所有東西(如“友好的”、protected 以及private),同時不要求用戶對他們的代碼作任何修改。我們現在是在一個面向對象的編程環境中,其中的一個類(class)實際是指“一類對象”。從屬於這個類的所有對象都共享這些特征與行為。“類”是對屬於這一類的所有對象的外觀及行為進行的一種描述。

在一些早期 OOP語言中,如 Simula-67,關鍵字class 的作用是描述一種新的數據類型。同樣的關鍵字在大多數面向對象的編程語言裡都得到了應用。

在Java中,類是最基本的OOP概念。為清楚起見,可考慮用特殊的樣式創建一個類:將 public成員置於最開頭,後面跟隨protected、友好以及private成員。這樣做的好處是類的使用者可從上向下依次閱讀,並首先看到對自己來說最重要的內容(即public成員,因為它們可從文件的外部訪問),並在遇到非公共成員後停止閱讀,後者已經屬於內部實施細節的一部分了。然而,利用由javadoc 提供支持的注釋文檔,代碼的可讀性問題已在很大程度上得到了解決。

 

由於接口和實施細節仍然混合在一起,所以只是部分容易閱讀。也就是說,仍然能夠看到源碼——實施的細節,因為它們需要保存在類裡面。向一個類的消費者顯示出接口實際是“類浏覽器”的工作。這種工具能查找所有可用的類,總結出可對它們采取的全部操作(比如可以使用哪些成員等),並用一種清爽悅目的形式顯示出來。

 

 

2.6 類訪問

在Java 中,可用訪問指示符判斷出一個庫內的哪些類可由那個庫的用戶使用。若想一個類能由客戶程序員調用,可在類主體的起始花括號前面某處放置一個 public關鍵字。它控制著客戶程序員是否能夠創建屬於這個類的一個對象。

為控制一個類的訪問,指示符必須在關鍵字class 之前出現。所以我們能夠使用:

public class Widget {

也就是說,假若我們的庫名是mylib,那麼所有客戶程序員都能訪問Widget——通過下述語句:

import mylib.Widget;

或者

import mylib.*;

然而,我們同時還要注意到一些額外的限制:

(1) 每個編譯單元(文件)都只能有一個public類。每個編譯單元有一個公共接口的概念是由那個公共類表達出來的。根據自己的需要,它可擁有任意多個提供支撐的“友好”類。但若在一個編譯單元裡使用了多個public類,編譯器就會向我們提示一條出錯消息。

(2) public類的名字必須與包含了編譯單元的那個文件的名字完全相符,甚至包括它的大小寫形式。所以對於Widget 來說,文件的名字必須是Widget.java,而不應是 widget.java或者WIDGET.java。同樣地,如果出現不符,就會報告一個編譯期錯誤。

(3) 可能(但並常見)有一個編譯單元根本沒有任何公共類。此時,可按自己的意願任意指定文件名。

將public 關鍵字從類中剔除即可,這樣便把類變成了“友好的”(類僅能在包內使用)。

注意不可將類設成private(那樣會使除類之外的其他東西都不能訪問它),也不能設成protected。因此,我們現在對於類的訪問只有兩個選擇:“友好的”或者public。若不願其他任何人訪問那個類,可將所有構建器設為private。這樣一來,在類的一個static 成員內部,除自己之外的其他所有人都無法創建屬於那個類的一個對象。

示例如下:

package com.main;

 

class Soup {

private Soup() {

System.out.println("privatesoup");

}

 

// (1) Allow creationvia static method:

publicstatic Soup makeSoup() {

returnnew Soup();

}

 

// (2) Create astatic object and

// return a referenceupon request.

// (The"Singleton" pattern):

privatestatic Soupps1 =new Soup();

 

publicstatic Soup access() {

returnps1;

}

 

publicvoid f() {

System.out.println("soup.f()");

}

}

 

class Sandwich {// Uses Lunch

void f() {

new Lunch();

}

}

 

// Only one public class allowed perfile:

publicclass Lunch {

void test() {

// Can't do this!Private constructor:

// ! Soup priv1 = newSoup();

Souppriv2 = Soup.makeSoup();

Sandwichf1 =new Sandwich();

Soup.access().f();

}

Lunch(){

System.out.println("Lunch");

}

} // /:~

Soup 類向我們展示出如何通過將所有構建器都設為 private,從而防止直接創建一個類。假若不明確地至少創建一個構建器,就會自動創建默認構建器(沒有自變量)。若自己編寫默認構建器,它就不會自動創建。把它變成private後,就沒人能為那個類創建一個對象。但別人怎樣使用這個類呢?上面的例子為我們揭示出了兩個選擇。

第一個選擇,我們可創建一個static 方法,再通過它創建一個新的Soup,然後返回指向它的一個句柄。如果想在返回之前對Soup 進行一些額外的操作,或者想了解准備創建多少個 Soup 對象(可能是為了限制它們的個數),這種方案無疑是特別有用的。

第二個選擇是采用“設計方案”(Design Pattern)技術。通常方案叫作“獨子”,因為它僅允許創建一個對象。類Soup 的對象被創建成Soup 的一個 static private 成員,所以有一個而且只能有一個。除非通過public 方法access(),否則根本無法訪問它。如果不針對類的訪問設置一個訪問指示符,那麼它會自動默認為“友好的”。這意味著那個類的對象可由包內的其他類創建,但不能由包外創建。對於相同目錄內的所有文件,如果沒有明確地進行package聲明,那麼它們都默認為那個目錄的默認包的一部分。然而,假若那個類一個static成員的屬性是public,那麼客戶程序員仍然能夠訪問那個static 成員——即使它們不能創建屬於那個類的一個對象。

添加測試文件test.java如下:

package com.main;

importcom.debug.*;

// import com.toad.*;

publicclass test {

publicstaticvoid main(String[] args) {

Lunch l=new Lunch();

l.test();

}

} ///:~

最後輸出如下:

Lunch

privatesoup

privatesoup

soup.f()

3 總結

創建一個庫時,相當於建立了同那個庫的用戶(即“客戶程序員”)的一種關系——那些用戶屬於另外的程序員,可能用我們的庫自行構建一個應用程序,或者用我們的庫構建一個更大的庫。

如果不制訂規則,客戶程序員就可以隨心所欲地操作一個類的所有成員,無論我們本來願不願意其中的一些成員被直接操作。所有東西都在別人面前都暴露無遺。

由於C僅有一個“命名空間”,所以名字會開始互相抵觸,從而造成額外的管理開銷。而在Java 中,package關鍵字、包命名方案以及import關鍵字為我們提供對名字的完全控制,所以命名沖突的問題可以很輕易地得到避免。

有兩方面的原因要求我們控制對成員的訪問。第一個是防止用戶接觸那些他們不應碰的工具。對於數據類型的內部機制,那些工具是必需的。但它們並不屬於用戶接口的一部分,用戶不必用它來解決自己的特定問題。所以將方法和字段變成“私有”(private)後,可極大方便用戶。因為他們能輕易看出哪些對於自己來說是最重要的,以及哪些是自己需要忽略的。這樣便簡化了用戶對一個類的理解。

進行訪問控制的第二個、也是最重要的一個原因是:允許庫設計者改變類的內部工作機制,同時不必擔心它會對客戶程序員產生什麼影響。最開始的時候,可用一種方法構建一個類,後來發現需要重新構建代碼,以便達到更快的速度。如接口和實施細節早已進行了明確的分隔與保護,就可以輕松地達到自己的目的,不要求用戶改寫他們的代碼。

利用Java 中的訪問指示符,可有效控制類的創建者。那個類的用戶可確切知道哪些是自己能夠使用的,哪些則是可以忽略的。但更重要的一點是,它可確保沒有任何用戶能依賴一個類的基礎實施機制的任何部分。作為一個類的創建者,我們可自由修改基礎的實施細節,這一改變不會對客戶程序員產生任何影響,因為他們不能訪問類的那一部分。

有能力改變基礎的實施細節後,除了能在以後改進自己的設置之外,也同時擁有了“犯錯誤”的自由。無論當初計劃與設計時有多麼仔細,仍然有可能出現一些失誤。由於知道自己能相當安全地犯下這種錯誤,所以可以放心大膽地進行更多、更自由的試驗。這對自己編程水平的提高是很有幫助的,使整個項目最終能更快、更好地完成。

一個類的公共接口是所有用戶都能看見的,所以在進行分析與設計的時候,這是應盡量保證其准確性的最重要的一個部分。但也不必過於緊張,少許的誤差仍然是允許的。若最初設計的接口存在少許問題,可考慮添加更多的方法,只要保證不刪除客戶程序員已在他們的代碼裡使用的東西。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved