1. 程式人生 > >Java程式設計思想第4版-第六章

Java程式設計思想第4版-第六章

第6章 訪問許可權控制

訪問控制(或隱藏具體實現)與“最初的實現並不恰當”有關。

所有優秀的作者,包括那些編寫軟體的程式設計師,都清楚其著作的某些部分直至重新創作的時候才變得完美,有時甚至要反覆重寫多次。如果你把一個程式碼段放到了某個位置,等過一會兒回頭再看時,有可能會發現有更好的方式去實現相同的功能。這正是重構的原動力之一,重構即重寫程式碼,以使得它更可讀、更易理解,並因此而更具可維護性。

但是,在這種修改和完善程式碼的願望之下,也存在著巨大的壓力。通常總是會有一些消費者(客戶端程式設計師)需要你的程式碼在某些方面保持不變。因此你想改變程式碼,而他們卻想讓程式碼保持不變。由此而產生了在面向物件設計中需要考慮的一個基本問題:“如何把變動的事物與保持不變的事物區分開來”。

這對類庫(library)而言尤為重要。該類庫的消費者必須依賴他所使用的那部分類庫,並且能夠知道如果類庫出現了新版本,他們並不需要改寫程式碼。從另一個方面來說,類庫的開發者必須有許可權進行修改和改進,並確保客戶程式碼不會因為這些改動而受到影響。

這一目標可以通過約定來達到。例如,類庫開發者必須同意在改動類庫中的類時不得刪除任何現有方法,因為那樣會破壞客戶端程式設計師的程式碼。但是,與之相反的情況會更加棘手。在有域(即資料成員)存在的情況下,類庫開發者要怎樣才能知道究竟都有哪些域已經被客戶端程式設計師所呼叫了呢?這對於方法僅為類的實現的一部分,因此並不想讓客戶端程式設計師直接使用的情況來說同樣如此。如果程式開發者想要移除舊的實現而要新增新的實現時,結果將會怎樣 呢?改動任何一個成員都有可能破壞客戶端程式設計師的程式碼。於是類庫開發者會手腳被縛,無法對任何事物進行改動。

為了解決這一問題,Java提供了訪問許可權修飾詞,以供類庫開發人員向客戶端程式設計師指明哪些是可用的,哪些是不可用的。訪問許可權控制的等級,從最大許可權到最小許可權依次為:public、protected、包訪問許可權(沒有關鍵詞)和private。根據前述內容,讀者可能會認為,作為一名類庫設計員,你會盡可能將一切方法都定為private,而僅向客戶端程式設計師公開你願意讓他們使用的方法。這樣做是完全正確的,儘管對於那些經常使用別的語言(特別是C語言)編寫程式並在訪問事物時不受任何限制的人而言,這與他們的直覺相違背。到了本章末,讀者將會信服Java的訪問許可權控制的價值。

不過,構件類庫的概念以及對於誰有權取用該類庫構件的控制問題都還是不完善的。其中仍舊存在著如何將構件捆綁到一個內聚的類庫單元中的問題。對於這一點,Java用關鍵字package加以控制,而訪問許可權修飾詞會因類是存在於一個相同的包,還是存在於一個單獨的包而受到影響。為此,要開始學習本章,首先要學習如何將類庫構件置於包中,然後就會理解訪問許可權修飾詞的全部含義。

6.1 包:庫單元

包內包含有一組類,它們在單一的名字空間之下被組織在了一起。

例如,在Java的標準釋出中有一個工具庫,它被組織在java.util名字空間之下。java.util中有一個叫做ArrayList的類,使用ArrayList的一種方式是用其全名java.util.ArrayList來指定。

//access/FullQualification.java
public class FullQualification
{
    public static void main(String[] args)
    {
        java.util.ArrayList list = new java.util.ArrayList();
    }
}///:~

這立刻就使程式變得很冗長了,因此你可能想轉而使用import關鍵字。如果你想要匯入單個的類,可以在import語句中命名該類:

import java.util.ArrayList;

//access/FullQualification.java
public class FullQualification
{
    public static void main(String[] args)
    {
        ArrayList list = new ArrayList();
    }
}///:~

現在,就可以不用限定地使用ArrayList了。但是,這樣做java.util中的其他類仍舊是都不可用的。要想匯入其中所有的類,只需要使用“*”,就像在本書剩餘部分的示例中所看到的那樣:

import java.util.*;

我們之所以要匯入,就是要提供一個管理名字空間的機制。所有類成員的名稱都是彼此隔離的。A類中的方法f()與B類中具有相同特徵標記(引數列表)的方法f()不會彼此衝突。但是如果類名稱相互衝突又該怎麼辦呢?假設你編寫了一個Stack類並安裝到了一臺機器上,而該機器上已經有了一個別人編寫的Stack類,我們該如何解決呢?由於名字之間的潛在衝突,在Java中對名稱空間進行完全控制併為每個類建立唯一的識別符號組合就成為了非常重要的事情。

到目前為止,書中大多數示例都存於單一檔案之中,並專為本地使用(local use)而設計,因而尚未受到包名的干擾。這些示例實際上已經位於包中了:即未命名包,或稱為預設包。這當然也是一種選擇,而且為了簡單起見,在本書其他部分都儘可能地使用了此方法。不過如果你正在準備編寫對在同一臺機器上共存的其他Java程式友好的類庫或程式的話,就需要專慮如何防止類名稱之間的衝突問題。

