thinking in Java 之訪問權限控制

如何將發生變化的東西與保持不變的東西分隔開—這一點對於庫開說特別重要,庫的創建者必須能自由的修改和改進代碼,同時客戶程序員不受到那些變動的影響。例如,庫程序員在修改庫內的一個類是,必須保證不刪除已有的方法,因爲那樣客戶程序員代碼會出現斷點。然而,對於數據成員,庫的創建者怎樣才知道哪些數據成員已受到客戶程序員的訪問呢?若方法屬於某個類唯一的一部分,而且不一定由客戶程序員直接使用,那麼這種痛苦的情況是真實的。如果庫的創建者想刪除一種舊的方案,並置入新的代碼,此時該腫麼辦呢?對那些成員進行的任何改動都可能中斷客戶程序員的代碼。
爲了解決這個問題,Java推出了“訪問指示符”的概念,聲明哪些東西是客戶程序員可以使用的,哪些是不可以使用的。訪問指示符有public、”友好的“(無關鍵字)、protected和private。

1.包:庫單元
當用import關鍵字導入一個完整的庫時,就會獲得”包“(package)。例如:import java.util.*;它的作用是導入完整的Utility庫。通過導入特定的包,類成員的名字相互都隔離起來,這樣位於A內的一個方法f()不會與位於B內的擁有相同自變量列表的f()發生衝突。但是類名會不會衝突呢?假設創建一個stack類,將它安裝到已有一個stack類(別人編寫)的機器上,類在運行的適合會自動下載。由於名字存在潛在衝突,所以必要對Java中命名空間進行完整控制,而且需要創建一個獨一無二的名字。
爲Java創建一個源碼文件的時候,它通常叫做一個”編輯單元“。每個編輯單元都必須有一個以.java結尾的名字。而且在編輯單元的內部,可以有一個public類,它必須與文件相同名字(包括大小寫形式,但排除.java文件擴展名),否則編譯器報錯。並且每個編譯單元內都只能有一個public類,否則編譯器會報錯。在編譯單元內如果有其他類,這些類不能夠被編譯單元之外的內容訪問,因爲它們非public。
編譯一個.java文件時,我們會獲得一個名字完全相同的輸出文件,但對於.java中每一個類,它們都有一個.class擴展名。一個有效的程序就是一系列.class文件,它們可以封裝和壓縮到一個JAR文件裏。
”庫“也由一系列文件構成。每個文件都有一個public類,所以每個文件都有一個組件。如果想將這些組件(它們在各自獨立的.java和.class文件裏)都歸納到一起,那麼package關鍵字就可以發揮作用。
如果在一個文件的開頭使用package mypackage;那麼package語句必須作爲文件的第一個非註釋語句出現,該語句指出這個編譯單元屬於名爲mypackage的庫的一部分。換句話說,它表明這個編譯單元內的public類名位於mypackage這個名字下面。如果其他人想使用這個名字,要麼之處完整的名字,要麼與mypackage聯合使用import關鍵字。注意包名必須小寫。
例如,假定文件名是MyClass.java。它意味着在那個文件有一個、而且只能有一個public類。而且那個類的名字必須是MyClass(包括大小寫形式):

