初始化和清除1
“隨著計算機的進(jìn)步,‘不安全’的程序設(shè)計已成為造成編程代價高昂的罪魁禍?zhǔn)字弧?#8221;
“初始化”和“清除”是這些安全問題的其中兩個。許多C程序的錯誤都是由于程序員忘記初始化一個變量造成的。對于現(xiàn)成的庫,若用戶不知道如何初始化庫的一個組件,就往往會出現(xiàn)這一類的錯誤。清除是另一個特殊的問題,因為用完一個元素后,由于不再關(guān)心,所以很容易把它忘記。這樣一來,那個元素占用的資源會一直保留下去,極易產(chǎn)生資源(主要是內(nèi)存)用盡的后果。
C++為我們引入了“構(gòu)建器”的概念。這是一種特殊的方法,在一個對象創(chuàng)建之后自動調(diào)用。Java也沿用了這個概念,但新增了自己的“垃圾收集器”,能在資源不再需要的時候自動釋放它們。本章將討論初始化和清除的問題,以及Java如何提供它們的支持。
4.1
用構(gòu)建器自動初始化
對于方法的創(chuàng)建,可將其想象成為自己寫的每個類都調(diào)用一次initialize()。這個名字提醒我們在使用對象之前,應(yīng)首先進(jìn)行這樣的調(diào)用。但不幸的是,這也意味著用戶必須記住調(diào)用方法。在Java中,由于提供了名為“構(gòu)建器”的一種特殊方法,所以類的設(shè)計者可擔(dān)保每個對象都會得到正確的初始化。若某個類有一個構(gòu)建器,那么在創(chuàng)建對象時,Java會自動調(diào)用那個構(gòu)建器——甚至在用戶毫不知覺的情況下。所以說這是可以擔(dān)保的!
接著的一個問題是如何命名這個方法。存在兩方面的問題。第一個是我們使用的任何名字都可能與打算為某個類成員使用的名字沖突。第二是由于編譯器的責(zé)任是調(diào)用構(gòu)建器,所以它必須知道要調(diào)用是哪個方法。C++采取的方案看來是最簡單的,且更有邏輯性,所以也在Java里得到了應(yīng)用:構(gòu)建器的名字與類名相同。這樣一來,可保證象這樣的一個方法會在初始化期間自動調(diào)用。
下面是帶有構(gòu)建器的一個簡單的類(若執(zhí)行這個程序有問題,請參考第3章的“賦值”小節(jié))。
現(xiàn)在,一旦創(chuàng)建一個對象:
new
Rock();
就會分配相應(yīng)的存儲空間,并調(diào)用構(gòu)建器。這樣可保證在我們經(jīng)手之前,對象得到正確的初始化。
請注意所有方法首字母小寫的編碼規(guī)則并不適用于構(gòu)建器。這是由于構(gòu)建器的名字必須與類名完全相同!
和其他任何方法一樣,構(gòu)建器也能使用自變量,以便我們指定對象的具體創(chuàng)建方式。可非常方便地改動上述例子,以便構(gòu)建器使用自己的自變量。如下所示:
利用構(gòu)建器的自變量,我們可為一個對象的初始化設(shè)定相應(yīng)的參數(shù)。舉個例子來說,假設(shè)類Tree有一個構(gòu)建器,它用一個整數(shù)自變量標(biāo)記樹的高度,那么就可以象下面這樣創(chuàng)建一個Tree對象:
tree
t = new Tree(12); //
12英尺高的樹
若Tree(int)是我們唯一的構(gòu)建器,那么編譯器不會允許我們以其他任何方式創(chuàng)建一個Tree對象。
構(gòu)建器有助于消除大量涉及類的問題,并使代碼更易閱讀。例如在前述的代碼段中,我們并未看到對initialize()方法的明確調(diào)用——那些方法在概念上獨立于定義內(nèi)容。在Java中,定義和初始化屬于統(tǒng)一的概念——兩者缺一不可。
構(gòu)建器屬于一種較特殊的方法類型,因為它沒有返回值。這與void返回值存在著明顯的區(qū)別。對于void返回值,盡管方法本身不會自動返回什么,但仍然可以讓它返回另一些東西。構(gòu)建器則不同,它不僅什么也不會自動返回,而且根本不能有任何選擇。若存在一個返回值,而且假設(shè)我們可以自行選擇返回內(nèi)容,那么編譯器多少要知道如何對那個返回值作什么樣的處理。
4.2
方法過載
在任何程序設(shè)計語言中,一項重要的特性就是名字的運用。我們創(chuàng)建一個對象時,會分配到一個保存區(qū)域的名字。方法名代表的是一種具體的行動。通過用名字描述自己的系統(tǒng),可使自己的程序更易人們理解和修改。它非常象寫散文——目的是與讀者溝通。
我們用名字引用或描述所有對象與方法。若名字選得好,可使自己及其他人更易理解自己的代碼。
將人類語言中存在細(xì)致差別的概念“映射”到一種程序設(shè)計語言中時,會出現(xiàn)一些特殊的問題。在日常生活中,我們用相同的詞表達(dá)多種不同的含義——即詞的“過載”。我們說“洗襯衫”、“洗車”以及“洗狗”。但若強制象下面這樣說,就顯得很愚蠢:“襯衫洗襯衫”、“車洗車”以及“狗洗狗”。這是由于聽眾根本不需要對執(zhí)行的行動作任何明確的區(qū)分。人類的大多數(shù)語言都具有很強的“冗余”性,所以即使漏掉了幾個詞,仍然可以推斷出含義。我們不需要獨一無二的標(biāo)識符——可從具體的語境中推論出含義。
大多數(shù)程序設(shè)計語言(特別是C)要求我們?yōu)槊總€函數(shù)都設(shè)定一個獨一無二的標(biāo)識符。所以絕對不能用一個名為print()的函數(shù)來顯示整數(shù),再用另一個print()顯示浮點數(shù)——每個函數(shù)都要求具備唯一的名字。
在Java里,另一項因素強迫方法名出現(xiàn)過載情況:構(gòu)建器。由于構(gòu)建器的名字由類名決定,所以只能有一個構(gòu)建器名稱。但假若我們想用多種方式創(chuàng)建一個對象呢?例如,假設(shè)我們想創(chuàng)建一個類,令其用標(biāo)準(zhǔn)方式進(jìn)行初始化,另外從文件里讀取信息來初始化。此時,我們需要兩個構(gòu)建器,一個沒有自變量(默認(rèn)構(gòu)建器),另一個將字串作為自變量——用于初始化對象的那個文件的名字。由于都是構(gòu)建器,所以它們必須有相同的名字,亦即類名。所以為了讓相同的方法名伴隨不同的自變量類型使用,“方法過載”是非常關(guān)鍵的一項措施。同時,盡管方法過載是構(gòu)建器必需的,但它亦可應(yīng)用于其他任何方法,且用法非常方便。
在下面這個例子里,我們向大家同時展示了過載構(gòu)建器和過載的原始方法:
Tree既可創(chuàng)建成一顆種子,不含任何自變量;亦可創(chuàng)建成生長在苗圃中的植物。為支持這種創(chuàng)建,共使用了兩個構(gòu)建器,一個沒有自變量(我們把沒有自變量的構(gòu)建器稱作“默認(rèn)構(gòu)建器”,注釋①),另一個采用現(xiàn)成的高度。
①:在Sun公司出版的一些Java資料中,用簡陋但很說明問題的詞語稱呼這類構(gòu)建器——“無參數(shù)構(gòu)建器”(no-arg
constructors)。但“默認(rèn)構(gòu)建器”這個稱呼已使用了許多年,所以我選擇了它。
我們也有可能希望通過多種途徑調(diào)用info()方法。例如,假設(shè)我們有一條額外的消息想顯示出來,就使用String自變量;而假設(shè)沒有其他話可說,就不使用。由于為顯然相同的概念賦予了兩個獨立的名字,所以看起來可能有些古怪。幸運的是,方法過載允許我們?yōu)閮烧呤褂孟嗤拿帧?br />
4.2.1
區(qū)分過載方法
若方法有同樣的名字,Java怎樣知道我們指的哪一個方法呢?這里有一個簡單的規(guī)則:每個過載的方法都必須采取獨一無二的自變量類型列表。
若稍微思考幾秒鐘,就會想到這樣一個問題:除根據(jù)自變量的類型,程序員如何區(qū)分兩個同名方法的差異呢?
即使自變量的順序也足夠我們區(qū)分兩個方法(盡管我們通常不愿意采用這種方法,因為它會產(chǎn)生難以維護(hù)的代碼):
兩個print()方法有完全一致的自變量,但順序不同,可據(jù)此區(qū)分它們。
4.2.2
主類型的過載
主(數(shù)據(jù))類型能從一個“較小”的類型自動轉(zhuǎn)變成一個“較大”的類型。涉及過載問題時,這會稍微造成一些混亂。下面這個例子揭示了將主類型傳遞給過載的方法時發(fā)生的情況:
若觀察這個程序的輸出,就會發(fā)現(xiàn)常數(shù)值5被當(dāng)作一個int值處理。所以假若可以使用一個過載的方法,就能獲取它使用的int值。在其他所有情況下,若我們的數(shù)據(jù)類型“小于”方法中使用的自變量,就會對那種數(shù)據(jù)類型進(jìn)行“轉(zhuǎn)型”處理。char獲得的效果稍有些不同,這是由于假期它沒有發(fā)現(xiàn)一個準(zhǔn)確的char匹配,就會轉(zhuǎn)型為int。
若我們的自變量“大于”過載方法期望的自變量,這時又會出現(xiàn)什么情況呢?對前述程序的一個修改揭示出了答案:
在這里,方法采用了容量更小、范圍更窄的主類型值。若我們的自變量范圍比它寬,就必須用括號中的類型名將其轉(zhuǎn)為適當(dāng)?shù)念愋汀H绻贿@樣做,編譯器會報告出錯。
大家可注意到這是一種“縮小轉(zhuǎn)換”。也就是說,在造型或轉(zhuǎn)型過程中可能丟失一些信息。這正是編譯器強迫我們明確定義的原因——我們需明確表達(dá)想要轉(zhuǎn)型的愿望。
4.2.3
返回值過載
我們很易對下面這些問題感到迷惑:為什么只有類名和方法自變量列出?為什么不根據(jù)返回值對方法加以區(qū)分?比如對下面這兩個方法來說,雖然它們有同樣的名字和自變量,但其實是很容易區(qū)分的:
void
f() {}
int f() {}
若編譯器可根據(jù)上下文(語境)明確判斷出含義,比如在int
x=f()中,那么這樣做完全沒有問題。然而,我們也可能調(diào)用一個方法,同時忽略返回值;我們通常把這稱為“為它的副作用去調(diào)用一個方法”,因為我們關(guān)心的不是返回值,而是方法調(diào)用的其他效果。所以假如我們象下面這樣調(diào)用方法:
f();
Java怎樣判斷f()的具體調(diào)用方式呢?而且別人如何識別并理解代碼呢?由于存在這一類的問題,所以不能根據(jù)返回值類型來區(qū)分過載的方法。
4.2.4
默認(rèn)構(gòu)建器
正如早先指出的那樣,默認(rèn)構(gòu)建器是沒有自變量的。它們的作用是創(chuàng)建一個“空對象”。若創(chuàng)建一個沒有構(gòu)建器的類,則編譯程序會幫我們自動創(chuàng)建一個默認(rèn)構(gòu)建器。例如:
對于下面這一行:
new
Bird();
它的作用是新建一個對象,并調(diào)用默認(rèn)構(gòu)建器——即使尚未明確定義一個象這樣的構(gòu)建器。若沒有它,就沒有方法可以調(diào)用,無法構(gòu)建我們的對象。然而,如果已經(jīng)定義了一個構(gòu)建器(無論是否有自變量),編譯程序都不會幫我們自動合成一個:
class
Bush {
Bush(int i) {}
Bush(double d) {}
}
現(xiàn)在,假若使用下述代碼:
new
Bush();
編譯程序就會報告自己找不到一個相符的構(gòu)建器。就好象我們沒有設(shè)置任何構(gòu)建器,編譯程序會說:“你看來似乎需要一個構(gòu)建器,所以讓我們給你制造一個吧。”但假如我們寫了一個構(gòu)建器,編譯程序就會說:“啊,你已寫了一個構(gòu)建器,所以我知道你想干什么;如果你不放置一個默認(rèn)的,是由于你打算省略它。”
4.2.5
this關(guān)鍵字
如果有兩個同類型的對象,分別叫作a和b,那么您也許不知道如何為這兩個對象同時調(diào)用一個f()方法:
class Banana
{ void f(int i) { } }
Banana a = new Banana(), b = new
Banana();
a.f(1);
b.f(2);
若只有一個名叫f()的方法,它怎樣才能知道自己是為a還是為b調(diào)用的呢?
為了能用簡便的、面向?qū)ο蟮恼Z法來書寫代碼——亦即“將消息發(fā)給對象”,編譯器為我們完成了一些幕后工作。其中的秘密就是第一個自變量傳遞給方法f(),而且那個自變量是準(zhǔn)備操作的那個對象的句柄。所以前述的兩個方法調(diào)用就變成了下面這樣的形式:
Banana.f(a,1);
Banana.f(b,2);
這是內(nèi)部的表達(dá)形式,我們并不能這樣書寫表達(dá)式,并試圖讓編譯器接受它。但是,通過它可理解幕后到底發(fā)生了什么事情。
假定我們在一個方法的內(nèi)部,并希望獲得當(dāng)前對象的句柄。由于那個句柄是由編譯器“秘密”傳遞的,所以沒有標(biāo)識符可用。然而,針對這一目的有個專用的關(guān)鍵字:this。this關(guān)鍵字(注意只能在方法內(nèi)部使用)可為已調(diào)用了其方法的那個對象生成相應(yīng)的句柄。可象對待其他任何對象句柄一樣對待這個句柄。但要注意,假若準(zhǔn)備從自己某個類的另一個方法內(nèi)部調(diào)用一個類方法,就不必使用this。只需簡單地調(diào)用那個方法即可。當(dāng)前的this句柄會自動應(yīng)用于其他方法。所以我們能使用下面這樣的代碼:
class
Apricot {
void pick() { }
void pit() { pick();
}
}
在pit()內(nèi)部,我們可以說this.pick(),但事實上無此必要。編譯器能幫我們自動完成。this關(guān)鍵字只能用于那些特殊的類——需明確使用當(dāng)前對象的句柄。例如,假若您希望將句柄返回給當(dāng)前對象,那么它經(jīng)常在return語句中使用。
由于increment()通過this關(guān)鍵字返回當(dāng)前對象的句柄,所以可以方便地對同一個對象執(zhí)行多項操作。
1.
在構(gòu)建器里調(diào)用構(gòu)建器
若為一個類寫了多個構(gòu)建器,那么經(jīng)常都需要在一個構(gòu)建器里調(diào)用另一個構(gòu)建器,以避免寫重復(fù)的代碼。可用this關(guān)鍵字做到這一點。
通常,當(dāng)我們說this的時候,都是指“這個對象”或者“當(dāng)前對象”。而且它本身會產(chǎn)生當(dāng)前對象的一個句柄。在一個構(gòu)建器中,若為其賦予一個自變量列表,那么this關(guān)鍵字會具有不同的含義:它會對與那個自變量列表相符的構(gòu)建器進(jìn)行明確的調(diào)用。這樣一來,我們就可通過一條直接的途徑來調(diào)用其他構(gòu)建器。如下所示:
其中,構(gòu)建器Flower(String
s,int
petals)向我們揭示出這樣一個問題:盡管可用this調(diào)用一個構(gòu)建器,但不可調(diào)用兩個。除此以外,構(gòu)建器調(diào)用必須是我們做的第一件事情,否則會收到編譯程序的報錯信息。
這個例子也向大家展示了this的另一項用途。由于自變量s的名字以及成員數(shù)據(jù)s的名字是相同的,所以會出現(xiàn)混淆。為解決這個問題,可用this.s來引用成員數(shù)據(jù)。經(jīng)常都會在Java代碼里看到這種形式的應(yīng)用,本書的大量地方也采用了這種做法。
在print()中,我們發(fā)現(xiàn)編譯器不讓我們從除了一個構(gòu)建器之外的其他任何方法內(nèi)部調(diào)用一個構(gòu)建器。
2.
static的含義
理解了this關(guān)鍵字后,我們可更完整地理解static(靜態(tài))方法的含義。它意味著一個特定的方法沒有this。我們不可從一個static方法內(nèi)部發(fā)出對非static方法的調(diào)用(注釋②),盡管反過來說是可以的。而且在沒有任何對象的前提下,我們可針對類本身發(fā)出對一個static方法的調(diào)用。事實上,那正是static方法最基本的意義。它就好象我們創(chuàng)建一個全局函數(shù)的等價物(在C語言中)。除了全局函數(shù)不允許在Java中使用以外,若將一個static方法置入一個類的內(nèi)部,它就可以訪問其他static方法以及static字段。
②:有可能發(fā)出這類調(diào)用的一種情況是我們將一個對象句柄傳到static方法內(nèi)部。隨后,通過句柄(此時實際是this),我們可調(diào)用非static方法,并訪問非static字段。但一般地,如果真的想要這樣做,只要制作一個普通的、非static方法即可。
有些人抱怨static方法并不是“面向?qū)ο?#8221;的,因為它們具有全局函數(shù)的某些特點;利用static方法,我們不必向?qū)ο蟀l(fā)送一條消息,因為不存在this。這可能是一個清楚的自變量,若您發(fā)現(xiàn)自己使用了大量靜態(tài)方法,就應(yīng)重新思考自己的策略。然而,static的概念是非常實用的,許多時候都需要用到它。所以至于它們是否真的“面向?qū)ο?#8221;,應(yīng)該留給理論家去討論。事實上,即使Smalltalk在自己的“類方法”里也有類似于static的東西。
“初始化”和“清除”是這些安全問題的其中兩個。許多C程序的錯誤都是由于程序員忘記初始化一個變量造成的。對于現(xiàn)成的庫,若用戶不知道如何初始化庫的一個組件,就往往會出現(xiàn)這一類的錯誤。清除是另一個特殊的問題,因為用完一個元素后,由于不再關(guān)心,所以很容易把它忘記。這樣一來,那個元素占用的資源會一直保留下去,極易產(chǎn)生資源(主要是內(nèi)存)用盡的后果。
C++為我們引入了“構(gòu)建器”的概念。這是一種特殊的方法,在一個對象創(chuàng)建之后自動調(diào)用。Java也沿用了這個概念,但新增了自己的“垃圾收集器”,能在資源不再需要的時候自動釋放它們。本章將討論初始化和清除的問題,以及Java如何提供它們的支持。
4.1
用構(gòu)建器自動初始化
對于方法的創(chuàng)建,可將其想象成為自己寫的每個類都調(diào)用一次initialize()。這個名字提醒我們在使用對象之前,應(yīng)首先進(jìn)行這樣的調(diào)用。但不幸的是,這也意味著用戶必須記住調(diào)用方法。在Java中,由于提供了名為“構(gòu)建器”的一種特殊方法,所以類的設(shè)計者可擔(dān)保每個對象都會得到正確的初始化。若某個類有一個構(gòu)建器,那么在創(chuàng)建對象時,Java會自動調(diào)用那個構(gòu)建器——甚至在用戶毫不知覺的情況下。所以說這是可以擔(dān)保的!
接著的一個問題是如何命名這個方法。存在兩方面的問題。第一個是我們使用的任何名字都可能與打算為某個類成員使用的名字沖突。第二是由于編譯器的責(zé)任是調(diào)用構(gòu)建器,所以它必須知道要調(diào)用是哪個方法。C++采取的方案看來是最簡單的,且更有邏輯性,所以也在Java里得到了應(yīng)用:構(gòu)建器的名字與類名相同。這樣一來,可保證象這樣的一個方法會在初始化期間自動調(diào)用。
下面是帶有構(gòu)建器的一個簡單的類(若執(zhí)行這個程序有問題,請參考第3章的“賦值”小節(jié))。
//: SimpleConstructor.java // Demonstration of a simple constructor package c04;class Rock { Rock() { // This is the constructor System.out.println("Creating Rock"); } } public class SimpleConstructor { public static void main(String[] args) { for(int i = 0; i < 10; i++) new Rock(); } } ///:~
現(xiàn)在,一旦創(chuàng)建一個對象:
new
Rock();
就會分配相應(yīng)的存儲空間,并調(diào)用構(gòu)建器。這樣可保證在我們經(jīng)手之前,對象得到正確的初始化。
請注意所有方法首字母小寫的編碼規(guī)則并不適用于構(gòu)建器。這是由于構(gòu)建器的名字必須與類名完全相同!
和其他任何方法一樣,構(gòu)建器也能使用自變量,以便我們指定對象的具體創(chuàng)建方式。可非常方便地改動上述例子,以便構(gòu)建器使用自己的自變量。如下所示:
class Rock { Rock(int i) { System.out.println( "Creating Rock number " + i); } } public class SimpleConstructor { public static void main(String[] args) { for(int i = 0; i < 10; i++) new Rock(i); } }
利用構(gòu)建器的自變量,我們可為一個對象的初始化設(shè)定相應(yīng)的參數(shù)。舉個例子來說,假設(shè)類Tree有一個構(gòu)建器,它用一個整數(shù)自變量標(biāo)記樹的高度,那么就可以象下面這樣創(chuàng)建一個Tree對象:
tree
t = new Tree(12); //
12英尺高的樹
若Tree(int)是我們唯一的構(gòu)建器,那么編譯器不會允許我們以其他任何方式創(chuàng)建一個Tree對象。
構(gòu)建器有助于消除大量涉及類的問題,并使代碼更易閱讀。例如在前述的代碼段中,我們并未看到對initialize()方法的明確調(diào)用——那些方法在概念上獨立于定義內(nèi)容。在Java中,定義和初始化屬于統(tǒng)一的概念——兩者缺一不可。
構(gòu)建器屬于一種較特殊的方法類型,因為它沒有返回值。這與void返回值存在著明顯的區(qū)別。對于void返回值,盡管方法本身不會自動返回什么,但仍然可以讓它返回另一些東西。構(gòu)建器則不同,它不僅什么也不會自動返回,而且根本不能有任何選擇。若存在一個返回值,而且假設(shè)我們可以自行選擇返回內(nèi)容,那么編譯器多少要知道如何對那個返回值作什么樣的處理。
4.2
方法過載
在任何程序設(shè)計語言中,一項重要的特性就是名字的運用。我們創(chuàng)建一個對象時,會分配到一個保存區(qū)域的名字。方法名代表的是一種具體的行動。通過用名字描述自己的系統(tǒng),可使自己的程序更易人們理解和修改。它非常象寫散文——目的是與讀者溝通。
我們用名字引用或描述所有對象與方法。若名字選得好,可使自己及其他人更易理解自己的代碼。
將人類語言中存在細(xì)致差別的概念“映射”到一種程序設(shè)計語言中時,會出現(xiàn)一些特殊的問題。在日常生活中,我們用相同的詞表達(dá)多種不同的含義——即詞的“過載”。我們說“洗襯衫”、“洗車”以及“洗狗”。但若強制象下面這樣說,就顯得很愚蠢:“襯衫洗襯衫”、“車洗車”以及“狗洗狗”。這是由于聽眾根本不需要對執(zhí)行的行動作任何明確的區(qū)分。人類的大多數(shù)語言都具有很強的“冗余”性,所以即使漏掉了幾個詞,仍然可以推斷出含義。我們不需要獨一無二的標(biāo)識符——可從具體的語境中推論出含義。
大多數(shù)程序設(shè)計語言(特別是C)要求我們?yōu)槊總€函數(shù)都設(shè)定一個獨一無二的標(biāo)識符。所以絕對不能用一個名為print()的函數(shù)來顯示整數(shù),再用另一個print()顯示浮點數(shù)——每個函數(shù)都要求具備唯一的名字。
在Java里,另一項因素強迫方法名出現(xiàn)過載情況:構(gòu)建器。由于構(gòu)建器的名字由類名決定,所以只能有一個構(gòu)建器名稱。但假若我們想用多種方式創(chuàng)建一個對象呢?例如,假設(shè)我們想創(chuàng)建一個類,令其用標(biāo)準(zhǔn)方式進(jìn)行初始化,另外從文件里讀取信息來初始化。此時,我們需要兩個構(gòu)建器,一個沒有自變量(默認(rèn)構(gòu)建器),另一個將字串作為自變量——用于初始化對象的那個文件的名字。由于都是構(gòu)建器,所以它們必須有相同的名字,亦即類名。所以為了讓相同的方法名伴隨不同的自變量類型使用,“方法過載”是非常關(guān)鍵的一項措施。同時,盡管方法過載是構(gòu)建器必需的,但它亦可應(yīng)用于其他任何方法,且用法非常方便。
在下面這個例子里,我們向大家同時展示了過載構(gòu)建器和過載的原始方法:
//: Overloading.java // Demonstration of both constructor // and ordinary method overloading. import java.util.*; class Tree { int height; Tree() { prt("Planting a seedling"); height = 0; } Tree(int i) { prt("Creating new Tree that is " + i + " feet tall"); height = i; } void info() { prt("Tree is " + height + " feet tall"); } void info(String s) { prt(s + ": Tree is " + height + " feet tall"); } static void prt(String s) { System.out.println(s); } } public class Overloading { public static void main(String[] args) { for(int i = 0; i < 5; i++) { Tree t = new Tree(i); t.info(); t.info("overloaded method"); } // Overloaded constructor: new Tree(); } } ///:~
Tree既可創(chuàng)建成一顆種子,不含任何自變量;亦可創(chuàng)建成生長在苗圃中的植物。為支持這種創(chuàng)建,共使用了兩個構(gòu)建器,一個沒有自變量(我們把沒有自變量的構(gòu)建器稱作“默認(rèn)構(gòu)建器”,注釋①),另一個采用現(xiàn)成的高度。
①:在Sun公司出版的一些Java資料中,用簡陋但很說明問題的詞語稱呼這類構(gòu)建器——“無參數(shù)構(gòu)建器”(no-arg
constructors)。但“默認(rèn)構(gòu)建器”這個稱呼已使用了許多年,所以我選擇了它。
我們也有可能希望通過多種途徑調(diào)用info()方法。例如,假設(shè)我們有一條額外的消息想顯示出來,就使用String自變量;而假設(shè)沒有其他話可說,就不使用。由于為顯然相同的概念賦予了兩個獨立的名字,所以看起來可能有些古怪。幸運的是,方法過載允許我們?yōu)閮烧呤褂孟嗤拿帧?br />
4.2.1
區(qū)分過載方法
若方法有同樣的名字,Java怎樣知道我們指的哪一個方法呢?這里有一個簡單的規(guī)則:每個過載的方法都必須采取獨一無二的自變量類型列表。
若稍微思考幾秒鐘,就會想到這樣一個問題:除根據(jù)自變量的類型,程序員如何區(qū)分兩個同名方法的差異呢?
即使自變量的順序也足夠我們區(qū)分兩個方法(盡管我們通常不愿意采用這種方法,因為它會產(chǎn)生難以維護(hù)的代碼):
//: OverloadingOrder.java // Overloading based on the order of // the arguments. public class OverloadingOrder { static void print(String s, int i) { System.out.println( "String: " + s + ", int: " + i); } static void print(int i, String s) { System.out.println( "int: " + i + ", String: " + s); } public static void main(String[] args) { print("String first", 11); print(99, "Int first"); } } ///:~
兩個print()方法有完全一致的自變量,但順序不同,可據(jù)此區(qū)分它們。
4.2.2
主類型的過載
主(數(shù)據(jù))類型能從一個“較小”的類型自動轉(zhuǎn)變成一個“較大”的類型。涉及過載問題時,這會稍微造成一些混亂。下面這個例子揭示了將主類型傳遞給過載的方法時發(fā)生的情況:
//: PrimitiveOverloading.java // Promotion of primitives and overloading public class PrimitiveOverloading { // boolean can't be automatically converted static void prt(String s) { System.out.println(s); } void f1(char x) { prt("f1(char)"); } void f1(byte x) { prt("f1(byte)"); } void f1(short x) { prt("f1(short)"); } void f1(int x) { prt("f1(int)"); } void f1(long x) { prt("f1(long)"); } void f1(float x) { prt("f1(float)"); } void f1(double x) { prt("f1(double)"); } void f2(byte x) { prt("f2(byte)"); } void f2(short x) { prt("f2(short)"); } void f2(int x) { prt("f2(int)"); } void f2(long x) { prt("f2(long)"); } void f2(float x) { prt("f2(float)"); } void f2(double x) { prt("f2(double)"); } void f3(short x) { prt("f3(short)"); } void f3(int x) { prt("f3(int)"); } void f3(long x) { prt("f3(long)"); } void f3(float x) { prt("f3(float)"); } void f3(double x) { prt("f3(double)"); } void f4(int x) { prt("f4(int)"); } void f4(long x) { prt("f4(long)"); } void f4(float x) { prt("f4(float)"); } void f4(double x) { prt("f4(double)"); } void f5(long x) { prt("f5(long)"); } void f5(float x) { prt("f5(float)"); } void f5(double x) { prt("f5(double)"); } void f6(float x) { prt("f6(float)"); } void f6(double x) { prt("f6(double)"); } void f7(double x) { prt("f7(double)"); } void testConstVal() { prt("Testing with 5"); f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5); } void testChar() { char x = 'x'; prt("char argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testByte() { byte x = 0; prt("byte argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testShort() { short x = 0; prt("short argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testInt() { int x = 0; prt("int argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testLong() { long x = 0; prt("long argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testFloat() { float x = 0; prt("float argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testDouble() { double x = 0; prt("double argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } public static void main(String[] args) { PrimitiveOverloading p = new PrimitiveOverloading(); p.testConstVal(); p.testChar(); p.testByte(); p.testShort(); p.testInt(); p.testLong(); p.testFloat(); p.testDouble(); } } ///:~
若觀察這個程序的輸出,就會發(fā)現(xiàn)常數(shù)值5被當(dāng)作一個int值處理。所以假若可以使用一個過載的方法,就能獲取它使用的int值。在其他所有情況下,若我們的數(shù)據(jù)類型“小于”方法中使用的自變量,就會對那種數(shù)據(jù)類型進(jìn)行“轉(zhuǎn)型”處理。char獲得的效果稍有些不同,這是由于假期它沒有發(fā)現(xiàn)一個準(zhǔn)確的char匹配,就會轉(zhuǎn)型為int。
若我們的自變量“大于”過載方法期望的自變量,這時又會出現(xiàn)什么情況呢?對前述程序的一個修改揭示出了答案:
//: Demotion.java // Demotion of primitives and overloading public class Demotion { static void prt(String s) { System.out.println(s); } void f1(char x) { prt("f1(char)"); } void f1(byte x) { prt("f1(byte)"); } void f1(short x) { prt("f1(short)"); } void f1(int x) { prt("f1(int)"); } void f1(long x) { prt("f1(long)"); } void f1(float x) { prt("f1(float)"); } void f1(double x) { prt("f1(double)"); } void f2(char x) { prt("f2(char)"); } void f2(byte x) { prt("f2(byte)"); } void f2(short x) { prt("f2(short)"); } void f2(int x) { prt("f2(int)"); } void f2(long x) { prt("f2(long)"); } void f2(float x) { prt("f2(float)"); } void f3(char x) { prt("f3(char)"); } void f3(byte x) { prt("f3(byte)"); } void f3(short x) { prt("f3(short)"); } void f3(int x) { prt("f3(int)"); } void f3(long x) { prt("f3(long)"); } void f4(char x) { prt("f4(char)"); } void f4(byte x) { prt("f4(byte)"); } void f4(short x) { prt("f4(short)"); } void f4(int x) { prt("f4(int)"); } void f5(char x) { prt("f5(char)"); } void f5(byte x) { prt("f5(byte)"); } void f5(short x) { prt("f5(short)"); } void f6(char x) { prt("f6(char)"); } void f6(byte x) { prt("f6(byte)"); } void f7(char x) { prt("f7(char)"); } void testDouble() { double x = 0; prt("double argument:"); f1(x);f2((float)x);f3((long)x);f4((int)x); f5((short)x);f6((byte)x);f7((char)x); } public static void main(String[] args) { Demotion p = new Demotion(); p.testDouble(); } } ///:~
在這里,方法采用了容量更小、范圍更窄的主類型值。若我們的自變量范圍比它寬,就必須用括號中的類型名將其轉(zhuǎn)為適當(dāng)?shù)念愋汀H绻贿@樣做,編譯器會報告出錯。
大家可注意到這是一種“縮小轉(zhuǎn)換”。也就是說,在造型或轉(zhuǎn)型過程中可能丟失一些信息。這正是編譯器強迫我們明確定義的原因——我們需明確表達(dá)想要轉(zhuǎn)型的愿望。
4.2.3
返回值過載
我們很易對下面這些問題感到迷惑:為什么只有類名和方法自變量列出?為什么不根據(jù)返回值對方法加以區(qū)分?比如對下面這兩個方法來說,雖然它們有同樣的名字和自變量,但其實是很容易區(qū)分的:
void
f() {}
int f() {}
若編譯器可根據(jù)上下文(語境)明確判斷出含義,比如在int
x=f()中,那么這樣做完全沒有問題。然而,我們也可能調(diào)用一個方法,同時忽略返回值;我們通常把這稱為“為它的副作用去調(diào)用一個方法”,因為我們關(guān)心的不是返回值,而是方法調(diào)用的其他效果。所以假如我們象下面這樣調(diào)用方法:
f();
Java怎樣判斷f()的具體調(diào)用方式呢?而且別人如何識別并理解代碼呢?由于存在這一類的問題,所以不能根據(jù)返回值類型來區(qū)分過載的方法。
4.2.4
默認(rèn)構(gòu)建器
正如早先指出的那樣,默認(rèn)構(gòu)建器是沒有自變量的。它們的作用是創(chuàng)建一個“空對象”。若創(chuàng)建一個沒有構(gòu)建器的類,則編譯程序會幫我們自動創(chuàng)建一個默認(rèn)構(gòu)建器。例如:
//: DefaultConstructor.java class Bird { int i; } public class DefaultConstructor { public static void main(String[] args) { Bird nc = new Bird(); // default! } } ///:~
對于下面這一行:
new
Bird();
它的作用是新建一個對象,并調(diào)用默認(rèn)構(gòu)建器——即使尚未明確定義一個象這樣的構(gòu)建器。若沒有它,就沒有方法可以調(diào)用,無法構(gòu)建我們的對象。然而,如果已經(jīng)定義了一個構(gòu)建器(無論是否有自變量),編譯程序都不會幫我們自動合成一個:
class
Bush {
Bush(int i) {}
Bush(double d) {}
}
現(xiàn)在,假若使用下述代碼:
new
Bush();
編譯程序就會報告自己找不到一個相符的構(gòu)建器。就好象我們沒有設(shè)置任何構(gòu)建器,編譯程序會說:“你看來似乎需要一個構(gòu)建器,所以讓我們給你制造一個吧。”但假如我們寫了一個構(gòu)建器,編譯程序就會說:“啊,你已寫了一個構(gòu)建器,所以我知道你想干什么;如果你不放置一個默認(rèn)的,是由于你打算省略它。”
4.2.5
this關(guān)鍵字
如果有兩個同類型的對象,分別叫作a和b,那么您也許不知道如何為這兩個對象同時調(diào)用一個f()方法:
class Banana
{ void f(int i) { } }
Banana a = new Banana(), b = new
Banana();
a.f(1);
b.f(2);
若只有一個名叫f()的方法,它怎樣才能知道自己是為a還是為b調(diào)用的呢?
為了能用簡便的、面向?qū)ο蟮恼Z法來書寫代碼——亦即“將消息發(fā)給對象”,編譯器為我們完成了一些幕后工作。其中的秘密就是第一個自變量傳遞給方法f(),而且那個自變量是準(zhǔn)備操作的那個對象的句柄。所以前述的兩個方法調(diào)用就變成了下面這樣的形式:
Banana.f(a,1);
Banana.f(b,2);
這是內(nèi)部的表達(dá)形式,我們并不能這樣書寫表達(dá)式,并試圖讓編譯器接受它。但是,通過它可理解幕后到底發(fā)生了什么事情。
假定我們在一個方法的內(nèi)部,并希望獲得當(dāng)前對象的句柄。由于那個句柄是由編譯器“秘密”傳遞的,所以沒有標(biāo)識符可用。然而,針對這一目的有個專用的關(guān)鍵字:this。this關(guān)鍵字(注意只能在方法內(nèi)部使用)可為已調(diào)用了其方法的那個對象生成相應(yīng)的句柄。可象對待其他任何對象句柄一樣對待這個句柄。但要注意,假若準(zhǔn)備從自己某個類的另一個方法內(nèi)部調(diào)用一個類方法,就不必使用this。只需簡單地調(diào)用那個方法即可。當(dāng)前的this句柄會自動應(yīng)用于其他方法。所以我們能使用下面這樣的代碼:
class
Apricot {
void pick() { }
void pit() { pick();
}
}
在pit()內(nèi)部,我們可以說this.pick(),但事實上無此必要。編譯器能幫我們自動完成。this關(guān)鍵字只能用于那些特殊的類——需明確使用當(dāng)前對象的句柄。例如,假若您希望將句柄返回給當(dāng)前對象,那么它經(jīng)常在return語句中使用。
//: Leaf.java // Simple use of the "this" keyword public class Leaf { private int i = 0; Leaf increment() { i++; return this; } void print() { System.out.println("i = " + i); } public static void main(String[] args) { Leaf x = new Leaf(); x.increment().increment().increment().print(); } } ///:~
由于increment()通過this關(guān)鍵字返回當(dāng)前對象的句柄,所以可以方便地對同一個對象執(zhí)行多項操作。
1.
在構(gòu)建器里調(diào)用構(gòu)建器
若為一個類寫了多個構(gòu)建器,那么經(jīng)常都需要在一個構(gòu)建器里調(diào)用另一個構(gòu)建器,以避免寫重復(fù)的代碼。可用this關(guān)鍵字做到這一點。
通常,當(dāng)我們說this的時候,都是指“這個對象”或者“當(dāng)前對象”。而且它本身會產(chǎn)生當(dāng)前對象的一個句柄。在一個構(gòu)建器中,若為其賦予一個自變量列表,那么this關(guān)鍵字會具有不同的含義:它會對與那個自變量列表相符的構(gòu)建器進(jìn)行明確的調(diào)用。這樣一來,我們就可通過一條直接的途徑來調(diào)用其他構(gòu)建器。如下所示:
//: Flower.java // Calling constructors with "this" public class Flower { private int petalCount = 0; private String s = new String("null"); Flower(int petals) { petalCount = petals; System.out.println( "Constructor w/ int arg only, petalCount= " + petalCount); } Flower(String ss) { System.out.println( "Constructor w/ String arg only, s=" + ss); s = ss; } Flower(String s, int petals) { this(petals); //! this(s); // Can't call two! this.s = s; // Another use of "this" System.out.println("String & int args"); } Flower() { this("hi", 47); System.out.println( "default constructor (no args)"); } void print() { //! this(11); // Not inside non-constructor! System.out.println( "petalCount = " + petalCount + " s = "+ s); } public static void main(String[] args) { Flower x = new Flower(); x.print(); } } ///:~
其中,構(gòu)建器Flower(String
s,int
petals)向我們揭示出這樣一個問題:盡管可用this調(diào)用一個構(gòu)建器,但不可調(diào)用兩個。除此以外,構(gòu)建器調(diào)用必須是我們做的第一件事情,否則會收到編譯程序的報錯信息。
這個例子也向大家展示了this的另一項用途。由于自變量s的名字以及成員數(shù)據(jù)s的名字是相同的,所以會出現(xiàn)混淆。為解決這個問題,可用this.s來引用成員數(shù)據(jù)。經(jīng)常都會在Java代碼里看到這種形式的應(yīng)用,本書的大量地方也采用了這種做法。
在print()中,我們發(fā)現(xiàn)編譯器不讓我們從除了一個構(gòu)建器之外的其他任何方法內(nèi)部調(diào)用一個構(gòu)建器。
2.
static的含義
理解了this關(guān)鍵字后,我們可更完整地理解static(靜態(tài))方法的含義。它意味著一個特定的方法沒有this。我們不可從一個static方法內(nèi)部發(fā)出對非static方法的調(diào)用(注釋②),盡管反過來說是可以的。而且在沒有任何對象的前提下,我們可針對類本身發(fā)出對一個static方法的調(diào)用。事實上,那正是static方法最基本的意義。它就好象我們創(chuàng)建一個全局函數(shù)的等價物(在C語言中)。除了全局函數(shù)不允許在Java中使用以外,若將一個static方法置入一個類的內(nèi)部,它就可以訪問其他static方法以及static字段。
②:有可能發(fā)出這類調(diào)用的一種情況是我們將一個對象句柄傳到static方法內(nèi)部。隨后,通過句柄(此時實際是this),我們可調(diào)用非static方法,并訪問非static字段。但一般地,如果真的想要這樣做,只要制作一個普通的、非static方法即可。
有些人抱怨static方法并不是“面向?qū)ο?#8221;的,因為它們具有全局函數(shù)的某些特點;利用static方法,我們不必向?qū)ο蟀l(fā)送一條消息,因為不存在this。這可能是一個清楚的自變量,若您發(fā)現(xiàn)自己使用了大量靜態(tài)方法,就應(yīng)重新思考自己的策略。然而,static的概念是非常實用的,許多時候都需要用到它。所以至于它們是否真的“面向?qū)ο?#8221;,應(yīng)該留給理論家去討論。事實上,即使Smalltalk在自己的“類方法”里也有類似于static的東西。