當編寫一個Java原始碼檔案時,此檔案通常被稱為編譯單元(有時也被稱為轉譯單元)。每個編譯單元都必須有一個字尾名Java,而在編譯單元內則可以有一個public類,該類的名稱必須與檔案的名稱相同(包括大小寫,但不包括檔案的字尾名.java)。每個編譯單元只能有一個public類,否則編譯器就不會接受。如果在該編譯單元之中還有額外的類的話,那麼在包之外的世界是無法看見這些類的,這是因為它們不是public類,而且它們主要用來為主public類提供支援。

6.1.1 程式碼組織

當編譯一個.java檔案時,在.java檔案中的每個類都會有一個輸出檔案,而該輸出檔案的名稱與.java檔案中每個類的名稱相同,只是多了一個字尾名.class。因此,在編譯少量.java檔案之後,會得到大量的.class檔案。如果用編譯型語言編寫過程式,那麼對於編譯器產生一箇中間檔案(通常是一個obj檔案),然後再與通過連結器(用以建立一個可執行檔案)或類庫產生器(librarian,用以建立一個類庫)產生的其他同類檔案捆綁在一起的情況,可能早已司空見慣。但這並不是Java的工作方式。 Java可執行程式是一組可以打包並壓縮為一個Java文件檔案(JAR,使用Java的jar文件生成器)的.class檔案。 Java直譯器負責這些檔案的查詢、裝載和解釋。

類庫實際上是一組類檔案。其中每個檔案都有一個public類,以及任意數量的非public類。因此每個檔案都有一個構件。如果希望這些構件(每一個都有它們自己的獨立的.java和.class檔案)從屬於同一個群組,就可以使用關鍵字package。

如果使用package語句,它必須是檔案中除註釋以外的第一句程式程式碼。在檔案起始處寫:package access;就表示你在宣告該編譯單元是名為access的類庫的一部分。或者換種說法,你正在宣告該編譯單元中的public類名稱是位於access名稱的保護傘下。任何想要使用該名稱的人都必須使用前面給出的選擇,指定全名或者與access結合使用關鍵字import。(請注意,Java包的命名規則全部使用小寫字母,包括中間的字也是如此。)

例如,假設檔案的名稱是MyClass.java.這就意味著在該檔案中有且只有一個public類,該類的名稱必須是MyClass(注意大小寫):

//: access/mypackage/MyClass.java

package access.mypackage;

public class MyClass
{
}///:~

現在,如果有人想用MyClass或者是access中的任何其他public類,就必須使用關鍵字import來使access中的名稱可用。另一個選擇是給出完整的名稱:

public class QualifiedMyClass
{
    public static void main(String[] args)
    {
        access.mypackage.MyClass m = new access.mypackage.MyClass();
    }
}

關鍵字Import可使之更加簡~:

import access.mypackage.*;

public class QualifiedMyClass
{
    public static void main(String[] args)
    {
        MyClass m = new MyClass();
    }
}

身為一名類庫設計員,很有必要牢記:package和import關鍵字允許你做的,是將單一的全域性名字空間分割開,使得無論多少人使用Internet以及Java開始編寫類,都不會出現名稱衝突問題。

6.1.2 建立獨一無二的包名

讀者也許會發現,既然一個包從未真正將被打包的東西包裝成單一的檔案,並且一個包可以由許多.class檔案構成,那麼情況就有點複雜了。為了避免這種情況的發生,一種合乎邏輯的做法就是將特定包的所有.class檔案都置於一個目錄下。也就是說,利用作業系統的層次化的檔案結構來解決這一問題。這是Java解決混亂問題的一種方式,讀者還會在我們介紹jar工具的時候看到另一種方式。

將所有的檔案收入一個子目錄還可以解決另外兩個問題:怎樣建立獨一無二的名稱以及怎樣查詢有可能隱藏於目錄結構中某處的類。這些任務是通過將.class檔案所在的路徑位置編碼成package的名稱來實現的。按照慣例,package名稱的第一部分是類的建立者的反順序的Internet域名。如果你遵照慣例,Internet域名應是獨—無二的,因此你的package名稱也將是獨一無二的,也就不會出現名稱衝突的問題了(也就是說,只有在你將自己的域名給了別人,而他又以你曾經使用過的路徑名稱來編寫Java程式程式碼時,才會出現衝突)。當然,如果你沒有自己的域名,你就得構造一組不大可能與他人重複的組合(例如你的姓名),來創立獨一無二的package名稱。如果你打算髮布你的Java程式程式碼,稍微花點力氣去取得一個域名,還是很有必要的。

此技巧的第二部分是把package名稱分解為你機器上的一個目錄。所以當Java程式執行並且需要載入.class檔案的時候,它就可以確定.class檔案在目錄上所處的位置。

Java直譯器的執行過程如下:首先,找出環境變數CLASSPATH(可以通過作業系統來設定,有時也可通過安裝程式一用來在你的機器上安裝Java或基於Java的工具一來設定)。CLASSPATH包含一個或多個目錄,用作查詢,class檔案的根目錄。從根目錄開始,直譯器獲取包的名稱並將每個句點替換成反斜槓,以從CLASSPATH根中產生一個路徑名稱(於是,package foo.bar.baz就變成為foo\bar\baz或foo/bar/baz或其他,這一切取決於作業系統)。得到的路徑會與CLASSPATH中的各個不同的項相連線,直譯器就在這些目錄中查詢與你所要建立的類名稱相關的.class檔案。(直譯器還會去查詢某些涉及Java直譯器所在位置的標準目錄。)