package mypackage;
public class MyClass {
// …

現在,如果有人想使用MyClass,或者想使用mypackage內的其他任何public類,他們必須用import關鍵字激活mypackage內的名字,使它們能夠使用。另一個辦法則是指定完整的名稱:

mypackage.MyClass m = new mypackage.MyClass();

import關鍵字則可將其變得簡潔得多:

import mypackage.*;
// …
MyClass m = new MyClass();

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

1.1 創建獨一無二的包名
由於一個包永遠不會真的”封裝“到單獨一個文件裏面,它可由多個.class文件構成,我們可以利用操作系統的分級文件避免文件結構混亂。同時也解決了另兩個問題:創建獨一無二的包名以及找出那些深藏於目錄結構某處的類。
Java解析器的工作程序如下:首先找到環境變量CLASSPATH,CLASSPATH包含一個或多個目錄,它們作爲特殊的“根”使用,從這裏展開對.class文件的搜索。從那個根開始,解析器會尋找包名,並將每個點號替換成一個斜槓,從而生成CLASSPATH根開始的一個路徑名。以後搜索.class文件時,就可從這些地方開始查找與準備創建的類名對應的名字。
下面舉個栗子,bruceeckel.com。將其反轉過來後,com.bruceeckel就爲我的類創建了獨一無二的全局名稱(com,edu,org,net)。由於決定創建一個名爲util的庫,我可以進一步地分割它,所以最後得到的包名如下:

package com.bruceeckel.util;
package com.bruceeckel.util;
public class Vector {
    pubilc Vector() {       System.out.println("com.bruceeckel.util.Vector");
    }
}
package com.bruceeckel.util;
public class List {
    pubilc List() {
        System.out.println("com.bruceeckel.util.List");
    }
}

這兩個文件置於我自己系統的一個子目錄中:C:\DOC\JavaT\com\bruceeckel\util。可以看出我的包名爲com.bruceeckel.util,但路徑的第一部分是什麼呢?這是由CLASSPATH環境變量決定的。在我的機器上,它是CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT 可以看出,CLASSPATH包含大量備用的搜索路徑。然而,使用JAR文件時需要注意,必須將JAR文件的名字置於類路徑裏,而不僅僅是它所在的路徑。所以對一個名爲grape.jar的JAR文件來說,我們的類路徑需要包括:CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar
針對上面代碼,編譯器遇到import語句後,它會搜索由CLASSPATH指定的目錄,查找子目錄com\bruceckel\util,然後查找名稱適當的已編譯文件(對於Vector是Vector.class,對於List則是List.class)。
1.自動編譯
爲導入的類首次創建一個對象或訪問一個類的static成員時,編譯器會在適當地目錄裏尋找同名的.class文件(如果創建類X的一個對象,就應該是X.class)。若只發現X.class,它就是必須使用的那一個類。如果在相同的目錄中還發現了一個X.java,編譯器會比較兩個文件的日期標記。如果X.java比X.class新,就會自動編譯X.java,生成一個最新的X.class。對於一個特定的類,或在與它同名的.java文件中沒有找到它,就會對那個類採取上述的處理。
2.衝突
若通過*導入了兩個庫,而且它們包括相同的名字,這時會出現什麼情況呢?例如
import com.bruceeckel.util.*;
import java.util.*;
由於java.util.也包含了一個Vector類,所以這會有潛在衝突。然而,只要衝突並不真的發生,那麼就不會產生任何問題。如果現在試着生成一個Vector,就肯定發生衝突。如Vector Vector = new Vector();它的引用到底是指向哪個Vector呢?編譯器不知道,所以它會報告一個錯誤,強迫我們進行明確說明。例如我們想使用標準的Java Vector,那麼就必須這樣編程:java.util.Vector v = new java.util.Vector();由於它(與CLASSPATH一起)完整指定了那個Vector的位置,所以不再需要import java.util.語句,除非還想使用來自java.util的其他東西。

3.CLASSPATH的陷阱
假如我們新建了一個P.java,編寫程序的時候我引入了P.java,它最初看起來似乎工作正常,但是某些情況下卻開始中斷,在很長時間我都覺得是Java或其他什麼在實現時的一個錯誤。但最後發現在一個地方引入了一個程序,使用了一個不同的類P。由於它作爲一個工具使用,所以有時候會進入類路徑裏;另一些時候則不會這樣。但只要它進入類路徑,那麼假若執行的程序需要尋找com.bruceeckel.tools中的類,Java首先發現的就是CodePackager.java中的P。此時,編譯器會報告一個特定的方法沒有找到。乍一看來,這似乎是編譯器的一個錯誤,但假若考察import語句,就會發現它只是說:“在這裏可能發現了P”。然而,我們假定的是編譯器搜索自己類路徑的任何地方,所以一旦它發現一個P,就會使用它;若在搜索過程中發現了“錯誤的”一個,它就會停止搜索。這與我們在前面表述的稍微有些區別,因爲存在一些討厭的類,它們都位於包內。而這裏有一個不在包內的P,但仍可在常規的類路徑搜索過程中找到。
如果您遇到象這樣的情況,請務必保證對於類路徑的每個地方,每個名字都僅存在一個類。

2.2 Java訪問指示符
針對類內每個成員的每個定義,Java訪問指示符public、protected、private都可置於它們的最前面–無論它們是一個數據成員還是一個方法。
2.2.1 “友好的”
如果不指定訪問指示符,通常被稱爲”友好“(Friendly)訪問。意味着當前包內的其他所有類都能訪問”友好的“成員,但對包外的所有類來說,這些成員卻是”私有“(Private)的,外界不得訪問。由於一個編譯單元只能從屬於單個包,所以單個編譯單元內的所有類相互間都市自動”友好“的。因此我們也說友好元素擁有”包訪問“權限。
友好訪問允許我們將相關的類都組合到一個包裏,使它們互相間方便地進行溝通。將類組合到一個包內以後,我們便”擁有“了那個包內的代碼。

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

2.2.3 private
private關鍵字意味着只有從那個類裏,其他沒有人能訪問這個成員。

2.3.4 protected
protected關鍵字爲我們引入了”繼承“的概念,它以現有的類爲基礎,其他類”擴展“(extends)現有類,同時不會對現有的類產生影響,現有的類被稱爲”基本類“(Base Class)。
若新建一個包,並從另一個包內的某個類裏繼承,則唯一能夠訪問的成員就是那個原來那個包的public成員。如果在相同的包裏進行繼承,那麼繼承獲得的包能夠訪問所有”友好“的成員。有時候,基礎類的創建者喜歡提供一個特殊的成員,並允許訪問衍生類。這正是protected的工作。
對於繼承,值得注意的一件事情,若方法foo()存在於類Cookie中,那麼也會存在於從Cookie繼承的所有類中。

2.3 接口與實現
訪問權限的控制常被稱爲具體實現的隱藏。把數據和方法包裝進類中,以及具體實現的隱藏,常被稱爲封裝。其結果是一個同時帶有特徵和行爲的數據類型。
處於兩個重要原因,訪問權限控制將權限的邊界劃在了數據類型的內部。第一個原因是要設定客戶端程序員可以使用和不可以使用的界限。可以在結構中建立自己的內部機制,而不必擔心客戶端程序員會偶然的將內部機制當作是他們可以使用的接口的一部分。這個原因直接引出了第二個原因,即將接口和具體實現進行分離。如果結構是用於一組程序之中,而客戶端程序員出了可以向接口發送信息之外什麼也不可以做的話,那麼就可以隨意更改所有不是public的東西(例如包訪問權限、protected和private成員),而不會破壞客戶端代碼。
爲了清楚起見,可能會採用一種將public成員置於開頭,後面跟着protected、包訪問權限和private成員的創建類的形式。這樣做的好處是類的使用者可以從頭讀起,首先閱讀對他們而言最爲重要的部分(即public成員,因爲可以從文件外部調用),等到遇見作爲內部實現細節的非public成員時停止閱讀。
這樣僅可以使程序閱讀起來稍微容易一些,因爲接口和具體實現仍舊混在一起。也就是說仍能看到源碼代碼–實現部分,因爲它就在類中。另外,javadoc所提供的註釋文檔功能降低了程序代碼的可讀性對客戶端程序員的重要性。

2.4 類的訪問權限
在Java中,訪問權限修飾詞也可以用於確定庫中的哪些類對於該庫中使用者是可用的。如果希望某個類可以爲某個客戶端程序員所用,就可以通過把關鍵字public作用於整個類的定義來達到目的。這樣做甚至可以控制客戶端程序員是否能創建一個該類的對象。
然而,這裏還有一些額外的限制:
1.每個編譯單元都只能有一個public類。這表示,每個編譯單元都有單一的公共接口,用public類來表示。該接口可以按要求包含衆多的支持包訪問權限的類。如果在某個編譯單元內有一個以上的public類,編譯器就會給出出錯信息。
2.public類的名稱必須完全與含有該編譯單元的文件名相匹配,包括大小寫。
3.雖然不是很常用,但編譯單元內完全不帶public類也是有可能的。在這種情況下,可以隨意對文件命名,但不建議。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章