為了理解這一點,以我的域名MindView.net為例。把它的順序倒過來,並且將其全部轉換為小寫,net.mindview就成了我所建立的類的獨一無二的全域性名稱。(com、edu、org等副檔名先前在Java包中都是大寫的,但在Java2中一切都已改觀,包的整個名稱全都變成了小寫。)若我決定再建立一個名為simple的類庫,裁可以將該名稱進一步細分,於是我可以得到一個包的名稱如下:

package net.mindview.slmple:

現在,這個包名稱就可以用作下面兩個檔案的名字空間保護傘了:

//: net.mindview.slmple.Vector.java
// Creating a package
package net.mindview.slmple:

public class Vector
{
    public Vector
    {
        System.out.println("net.mindview.slmple.Vector");
    }
}

如前所述,package語句必須是檔案中的第一行非註釋程式程式碼。第二個檔案看起來也極其相似:

//: net.mindview.slmple.List.java
// Creating a package
package net.mindview.slmple:

public class List
{
    public List
    {
        System.out.println("net.mindview.slmple.List");
    }
}

這兩個檔案均被置於我的系統的子目錄下:

c:\DOC\JavaT\net\mindview\simple

(注意,在本書的每一個檔案中的第一行註釋都指定了該檔案在原始碼目錄樹中的位置,這個資訊將由針對本書的自動程式碼抽取工具使用。)

如果沿此路徑往回看,可以看到包的名稱com.bruceeckel.simple,但此路徑的第一部分怎樣辦呢?它將由環境變數CLASSPATH關照,在我的機器上是:

CLASSPATH=.; D:\JAVA\LIB;C: \DOC\JavaT

可以看到,CLASSPATH可以包含多個可供選擇的查詢路徑。

但在使用JAR檔案時會有一點變化。必須在類路徑中將JAR檔案的實際名稱寫清楚,而不僅
是指明它所在位置的目錄。因此,對於一個名為grape.jar的JAR檔案,類路徑應包括:

CLASSPATH=.; D:\JAVA\LIB;C: \flavors\grape.jar

一旦類路徑得以正確建立,下面的檔案就可以放於任何目錄之下:

//: access/LibTest.java
// Uses the llbrary
import net.mindview.slmple.*;

publlc class LibTest
{
    public static void main(String[] args)
    {
        Vector v=new Vector():
        List l-=new List():
    }
}/* Output
    net.mindview.simple.Vector
    net.mindview.simple.List    
    ///:~

當編譯器碰到simple庫的import語句時,就開始在CLASSPATH所指定的目錄中查詢,查詢子目錄net\mindview\simple,然後從已編譯的檔案中找出名稱相符者(對Vector而言是Vector.class,對List而言是List.class)。請注意,Vector和List中的類以及要使用的方法都必須是public的。

對於使用Java的新手而言,設立CLASSPATH是很麻煩的一件事(我最初使用時就是這樣的),為此,Sun將Java2中的JDK改造得更聰明瞭一些。在安裝後你會發現,即使你未設立CLASSPATH,你也可以編譯並執行基本的Java程式。然而,要編譯和執行本書的原始碼包(從www.MindView.net網站可以取得),就得向你的CLASSPATH中新增本書程式程式碼樹中的基目錄了。

  • 練習1:(1)在某個包中建立一個類,在這個類所處的包的外部建立該類的一個例項。

衝突

如果將兩個含有相同名稱的類庫以“*”形式同時匯入,將會出現什麼情況呢?例如,假設某程式這樣寫:

import net.mindview.slmple.*;
import java.util.*;

由於java.util也含有一個Vector類,這就存在潛在的衝突。但是隻要你不寫那些導致衝突的程式程式碼,就不會有什麼問題一這樣很好,否則就得做很多的型別檢查工作來防止那些根本不會出現的衝突。

如果現在要建立一個Vector類的話,就會產生衝突:

Vector v = new Vector();

這行到底取用的是哪個Vector類?編譯器不知道,讀者同樣也不知道。於是編譯器提出錯誤資訊,強制你明確指明。舉例說明,如果想要一個標準的Java Vector類,就得這樣寫:

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

由於這樣可以完全指明該Vector類的位置(配合CLASSPATH),所以除非還要使用java.util中的其他東西,否則就沒有必要寫import java.util.*語句了。

或者,可以使用單個類匯入的形式來防止衝突,只要你在同一個程式中沒有使用有衝突的名字(在使用了有衝突名字的情況下,必須返回到指定全名的方式)。

  • 練習2:(1)將本節中的程式碼片段改寫為完整的程式,並校驗實際所發生的衝突。

6.1.3 定製工具庫

具備了這些知識以後,現在就可以建立自己的工具庫來減少或消除重複的程式程式碼了。例如,我們已經用到的System.out.println()的別名可以減少輸入負擔,這種機制可以用於名為Print的類中,這樣,我們在使用該類時可以用一個更具可讀性的靜態import語句來匯入:

//: net/mindview/util/Print.jva
// Print methods that can be used without qualifiers, using Java SE5 static imports:
package net.mindview.util;

import java.io.*;

public class Print
{
    public static void print(Object obj)
    {
        System.out.println(obj);
    }

    public static void print()
    {
        System.out.println();
    }

    public static void printnb(Object obj)
    {
        System.out.print(obj);
    }

    public static PrintStream printf(String format,Object... args)
    {
        return System.out.printf(format,args);
    }
}

可以使用列印便捷工具來列印String,無論是需要換行(print())還是不需要換行(printnb())。

可以猜到,這個檔案的位置一定是在某個以一個CLASSPATH位置開始,然後接著是net/mindview的目錄下。編譯完之後,就可以用import static語句在你的系統上使用靜態的print()和printnb()方法了。

//: access/PrintTest.java
// Uses the static printing methods in Print.java
import static net.mindview.util.Print.*;

public class PrintTest
{
    public static void main(String[] args)
    {
       print("Available from now on!");
       print(100);
       print(100L);
       print(3.14159);
    }
}/*: Output:
    Available from now on!
    100
    100
    3.14159
    ///:~

這個類庫的第二個構件可以是在第4章中引入的range()方法,它使得foreach語法可以用於簡
單的整數序列:

//: net/mindview/util/Range.jva
package net.mindview.util;

public class Range
{
    public static int[] range(int count)
    {
        int[] ints = new int[count];
        for (int i = 0; i < count; i++)
            ints[i] = i;
        return ints;
    }

    public static int[] range(int start, int end)
    {
        int size = end - start;
        int[] ints = new int[size];
        for (int i = 0; i < size; i++)
            ints[i] = start + i;
        return ints;
    }

    public static int[] range(int start, int end, int step)
    {
        int size = (end - start) / step;
        int[] ints = new int[size];
        for (int i = 0; i < size; i++)
            ints[i] = start + i * step;
        return ints;
    }
}

從現在開始,你無論何時建立了有用的新工具,都可以將其新增到你自己的類庫中。你將看到在本書中還有更多的構件新增到了net.mindview.util類庫中。

6.1.4 用import改變行為

Java沒有C的條件編譯功能,該功能可以使你不必更改任何程式程式碼,就能夠切換開關併產生不同的行為。 Java去掉此功能的原因可能是因為C在絕大多數情況下是用此功能來解決跨平臺問題的,即程式程式碼的不同部分是根據不同的平臺來編譯的。由於Java自身可以自動跨越不同的平臺,因此這個功能對Java而言是沒有必要的。

然而,條件編譯還有其他一些有價值的用途。除錯就是一個很常見的用途。除錯功能在開發過程中是開啟的,而在釋出的產品中是禁用的。可以通過修改被匯入的package的方法來實現這一目的,修改的方法是將你程式中用到的程式碼從除錯版改為釋出版。這一技術可以適用於任何種類的條件程式碼。

練習3:(2)建立兩個包:debug和debugoff,它們都包含一個相同的類,該類有一個debug()方法。第一個版本顯示傳送給控制檯的String引數,而第二個版本什麼也不做。使用靜態import語句將該類匯入到一個測試程式中,並示範條件編譯效果。

6.1.5 對使用包的忠告

務必記住,無論何時建立包,都已經在給定包的名稱的時候隱含地指定了目錄結構。這個包必須位於其名稱所指定的目錄之中,而該目錄必須是在以CLASSPATH開始的目錄中可以查詢到的。最初使用關鍵字package,可能會有一點不順,因為除非遵守“包的名稱對應目錄路徑”的規則,否則將會收到許多出乎意料的執行時資訊,告知無法找到特定的類,哪怕是這個類就位於同一個目錄之中。如果你收到類似資訊,就用註釋掉package語句的方法試一下,如果這樣程式就能執行的話,你就可以知道問題出在哪裡了。

注意,編譯過的程式碼通常放置在與原始碼的不同目錄中,但是必須保證JVN使用CLASSPAH可以找到該路徑。

6.2 Java訪問許可權修飾詞

public、protected和private達幾個Java訪問許可權修飾詞在使用時,是置於類中每個成員的定義之前的一無論它是一個域還是一個方法。每個訪問許可權修飾詞僅控制它所修飾的特定定義的訪問權。

如果不提供任何訪問許可權修飾詞,則意味著它是“包訪問許可權”。因此,無論如何,所有事物都具有某種形式的訪問許可權控制。在以下幾節中,讀者將學習各種型別的訪問許可權。

6.2.1 包訪問許可權

本章之前的所有示例都沒有使用任何訪問許可權修飾詞。預設訪問許可權沒有任何關鍵字,但通常是指包訪問許可權(有時也表示成為friendly)。這就意味著當前的包中的所有其他類對那個成員都有訪問許可權,但對於這個包之外的所有類,這個成員卻是private。由於一個編譯單元(即一個檔案),只能隸屬於一個包,所以經由包訪問許可權,處於同一個編譯單元中的所有類彼此之間都是自動可訪問的。

包訪問許可權允許將包內所有相關的類組合起來,以使它們彼此之間可以輕鬆地相互作用。當把類組織起來放進一個包內之時,也就給它們的包訪問許可權的成員賦予了相互訪問的許可權,你“擁有”了該包內的程式程式碼。“只有你擁有的程式程式碼才可以訪問你所擁有的其他程式程式碼”,這是合理的。應該說,包訪問許可權為把類群聚在一個包中的做法提供了意義和理由。在許多語言中,在檔案內組織定義的方式是任意的,但在Java中,則要強制你以一種合理的方式對它們加以組織。另外,你可能還想要排除這樣的類一它們不應該訪問在當前包中所定義的類。

類控制著哪些程式碼有權訪問自己的成員。其他包內的類不能剛一上來就說:“嗨,我是Bob的朋友。”並且還想看到Bob的protected、包訪問許可權和private成員。取得對某成員的訪問權的唯一途徑是:

  1. 使該成員成為public()於是,無論是誰,無論在哪裡,都可以訪問該成員。
  2. 通過不加訪問許可權修飾詞並將其他類放置於同一個包內的方式給成員賦予包訪問權。於是包內的其他類也就可以訪問該成員了。
  3. 在第7章將會介紹繼承技術,屆時讀者將會看到繼承而來的類既可以訪問public成員也可以訪問protected成員(但訪問private成員卻不行)。只有在兩個類都處於同一個包內時,它才可以訪問包訪問許可權的成員。但現在不必擔心繼承和protected。
  4. 提供訪問器( accessor)和變異器(mutator)方法(也稱作get/set方法),以讀取和改變數值。正如將在第22章中看到的,對OOP而言,這是最優雅的方式,而且這也是JavaBeans的基本原理。

6.2.2 public:介面訪問許可權

使用關鍵字public,就意味著public之後緊跟著的成員宣告自己對每個人都是可用的,尤其是使用類庫的客戶程式設計師更是如此。假設定義了一個包含下面編譯單元的dessert包:

//: access/dessert/Cookie.java
// Creates a library.
package access.dessert;

public class Cookie
{
    public Cookie() 
    {
        System.out.println("Cookie  constructor");
    }

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

記住,Cookie.java檔案必須位於名為dessert的子目錄之中,該子目錄在access(意指本書第6章)下,而c05則必須位於CLASSPATH指定的眾多路徑的其中之一的下邊。不要錯誤地認為Java總是將當前目錄視作是查詢行為的起點之一。如果你的CLASSPATH之中缺少一個“.”作為路徑之一的話,Java就不會查詢那裡。

現在如果建立了一個使用Cookie的程式:

//: access/Dinner.jva
import access.dessert;
public class Dinner
{
    public static void main(String[] args)
    {
        Cookie c = new Cookie();
        //! c.bite(); //Can't access
    }
}/* Output
    Cookie  constructor
    ///:~

就可以建立一個Cookie物件,因為它的構造器是public而且類也是public的。(此後我們將會對public類的概念瞭解更多。)但是,由於bite()只向在dessert包中的類提供訪問權,所以bite()成員在Dinner.java之中是無法訪問的,因此編譯器也禁止你使用它。

預設包

令人吃驚的是,下面的程式程式碼雖然看起來破壞了上述規則,但它仍可以編譯:

//: access/Cake.jva
import access.dessert;
public class Cake
{
    public static void main(String[] args)
    {
        Pie p = new Pie ();
        p.f();
    }
}/* Output
    Pie.f()
    ///:~

在第二個處於相同目錄的檔案中:

//: access/Pie.jva
import access.dessert;
public class Pie
{
    void f()
    {
        System.out.println("Pie.f()");
    }
}///:~

最初或許會認為這兩個檔案毫不相關,但Cake卻可以建立一個Pie物件並呼叫它的f()方法!(記住,為了使檔案可以被編譯,在你的CLASSPATH之中一定要有“.”。)通常會認為Pie和f()享有包訪問許可權,因而是不可以為Cake所用的。它們的確享有包訪問許可權,但這只是部分正確的。Cake.java可以訪問它們的原因是因為它們同處於相同的目錄並且沒有給自己設定任何包名稱。Java將這樣的檔案自動看作是隸屬於該目錄的預設包之中,於是它們為該目錄中所有其他的檔案都提供了包訪問許可權。

6.2.3 private:你無法訪問

關鍵字private的意思是,除了包含該成員的類之外,其他任何類都無法訪問這個成員。由於處於同一個包內的其他類是不可以訪問private成員的,因此這等於說是自己隔離了自己。從另一方面說,讓許多人共同合作來建立一個包也是不大可能的,為此private就允許你隨意改變該成員,而不必考慮這樣做是否會影響到包內其他的類。

預設的包訪問許可權通常已經提供了充足的隱藏措施。請記住,使用類的客戶端程式設計師是無法訪問包訪問許可權成員的。這樣做很好,因為預設訪問許可權是一種我們常用的許可權,同時也是一種在忘記新增任何訪問許可權控制時能夠自動得到的許可權。因此,通常考慮的是,哪些成員是想要明確公開給客戶端程式設計師使用的,從而將它們宣告為public,而在最初,你可能不會認為自己經常會需要使用關鍵字private,因為沒有它,照樣可以工作。然而,事實很快就會證明,對private的使用是多麼的重要,在多執行緒環境下更是如此(正如將在第21章中看到的)。

此處有一個使用private的示例。

//: access/IceCream.java

class Sundae
{
    private Sundae()
    {       
    }

    static Sundae makeASundae()
    {       
        return new Sundae();
    }
}

public class IceCream
{
    public static void main(String[] args)
    {
        //! Sundae x = new Sundae();
        Sundae x = Sundae.makeASundae();
    }
}

這是一個說明private終有其用武之地的示例:可能想控制如何建立物件,並阻止別人直接訪問某個特定的構造器(或全部構造器)。在上面的例子中,不能通過構造器來建立Sundae物件,而必須調makeASundae()方法來達到此目的。

任何可以肯定只是該類的一個“助手”方法的方法,都可以把它指定為private,以確保不會在包內的其他地方誤用到它,於是也就防止了你會去改變或刪除這個方法。將方法指定為private確保了你擁有這種選擇權。

這對於類中的private域同樣適用。除非必須公開底層實現細目(此種境況很少見),否則就應該將所有的域指定為private。然而,不能因為在類中某個物件的引用是private,就認為其他的物件無法擁有該物件的public引用(參見本書的線上補充材料以瞭解有關別名機制的話題)。

6.2.4 protected:繼承訪問許可權

要理解protected的訪問許可權,我們在內容上需要作一點跳躍。首先,在本書介紹繼承(第7章)之前,讀者並不需真正理解本節的內容。但為了內容的完整性,這裡還是提供了一個簡要介紹和使用protected的示例。

關鍵字protected處理的是繼承的概念,通過繼承可以利用一個現有類一我們將其稱為基類,然後將新成員新增到該現有類中而不必碰該現有類。還可以改變該類的現有成員的行為。為了從現有類中繼承,需要宣告新類extends(擴充套件)了一個現有類,就像這樣:

class Foo extends Bar {

類定義中的其他部分看起來都是一樣的。

如果建立了一個新包,並自另一個包中繼承類,那麼唯一可以訪問的成員就是源包的public成員。(當然,如果在同一個包內執行繼承工作,就可以操縱所有的擁有包訪問許可權的成員。)有時,基類的建立者會希望有某個特定成員,把對它的訪問許可權賦予派生類而不是所有類。這要protected來完成這一工作。 protected也提供包訪問許可權,也就是說,相同包內的其他類可以訪問protected元素。

回顧一下先前的例子Cookie.java,就可以得知下面的類是不可以呼叫擁有包訪問許可權的成員bite()的:

//: access/ChocolateChip.java
import access.dessert.*;

public class ChocolateChip extends Cookie 
{
    public ChocolateChip() 
    {
        System.out.println ( "ChocolateChip constructor");
    }

    publlc void chomp() 
    {
        //! bite();// Can't access bite
    }

    public static void main(String[] args) 
    {
        ChocolateChip x = new ChocolateChip();
        x.chomp();
    }
} /* Output:
    Cookie constructor
    ChocolateChip constructor
    ///:~

有關繼承技術的一個很有趣的事情是,如果類Cooloe中存在一個方法bite()的話,那麼該方法同時也存在於任何一個從Cookie繼承而來的類中。但是由於bite()有包訪問許可權而且它位於另一個包內,所以我們在這個包內是無法使用它的。當然,也可以把它指定為public,但是這樣做所有的人就都有了訪問許可權,而且很可能這並不是你所希望的。如果我們將類Cookie像這樣加以更改:

//: access/cookie2/Cookie.java
package access.cookie2;

public class Cookie
{
    public Cookie
    {
        System.out.println("Cookie constructor");
    }

    protected void bite()
    {
        System.out.println("bite()");
    }
}

現在bite()對於所有繼承自Cookie的類而言,也是可以使用的。

//: access/ChocolateChip2.java
package access.cookie2.*;

public class ChocolateChip2 extends Cookie
{
    public ChocolateChip2 ()
    {
        System.out.println("ChocolateChip2 constructor");
    } 

    public void chomp()
    {
        bite();//Protected method
    }

    public static void main(String[] args)
    {
        ChocolateChip2 x = new ChocolateChip2 ();
        x.chomp();
    }
}/* Output:
    Cookie constructor
    ChocolateChip2 constructor
    bite
    ///:~

注意,儘管bite()也具有包訪問許可權,但是它仍舊不是public的。

  • 練習4:(2)展示protected方法具有包訪問許可權,但不是public。
  • 練習5:(2)建立一個帶有public,private,protected和包訪問許可權域以及方法成員的類。建立該類的一個物件,看看在你試圖呼叫所有類成員時,會得到什麼型別的編譯資訊。請注意,處於同一個目錄中的所有類都是預設包的一部分。
  • 練習6:(1)建立一個帶有protected資料的類。運用在第一個類中處理protected資料的方法在相同的檔案中建立第二個類。

6.3 介面和實現

訪問許可權的控制常被稱為是具體實現的隱藏。把資料和方法包裝進類中,以及具體實現的隱藏,帶共同披稱作是封裝。其結果是一個同時帶有特徵和行為的資料型別。

出於兩個很重要的原因,訪問許可權控制將許可權的邊界劃在了資料型別的內部。第一個原因是要設定客戶端程式設計師可以使用和不可以使用的界限。可以在結構中建立自己的內部機制,而不必擔心客戶端程式設計師會偶然地將內部機制當作是他們可以使用的介面的一部分。

這個原因直接引出了第二個原因,即將介面和具體實現進行分離。如果結構是用於一組程式之中,而客戶端程式設計師除了可以向介面傳送資訊之外什麼也不可以做的話,那麼就可以隨意更改所有不是public的東西(例如有包訪問許可權、protected和private的成員),而不會破壞客戶端程式碼。

為了清楚起見,可能會採用一種將public成員置於開頭,後面跟著protected、包訪問許可權和private成員的建立類的形式。這樣做的好處是類的使用者可以從頭讀起,首先閱讀對他們而言最為重要的部分(即public成員,因為可以從檔案外部呼叫它們),等到遇見作為內部實現細節的非public成員時停止閱讀:

//:access/OrganizedByAccess.java

public class OrganizedByAccess
{
    public vold publ() { /* ... */ }
    publlc vold pub2() { /* ... */ }
    publlc void pub3() { /* ... *, }
    private void privl() ( /, ... */ }
    private void priv2() { /* ... */ }
    private void priv3() { /*: -.. */ )
    private int i;
    //...
}///:~

這樣做僅能使程式閱讀起來稍微容易一些,因為介面和具體實現仍舊混在一起。也就是說,仍能看到原始碼一實現部分,因為它就在類中。另外,javadoc所提供的註釋文件功能降低了程式程式碼的可讀性對客戶端程式設計師的重要性。將介面展現給某個類的使用者實際上是類瀏覽器的任務。類瀏覽器是一種以非常有用的方式來查閱所有可用的類,並告訴你用它們可以做些什麼(也就是顯示出可用成員)的工具。在Java中,用Web瀏覽器瀏覽JDK文件可以得到使用類瀏覽器的相同效果。

6.4 類的訪問許可權

在Java中,訪問許可權修飾詞也可以用於確定庫中的哪些類對於該庫的使用者是可用的。如果希望某個類可以為某個客戶端程式設計師所用,就可以通過把關鍵字public作用於整個類的定義來達到目的。這樣做甚至可以控制客戶端程式設計師是否能建立一個該類的物件。

為了控制某個類的訪問許可權,修飾詞必須出現於關鍵字class之前。因此可以像下面這樣宣告:

public class Widget{

現在如果庫的名字是access,那麼任何客戶端程式設計師都可以通過下面的宣告訪問Widget:

import access.Widget;

import access.*;

然而,這裡還有一些額外的限制:

  1. 每個編譯單元(檔案)都只能有一個public類。這表示,每個編譯單元都有單一的公共介面,用public類來表現。該介面可以按要求包含眾多的支援包訪問許可權的類。如果在某個編譯單元內有一個以上的public類,編譯器就會紿出出錯資訊。

  2. public類的名稱必須完全與含有該編譯單元的檔名相匹配,包括大小寫。所以對於Widget而言,檔案的名稱必須是Widget.java,而不是widget.java或WIDGET.java.如果不匹配,同樣將得到編譯時錯誤。

  3. 雖然不是很常用,但編譯單元內完全不帶public類也是可能的。在這種情況下,可以隨意對檔案命名。(儘管隨意命名會使得人們在閱讀和維護程式碼時產生混淆。)

如果獲取了一個在access內部的類,用來完成Widget或是其他在access中的public類所要執行的任務,將會出現什麼樣的情況呢?你不想自找麻煩去為客戶端程式設計師建立說明文件,而且你認為不久可能會想要完全改變原有方案並將舊版本一起刪除,代之以一種不同的版本。為了保留此靈活性,需要確保客戶端程式設計師不會依賴於隱藏在access之中的任何特定實現細節。為了達到這一點,只需將關鍵字public從類中拿掉,這個類就擁有了包訪問許可權。(該類只可以用於該包之中。)

  • 練習7:(1)根據描述access和Widget的程式碼片段建立類庫。在某個不屬於access類庫的類中建立一個Widget例項。

在建立一個包訪問許可權的類時,仍舊是在將該類的域宣告為private時才有意義一應儘可能地總是將域指定為私有的,但是通常來說,將與類(包訪問許可權)相同的訪問許可權賦予方法也是很合理的。既然一個有包訪問許可權的類通常只能被用於包內,那麼如果對你有強制要求,在此種情況下,編譯器會告訴你,你只需要將這樣的類的方法設定為public就可以了。

請注意,類既不可以是private的(這樣會使得除該類之外,其他任何類都不可以訪問它),也不可以是protected的。所以對於類的訪問許可權,僅有兩個選擇:包訪問許可權或public。如果不希望其他任何人對該類擁有訪問許可權,可以把所有的構造器都指定為private,從而阻止任何人建立該類的物件,但是有一個例外,就是你在該類的static成員內部可以建立。下面是一個示例:

——注:事實上,一個內部類可以是private或是protected的,但那是一個特例.這將在第10章中介紹到。

//: access/Lunch.java

class Soup1
{
    private Soup1()
    {
    }

    public static Soup1 makeSoup()
    {
        return new Soup1();
    }
}

class Soup2
{
    private Soup2()
    {
    }

    private static Soup2 pas1 = new Soup2();

    public static Soup2 access()
    {
        return pas1 ;
    }

    public void f()
    {
    }
}

public class Lunch
{
    void testPrivate()
    {
        //! Soup1  soup = new Soup1();
    }
    void testStatic()
    {
        Soup1  soup = Soup1.makeSoup();
    }
    void testSingleton()
    {
        Soup2.access().f();
    }
}

到目前為止,絕大多數方法均返回void或基本型別,所以定義

public static Soup1 makeSoup()
{
    return new Soup1();
}

初看起來可能有點令人迷惑不觶。方法名稱( makeSoup)前面的詞Soup1告知了該方法返回的東西。本書到目前為止,這裡經常是void,意思是它不返回任何東西。但是也可以返回一個物件引用,示例中就是這種情況。這個方法返回了一個對Soup1類的物件的引用。

Soup1類和Soup2類展示瞭如何通過將所有的構造器指定為private來阻止直接建立某個類的例項。請一定要牢記,如果沒有明確地至少建立一個構造器的話,就會幫你建立一個預設構造器(不帶有任何引數的構造器)。如果我們自己編寫了預設的構造器,那麼就不會自動建立它了。如果把該構造器指定為private,那麼就誰也無法建立該類的物件了。但是現在別人該怎樣使用這個類呢?上面的例子就給出了兩種選擇:在Soup1中,建立一個static方法,它建立一個新的Soup1物件並返回一個對它的引用。如果想要在返回引用之前在Soup1上做一些額外的工作,或是如果想要記錄到底建立了多少個Soup1物件(可能要限制其數量),這種做法將會是大有裨益的。

Soup2用到了所謂的設計模式,該模式在www.MindView.net網站《Thinking in Patterns (with Java)》>一書中有所介紹。這種特定的模式被稱為singleton(單例),這是因為你始終只能建立它的一個物件。Soup2類的物件是作為Soup2的一個static private成員而建立的,所以有且僅有一個,而且除非是通過public方法access(),否則是無法訪問到它的。

正如前面所提到的,如果沒能為類訪問許可權指定一個訪問修飾符,它就會預設得到包訪問許可權。這就意味著該類的物件可以由包內任何其他類來建立,但在包外則是不行的。(一定要記住,相同目錄下的所有不具有明確package宣告的檔案,都被視作是該目錄下預設包的一部分。) 然而,如果該類的某個static成員是public的話,則客戶端程式設計師仍舊可以呼叫該static成員,儘管他們並不能生成該類的物件。

  • 練習8:(4)效仿示例Lunch.java的形式,建立一個名為ConnectonManager的類,該類管理一個元素為Connection物件的固定陣列。客戶端程式設計師不能直接建立Connection物件,而只能通過ConnectionManager中的某個static方法來獲取它們。當ConnectionManager之中不再有物件時,它會返回null引用。在main()之中檢測這些類。
  • 練習9:(2)在access/local目錄下編寫以下檔案(假定access/local目錄在你的CLASSPATH中):
//: access/local/PackagedClass.java
package access.local;

class PackagedClass
{
    publlc PackagedClass()  
    {
        System.out.println("Creating a packaged class");
    }
}
然後在access/local之外的另一個目錄中建立下列檔案:
//: access/foreign/Foreign.java
package access.foreign;
import access.local.*;

public class Foreign
{
    public static void main(String[] args)
    {
        PackagedClass pc = new PackagedClass();
    }
}
解釋一下為什麼編譯器會產生錯誤。如果將Foreign類置於access.local包之中的話,會有所改變嗎?

6.5 總結

無論是在什麼樣的關係之中,設立一些為各成員所遵守的界限始終是很重要的。當建立了一個類庫,也就與該類庫的使用者建立了某種關係,這些使用者就是客戶端程式設計師,他們是另外一些程式設計師,他們將你的類庫聚合成為一個應用程式,或是運用你的類庫來建立一個更大的類庫。 如果不制定規則,客戶端程式設計師就可以對類的所有成員隨心而為,即使你可能並不希望他們直接複製其中的一些成員。在這種情況下,所有事物都是公開的。

本章討論了類是如何被構建成類庫的:首先,介紹了一組類是如何被打包到一個類庫中的;其次,類是如何控制對其成員的訪問的。

據估計,用C語言開發專案,在50千行至100千行程式碼之間就會出現問題。這是因為C語言僅有單一的“名字空間”,並且名稱開始發生衝突,引發額外的管理開銷。而對於java,關鍵字package、包的命名模式和關鍵字import,可以使你對名稱進行完全的控制,因此名稱衝突的問題是很容易避免的。

控制對成員的訪問許可權有兩個原因。第一是為了使使用者不要碰觸那些他們不該碰觸的部分,這些部分對於類內部的操作是必要的,但是它並不屬於客戶端程式設計師所需介面的一部分。因此,將方法和域指定成private,對客戶端程式設計師而言是一種服務。因為這樣他們可以很清楚地看到什麼對他們重要,什麼是他們可以忽略的。這樣簡化了他們對類的理解。

第二個原因,也是最重要的原因,是為了讓類庫設計者可以更改類的內部工作方式,而不必擔心這樣會對客戶端程式設計師產生重大的影響。例如,最初可能會以某種方式建立一個類,然後發現如果更改程式結構,可以大大提高執行速度。如果介面和實現可以被明確地隔離和加以保護,那麼就可以實現這一目的,而不必強制客戶端程式設計師重新編寫程式碼。訪問許可權控制可以確保不會有任何客戶端程式設計師依賴於某個類的底層實現的任何部分。

當具備了改變底層實施細節的能力時,不僅可以隨意地改善設計,還可能會隨意地犯錯誤,同時也就有了犯錯的可能性。無論如何細心地計劃並設計,都有可能犯錯。當了解到你所犯錯誤是相對安全的時候,就可以更加放心地進行實驗,也就可以更快地學會,更快地完成專案。

類的公共介面是使用者真正能夠看到的,所以這一部分是在分析和設計的過程中決定該類是否正確的最重要的部分。儘管如此,你仍然有進行改變的空間。如果在最初無法創建出正確的介面,那麼只要不刪除任何客戶端程式設計師在他們的程式中已經用到的東西,就可以在以後新增更多的方法。

注意,訪問許可權控制專注於類庫建立者和該類庫的外部使用者之間的關係,這種關係也是一種通訊方式。然而,在許多情況下事情並非如此。例如,你自己編寫了所有的程式碼,或者你在一個組員聚集在一起的專案組中工作,所有的東西都放在同一個包中。這些情況是另外一種不同的通訊方式,因此嚴格地遵循訪問許可權規則並不一定是最佳選擇,預設(包)訪問許可權也許只是可行而已。

所選習題的答案都可以在名為The Thinking in Java Annotated Solution Guide的電子文件中找到,讀者可以從www.MindView.net購買此文件。