Skip to content
This repository has been archived by the owner on Aug 25, 2019. It is now read-only.

Latest commit

 

History

History
711 lines (443 loc) · 48.7 KB

第2章.md

File metadata and controls

711 lines (443 loc) · 48.7 KB

第2章 一切都是對象

“儘管以C++為基礎,但Java是一種更純粹的面向對象程序設計語言”。

無論C++還是Java都屬於雜合語言。但在Java中,設計者覺得這種雜合併不象在C++裡那麼重要。雜合語言允許採用多種編程風格;之所以說C++是一種雜合語言,是因為它支持與C語言的向後兼容能力。由於C++是C的一個超集,所以包含的許多特性都是後者不具備的,這些特性使C++在某些地方顯得過於複雜。

Java語言首先便假定了我們只希望進行面向對象的程序設計。也就是說,正式用它設計之前,必須先將自己的思想轉入一個面向對象的世界(除非早已習慣了這個世界的思維方式)。只有做好這個準備工作,與其他OOP語言相比,才能體會到Java的易學易用。在本章,我們將探討Java程序的基本組件,並體會為什麼說Java乃至Java程序內的一切都是對象。

2.1 用引用操縱對象

每種編程語言都有自己的數據處理方式。有些時候,程序員必須時刻留意準備處理的是什麼類型。您曾利用一些特殊語法直接操作過對象,或處理過一些間接表示的對象嗎(C或C++裡的指針)?

所有這些在Java裡都得到了簡化,任何東西都可看作對象。因此,我們可採用一種統一的語法,任何地方均可照搬不誤。但要注意,儘管將一切都“看作”對象,但操縱的標識符實際是指向一個對象的“句柄”(Handle)。在其他Java參考書裡,還可看到有的人將其稱作一個“引用”,甚至一個“指針”。可將這一情形想象成用遙控板(引用)操縱電視機(對象)。只要握住這個遙控板,就相當於掌握了與電視機連接的通道。但一旦需要“換頻道”或者“關小聲音”,我們實際操縱的是遙控板(引用),再由遙控板自己操縱電視機(對象)。如果要在房間裡四處走走,並想保持對電視機的控制,那麼手上拿著的是遙控板,而非電視機。

此外,即使沒有電視機,遙控板亦可獨立存在。也就是說,只是由於擁有一個引用,並不表示必須有一個對象同它連接。所以如果想容納一個詞或句子,可創建一個String引用:

String s;

但這裡創建的只是引用,並不是對象。若此時向s發送一條消息,就會獲得一個錯誤(運行期)。這是由於s實際並未與任何東西連接(即“沒有電視機”)。因此,一種更安全的做法是:創建一個引用時,記住無論如何都進行初始化:

String s = "asdf";

然而,這裡採用的是一種特殊類型:字符串可用加引號的文字初始化。通常,必須為對象使用一種更通用的初始化類型。

2.2 所有對象都必須創建

創建引用時,我們希望它同一個新對象連接。通常用new關鍵字達到這一目的。new的意思是:“把我變成這些對象的一種新類型”。所以在上面的例子中,可以說:

String s = new String("asdf");

它不僅指出“將我變成一個新字符串”,也通過提供一個初始字符串,指出了“如何生成這個新字符串”。

當然,字符串(String)並非唯一的類型。Java配套提供了數量眾多的現成類型。對我們來講,最重要的就是記住能自行創建類型。事實上,這應是Java程序設計的一項基本操作,是繼續本書後餘部分學習的基礎。

2.2.1 保存到什麼地方

程序運行時,我們最好對數據保存到什麼地方做到心中有數。特別要注意的是內存的分配。有六個地方都可以保存數據:

(1) 寄存器。這是最快的保存區域,因為它位於和其他所有保存方式不同的地方:處理器內部。然而,寄存器的數量十分有限,所以寄存器是根據需要由編譯器分配。我們對此沒有直接的控制權,也不可能在自己的程序裡找到寄存器存在的任何蹤跡。

(2) 棧。駐留於常規RAM(隨機訪問存儲器)區域,但可通過它的“棧指針”獲得處理的直接支持。棧指針若向下移,會創建新的內存;若向上移,則會釋放那些內存。這是一種特別快、特別有效的數據保存方式,僅次於寄存器。創建程序時,Java編譯器必須準確地知道棧內保存的所有數據的“長度”以及“存在時間”。這是由於它必須生成相應的代碼,以便向上和向下移動指針。這一限制無疑影響了程序的靈活性,所以儘管有些Java數據要保存在棧裡——特別是對象引用,但Java對象並不放到其中。

(3) 堆。一種常規用途的內存池(也在RAM區域),其中保存了Java對象。和棧不同,“內存堆”或“堆”(Heap)最吸引人的地方在於編譯器不必知道要從堆裡分配多少存儲空間,也不必知道存儲的數據要在堆裡停留多長的時間。因此,用堆保存數據時會得到更大的靈活性。要求創建一個對象時,只需用new命令編制相關的代碼即可。執行這些代碼時,會在堆裡自動進行數據的保存。當然,為達到這種靈活性,必然會付出一定的代價:在堆裡分配存儲空間時會花掉更長的時間!

(4) 靜態存儲。這兒的“靜態”(Static)是指“位於固定位置”(儘管也在RAM裡)。程序運行期間,靜態存儲的數據將隨時等候調用。可用static關鍵字指出一個對象的特定元素是靜態的。但Java對象本身永遠都不會置入靜態存儲空間。

(5) 常數存儲。常數值通常直接置於程序代碼內部。這樣做是安全的,因為它們永遠都不會改變。有的常數需要嚴格地保護,所以可考慮將它們置入只讀存儲器(ROM)。

(6) 非RAM存儲。若數據完全獨立於一個程序之外,則程序不運行時仍可存在,並在程序的控制範圍之外。其中兩個最主要的例子便是“流式對象”和“固定對象”。對於流式對象,對象會變成字節流,通常會發給另一臺機器。而對於固定對象,對象保存在磁盤中。即使程序中止運行,它們仍可保持自己的狀態不變。對於這些類型的數據存儲,一個特別有用的技巧就是它們能存在於其他媒體中。一旦需要,甚至能將它們恢復成普通的、基於RAM的對象。Java 1.1提供了對Lightweight persistence的支持。未來的版本甚至可能提供更完整的方案。

2.2.2 特殊情況:基本類型

有一系列類需特別對待;可將它們想象成“基本”、“主要”或者“主”(Primitive)類型,進行程序設計時要頻繁用到它們。之所以要特別對待,是由於用new創建對象(特別是小的、簡單的變量)並不是非常有效,因為new將對象置於“堆”裡。對於這些類型,Java採納了與C和C++相同的方法。也就是說,不是用new創建變量,而是創建一個並非引用的“自動”變量。這個變量容納了具體的值,並置於棧中,能夠更高效地存取。

Java決定了每種主要類型的大小。就象在大多數語言裡那樣,這些大小並不隨著機器結構的變化而變化。這種大小的不可更改正是Java程序具有很強移植能力的原因之一。

基本類型 大小 最小值 最大值 包裝器類型
boolean 1-bit Boolean
char 16-bit Unicode 0 Unicode 216- 1 Character
byte 8-bit -128 +127 Byte[11]
short 16-bit -215 +215 – 1 Short1
int 32-bit -231 +231 – 1 Integer
long 64-bit -263 +263 – 1 Long
float 32-bit IEEE754 IEEE754 Float
double 64-bit IEEE754 IEEE754 Double
void Void1

①:到Java 1.1才有,1.0版沒有。

數值類型全都是有符號(正負號)的,所以不必費勁尋找沒有符號的類型。 主數據類型也擁有自己的“包裝器”(wrapper)類。這意味著假如想讓堆內一個非主要對象表示那個基本類型,就要使用對應的包裝器。例如:

char c = 'x';
Character C = new Character('c');

也可以直接使用:

Character C = new Character('x');

這樣做的原因將在以後的章節裡解釋。

1. 高精度數字

Java 1.1增加了兩個類,用於進行高精度的計算:BigIntegerBigDecimal。儘管它們大致可以劃分為“包裝器”類型,但兩者都沒有對應的“基本類型”。

這兩個類都有自己特殊的“方法”,對應於我們針對基本類型執行的操作。也就是說,能對intfloat做的事情,對BigIntegerBigDecimal一樣可以做。只是必須使用方法調用,不能使用運算符。此外,由於牽涉更多,所以運算速度會慢一些。我們犧牲了速度,但換來了精度。

BigInteger支持任意精度的整數。也就是說,我們可精確表示任意大小的整數值,同時在運算過程中不會丟失任何信息。 BigDecimal支持任意精度的定點數字。例如,可用它進行精確的幣值計算。

至於調用這兩個類時可選用的構造器和方法,請自行參考聯機幫助文檔。

2.2.3 Java的數組

幾乎所有程序設計語言都支持數組。在C和C++裡使用數組是非常危險的,因為那些數組只是內存塊。若程序訪問自己內存塊以外的數組,或者在初始化之前使用內存(屬於常規編程錯誤),會產生不可預測的後果(註釋②)。

②:在C++裡,應儘量不要使用數組,換用標準模板庫(Standard TemplateLibrary)裡更安全的容器。

Java的一項主要設計目標就是安全性。所以在C和C++裡困擾程序員的許多問題都未在Java裡重複。一個Java可以保證被初始化,而且不可在它的範圍之外訪問。由於系統自動進行範圍檢查,所以必然要付出一些代價:針對每個數組,以及在運行期間對索引的校驗,都會造成少量的內存開銷。但由此換回的是更高的安全性,以及更高的工作效率。為此付出少許代價是值得的。

創建對象數組時,實際創建的是一個引用數組。而且每個引用都會自動初始化成一個特殊值,並帶有自己的關鍵字:null(空)。一旦Java看到null,就知道該引用並未指向一個對象。正式使用前,必須為每個引用都分配一個對象。若試圖使用依然為null的一個引用,就會在運行期報告問題。因此,典型的數組錯誤在Java裡就得到了避免。

也可以創建基本類型數組。同樣地,編譯器能夠擔保對它的初始化,因為會將那個數組的內存劃分成零。

數組問題將在以後的章節裡詳細討論。

2.3 絕對不要清除對象

在大多數程序設計語言中,變量的“存在時間”(Lifetime)一直是程序員需要著重考慮的問題。變量應持續多長的時間?如果想清除它,那麼何時進行?在變量存在時間上糾纏不清會造成大量的程序錯誤。在下面的小節裡,將闡示Java如何幫助我們完成所有清除工作,從而極大了簡化了這個問題。

2.3.1 作用域

大多數程序設計語言都提供了“作用域”(Scope)的概念。對於在作用域裡定義的名字,作用域同時決定了它的“可見性”以及“存在時間”。在C,C++和Java裡,作用域是由花括號的位置決定的。參考下面這個例子:

{
  int x = 12;
  /* only x available */
  {
    int q = 96;
    /* both x & q available */
  }
  /* only x available */
  /* q “out of scope” */
}

作為在作用域裡定義的一個變量,它只有在那個作用域結束之前才可使用。

在上面的例子中,縮進排版使Java代碼更易辨讀。由於Java是一種形式自由的語言,所以額外的空格、製表位以及回車都不會對結果程序造成影響。

注意儘管在C和C++裡是合法的,但在Java裡不能象下面這樣書寫代碼:

{
  int x = 12;
  {
    int x = 96; /* illegal */
  }
}

編譯器會認為變量x已被定義。所以C和C++能將一個變量“隱藏”在一個更大的作用域裡。但這種做法在Java裡是不允許的,因為Java的設計者認為這樣做使程序產生了混淆。

2.3.2 對象的作用域

Java對象不具備與基本類型一樣的存在時間。用new關鍵字創建一個Java對象的時候,它會超出作用域的範圍之外。所以假若使用下面這段代碼:

{
String s = new String("a string");
} /* 作用域的終點 */

那麼引用s會在作用域的終點處消失。然而,s指向的String對象依然佔據著內存空間。在上面這段代碼裡,我們沒有辦法訪問對象,因為指向它的唯一一個引用已超出了作用域的邊界。在後面的章節裡,大家還會繼續學習如何在程序運行期間傳遞和複製對象引用。

這樣造成的結果便是:對於用new創建的對象,只要我們願意,它們就會一直保留下去。這個編程問題在C和C++裡特別突出。看來在C++裡遇到的麻煩最大:由於不能從語言獲得任何幫助,所以在需要對象的時候,根本無法確定它們是否可用。而且更麻煩的是,在C++裡,一旦工作完成,必須保證將對象清除。

這樣便帶來了一個有趣的問題。假如Java讓對象依然故我,怎樣才能防止它們大量充斥內存,並最終造成程序的“凝固”呢。在C++裡,這個問題最令程序員頭痛。但Java以後,情況卻發生了改觀。Java有一個特別的“垃圾收集器”,它會查找用new創建的所有對象,並辨別其中哪些不再被引用。隨後,它會自動釋放由那些閒置對象佔據的內存,以便能由新對象使用。這意味著我們根本不必操心內存的回收問題。只需簡單地創建對象,一旦不再需要它們,它們就會自動離去。這樣做可防止在C++裡很常見的一個編程問題:由於程序員忘記釋放內存造成的“內存溢出”。

2.4 新建數據類型:類

(2)4 新建數據類型:類

如果說一切東西都是對象,那麼用什麼決定一個“類”(Class)的外觀與行為呢?換句話說,是什麼建立起了一個對象的“類型”(Type)呢?大家可能猜想有一個名為type的關鍵字。但從歷史看來,大多數面向對象的語言都用關鍵字class表達這樣一個意思:“我準備告訴你對象一種新類型的外觀”。class關鍵字太常用了,以至於本書許多地方並沒有用粗體字或雙引號加以強調。在這個關鍵字的後面,應該跟隨新數據類型的名稱。例如:

class ATypeName {/*類主體置於這裡}

這樣就引入了一種新類型,接下來便可用new創建這種類型的一個新對象:

ATypeName a = new ATypeName();

ATypeName裡,類主體只由一條註釋構成(星號和斜槓以及其中的內容,本章後面還會詳細講述),所以並不能對它做太多的事情。事實上,除非為其定義了某些方法,否則根本不能指示它做任何事情。

2.4.1 字段和方法

定義一個類時(我們在Java裡的全部工作就是定義類、製作那些類的對象以及將消息發給那些對象),可在自己的類裡設置兩種類型的元素:數據成員(有時也叫“字段”)以及成員函數(通常叫“方法”)。其中,數據成員是一種對象(通過它的引用與其通信),可以為任何類型。它也可以是基本類型(並不是引用)之一。如果是指向對象的一個引用,則必須初始化那個引用,用一種名為“構造器”(第4章會對此詳述)的特殊函數將其與一個實際對象連接起來(就象早先看到的那樣,使用new關鍵字)。但若是一種基本類型,則可在類定義位置直接初始化(正如後面會看到的那樣,引用亦可在定義位置初始化)。

每個對象都為自己的數據成員保有存儲空間;數據成員不會在對象之間共享。下面是定義了一些數據成員的類示例:

class DataOnly {
  int i;
  float f;
  boolean b;
}

這個類並沒有做任何實質性的事情,但我們可創建一個對象:

DataOnly d = new DataOnly();

可將值賦給數據成員,但首先必須知道如何引用一個對象的成員。為達到引用對象成員的目的,首先要寫上對象引用的名字,再跟隨一個點號(句點),再跟隨對象內部成員的名字。即“對象引用.成員”。例如:

d.i = 47;
d.f = 1.1f;
d.b = false;

一個對象也可能包含了另一個對象,而另一個對象裡則包含了我們想修改的數據。對於這個問題,只需保持“連接句點”即可。例如:

myPlane.leftTank.capacity = 100;

除容納數據之外,DataOnly類再也不能做更多的事情,因為它沒有成員函數(方法)。為正確理解工作原理,首先必須知道“參數”和“返回值”的概念。我們馬上就會詳加解釋。

1. 基本類型的成員的默認值

若某個類成員屬於基本類型,那麼即使不明確(顯式)進行初始化,也可以保證它們獲得一個默認值。

基本類型 默認值

Boolean false
Char '\u0000'(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d

一旦將變量作為類成員使用,就要特別注意由Java分配的默認值。這樣做可保證基本類型的成員變量肯定得到了初始化(C++不具備這一功能),可有效遏止多種相關的編程錯誤。

然而,這種保證卻並不適用於“局部”變量——那些變量並非一個類的字段。所以,假若在一個函數定義中寫入下述代碼:

int x;

那麼x會得到一些隨機值(這與C和C++是一樣的),不會自動初始化成零。我們責任是在正式使用x前分配一個適當的值。如果忘記,就會得到一條編譯期錯誤,告訴我們變量可能尚未初始化。這種處理正是Java優於C++的表現之一。許多C++編譯器會對變量未初始化發出警告,但在Java裡卻是錯誤。

2.5 方法、參數和返回值

迄今為止,我們一直用“函數”(Function)這個詞指代一個已命名的子例程。但在Java裡,更常用的一個詞卻是“方法”(Method),代表“完成某事的途徑”。儘管它們表達的實際是同一個意思,但從現在開始,本書將一直使用“方法”,而不是“函數”。

Java的“方法”決定了一個對象能夠接收的消息。通過本節的學習,大家會知道方法的定義有多麼簡單!

方法的基本組成部分包括名字、參數、返回類型以及主體。下面便是它最基本的形式:

返回類型 方法名( /* 參數列表*/ ) {/* 方法主體 */}

返回類型是指調用方法之後返回的數值類型。顯然,方法名的作用是對具體的方法進行標識和引用。參數列表列出了想傳遞給方法的信息類型和名稱。

Java的方法只能作為類的一部分創建。只能針對某個對象調用一個方法(註釋③),而且那個對象必須能夠執行那個方法調用。若試圖為一個對象調用錯誤的方法,就會在編譯期得到一條出錯消息。為一個對象調用方法時,需要先列出對象的名字,在後面跟上一個句點,再跟上方法名以及它的參數列表。亦即對象名.方法名(參數1,參數2,參數3...)。舉個例子來說,假設我們有一個方法名叫f(),它沒有參數,返回的是類型為int的一個值。那麼,假設有一個名為a的對象,可為其調用方法f(),則代碼如下:

int x = a.f();

返回值的類型必須兼容x的類型。

象這樣調用一個方法的行動通常叫作“向對象發送一條消息”。在上面的例子中,消息是f(),而對象是a。面向對象的程序設計通常簡單地歸納為“向對象發送消息”。

③:正如馬上就要學到的那樣,“靜態”方法可針對類調用,毋需一個對象。

2.5.1 參數列表

參數列表規定了我們傳送給方法的是什麼信息。正如大家或許已猜到的那樣,這些信息——如同Java內其他任何東西——採用的都是對象的形式。因此,我們必須在參數列表裡指定要傳遞的對象類型,以及每個對象的名字。正如在Java其他地方處理對象時一樣,我們實際傳遞的是“引用”(註釋④)。然而,引用的類型必須正確。倘若希望參數是一個“字符串”,那麼傳遞的必須是一個字符串。

④:對於前面提及的“特殊”數據類型booleancharbyteshortintlong,,float以及double來說是一個例外。但在傳遞對象時,通常都是指傳遞指向對象的引用。

下面讓我們考慮將一個字符串作為參數使用的方法。下面列出的是定義代碼,必須將它置於一個類定義裡,否則無法編譯:

int storage(String s) {
return s.length() * 2;
}

這個方法告訴我們需要多少字節才能容納一個特定字符串裡的信息(字符串裡的每個字符都是16位,或者說2個字節、長整數,以便提供對Unicode字符的支持)。參數的類型為String,而且叫作s。一旦將s傳遞給方法,就可將它當作其他對象一樣處理(可向其發送消息)。在這裡,我們調用的是length()方法,它是String的方法之一。該方法返回的是一個字符串裡的字符數。

通過上面的例子,也可以瞭解return關鍵字的運用。它主要做兩件事情。首先,它意味著“離開方法,我已完工了”。其次,假設方法生成了一個值,則那個值緊接在return語句的後面。在這種情況下,返回值是通過計算表達式s.length()*2而產生的。 可按自己的願望返回任意類型,但倘若不想返回任何東西,就可指示方法返回void(空)。下面列出一些例子。

boolean flag() { return true; }
float naturalLogBase() { return 2.718; }
void nothing() { return; }
void nothing2() {}

若返回類型為void,則return關鍵字唯一的作用就是退出方法。所以一旦抵達方法末尾,該關鍵字便不需要了。可在任何地方從一個方法返回。但假設已指定了一種非void的返回類型,那麼無論從何地返回,編譯器都會確保我們返回的是正確的類型。

到此為止,大家或許已得到了這樣的一個印象:一個程序只是一系列對象的集合,它們的方法將其他對象作為自己的參數使用,而且將消息發給那些對象。這種說法大體正確,但通過以後的學習,大家還會知道如何在一個方法裡作出決策,做一些更細緻的基層工作。至於這一章,只需理解消息傳送就足夠了。

2.6 構建Java程序

正式構建自己的第一個Java程序前,還有幾個問題需要注意。

2.6.1 名字的可見性

在所有程序設計語言裡,一個不可避免的問題是對名字或名稱的控制。假設您在程序的某個模塊裡使用了一個名字,而另一名程序員在另一個模塊裡使用了相同的名字。此時,如何區分兩個名字,並防止兩個名字互相沖突呢?這個問題在C語言裡特別突出。因為程序未提供很好的名字管理方法。C++的類(即Java類的基礎)嵌套使用類裡的函數,使其不至於同其他類裡的嵌套函數名衝突。然而,C++仍然允許使用全局數據以及全局函數,所以仍然難以避免衝突。為解決這個問題,C++用額外的關鍵字引入了“命名空間”的概念。

由於採用全新的機制,所以Java能完全避免這些問題。為了給一個庫生成明確的名字,採用了與Internet域名類似的名字。事實上,Java的設計者鼓勵程序員反轉使用自己的Internet域名,因為它們肯定是獨一無二的。由於我的域名是BruceEckel.com,所以我的實用工具庫就可命名為com.bruceeckel.utility.foibles。反轉了域名後,可將點號想象成子目錄。

在Java 1.0和Java 1.1中,域擴展名comeduorgnet等都約定為大寫形式。所以庫的樣子就變成:COM.bruceeckel.utility.foibles。然而,在Java 1.2的開發過程中,設計者發現這樣做會造成一些問題。所以目前的整個軟件包都以小寫字母為標準。

Java的這種特殊機制意味著所有文件都自動存在於自己的命名空間裡。而且一個文件裡的每個類都自動獲得一個獨一無二的標識符(當然,一個文件裡的類名必須是唯一的)。所以不必學習特殊的語言知識來解決這個問題——語言本身已幫我們照顧到這一點。

2.6.2 使用其他組件

一旦要在自己的程序裡使用一個預先定義好的類,編譯器就必須知道如何找到它。當然,這個類可能就在發出調用的那個相同的源碼文件裡。如果是那種情況,只需簡單地使用這個類即可——即使它直到文件的後面仍未得到定義。Java消除了“向前引用”的問題,所以不要關心這些事情。

但假若那個類位於其他文件裡呢?您或許認為編譯器應該足夠“聯盟”,可以自行發現它。但實情並非如此。假設我們想使用一個具有特定名稱的類,但那個類的定義位於多個文件裡。或者更糟,假設我們準備寫一個程序,但在創建它的時候,卻向自己的庫加入了一個新類,它與現有某個類的名字發生了衝突。

為解決這個問題,必須消除所有潛在的、糾纏不清的情況。為達到這個目的,要用import關鍵字準確告訴Java編譯器我們希望的類是什麼。import的作用是指示編譯器導入一個“包”——或者說一個“類庫”(在其他語言裡,可將“庫”想象成一系列函數、數據以及類的集合。但請記住,Java的所有代碼都必須寫入一個類中)。

大多數時候,我們直接採用來自標準Java庫的組件(部件)即可,它們是與編譯器配套提供的。使用這些組件時,沒有必要關心冗長的保留域名;舉個例子來說,只需象下面這樣寫一行代碼即可:

import java.util.Vector;

它的作用是告訴編譯器我們想使用Java的Vector類。然而,util包含了數量眾多的類,我們有時希望使用其中的幾個,同時不想全部明確地聲明它們。為達到這個目的,可使用*通配符。如下所示:

import java.util.*;

需導入一系列類時,採用的通常是這個辦法。應儘量避免一個一個地導入類。

2.6.3 static關鍵字

通常,我們創建類時會指出那個類的對象的外觀與行為。除非用new創建那個類的一個對象,否則實際上並未得到任何東西。只有執行了new後,才會正式生成數據存儲空間,並可使用相應的方法。

但在兩種特殊的情形下,上述方法並不堪用。一種情形是隻想用一個存儲區域來保存一個特定的數據——無論要創建多少個對象,甚至根本不創建對象。另一種情形是我們需要一個特殊的方法,它沒有與這個類的任何對象關聯。也就是說,即使沒有創建對象,也需要一個能調用的方法。為滿足這兩方面的要求,可使用static(靜態)關鍵字。一旦將什麼東西設為static,數據或方法就不會同那個類的任何對象實例聯繫到一起。所以儘管從未創建那個類的一個對象,仍能調用一個static方法,或訪問一些static數據。而在這之前,對於非static數據和方法,我們必須創建一個對象,並用那個對象訪問數據或方法。這是由於非static數據和方法必須知道它們操作的具體對象。當然,在正式使用前,由於static方法不需要創建任何對象,所以它們不可簡單地調用其他那些成員,同時不引用一個已命名的對象,從而直接訪問非static成員或方法(因為非static成員和方法必須同一個特定的對象關聯到一起)。

有些面向對象的語言使用了“類數據”和“類方法”這兩個術語。它們意味著數據和方法只是為作為一個整體的類而存在的,並不是為那個類的任何特定對象。有時,您會在其他一些Java書刊裡發現這樣的稱呼。

為了將數據成員或方法設為static,只需在定義前置和這個關鍵字即可。例如,下述代碼能生成一個static數據成員,並對其初始化:

class StaticTest {
Static int i = 47;
}

現在,儘管我們製作了兩個StaticTest對象,但它們仍然只佔據StaticTest.i的一個存儲空間。這兩個對象都共享同樣的i。請考察下述代碼:

StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();

此時,無論st1.i還是st2.i都有同樣的值47,因為它們引用的是同樣的內存區域。

有兩個辦法可引用一個static變量。正如上面展示的那樣,可通過一個對象命名它,如st2.i。亦可直接用它的類名引用,而這在非靜態成員裡是行不通的(最好用這個辦法引用static變量,因為它強調了那個變量的“靜態”本質)。

StaticTest.i++;

其中,++運算符會使變量自增。此時,無論st1.i還是st2.i的值都是48。

類似的邏輯也適用於靜態方法。既可象對其他任何方法那樣通過一個對象引用靜態方法,亦可用特殊的語法格式類名.方法()加以引用。靜態方法的定義是類似的:

class StaticFun {
static void incr() { StaticTest.i++; }
}

從中可看出,StaticFun的方法incr()使靜態數據i自增。通過對象,可用典型的方法調用incr()

StaticFun sf = new StaticFun();
sf.incr();

或者,由於incr()是一種靜態方法,所以可通過它的類直接調用:

StaticFun.incr();

儘管是“靜態”的,但只要應用於一個數據成員,就會明確改變數據的創建方式(一個類一個成員,以及每個對象一個非靜態成員)。若應用於一個方法,就沒有那麼戲劇化了。對方法來說,static一項重要的用途就是幫助我們在不必創建對象的前提下調用那個方法。正如以後會看到的那樣,這一點是至關重要的——特別是在定義程序運行入口方法main()的時候。

和其他任何方法一樣,static方法也能創建自己類型的命名對象。所以經常把static方法作為一個“領頭羊”使用,用它生成一系列自己類型的“實例”。

2.7 我們的第一個Java程序

最後,讓我們正式編一個程序(註釋⑤)。它能打印出與當前運行的系統有關的資料,並利用了來自Java標準庫的System對象的多種方法。注意這裡引入了一種額外的註釋樣式://。它表示到本行結束前的所有內容都是註釋:

// Property.java
import java.util.*;

public class Property {
  public static void main(String[] args) {
    System.out.println(new Date());
    Properties p = System.getProperties();
    p.list(System.out);
    System.out.println("--- Memory Usage:");
    Runtime rt = Runtime.getRuntime();
    System.out.println("Total Memory = "
                       + rt.totalMemory()
                       + " Free Memory = "
                       + rt.freeMemory());
  }
}

⑤:在某些編程環境裡,程序會在屏幕上一切而過,甚至沒機會看到結果。可將下面這段代碼置於main()的末尾,用它暫停輸出:

try {
Thread.currentThread().sleep(5 * 1000);
} catch(InterruptedException e) {}
}

它的作用是暫停輸出5秒鐘。這段代碼涉及的一些概念要到本書後面才會講到。所以目前不必深究,只知道它是讓程序暫停的一個技巧便可。

在每個程序文件的開頭,都必須放置一個import語句,導入那個文件的代碼裡要用到的所有額外的類。注意我們說它們是“額外”的,因為一個特殊的類庫會自動導入每個Java文件:java.lang。啟動您的Web瀏覽器,查看由Sun提供的用戶文檔(如果尚未從 http://www.java.sun.com 下載,或用其他方式安裝了Java文檔,請立即下載)。在packages.html文件裡,可找到Java配套提供的所有類庫名稱。請選擇其中的java.lang。在“Class Index”下面,可找到屬於那個庫的全部類的列表。由於java.lang默認進入每個Java代碼文件,所以這些類在任何時候都可直接使用。在這個列表裡,可發現SystemRuntime,我們在Property.java裡用到了它們。java.lang裡沒有列出Date類,所以必須導入另一個類庫才能使用它。如果不清楚一個特定的類在哪個類庫裡,或者想檢視所有的類,可在Java用戶文檔裡選擇“Class Hierarchy”(類分級結構)。在Web瀏覽器中,雖然要花不短的時間來建立這個結構,但可清楚找到與Java配套提供的每一個類。隨後,可用瀏覽器的“查找”(Find)功能搜索關鍵字Date。經這樣處理後,可發現我們的搜索目標以java.util.Date的形式列出。我們終於知道它位於util庫裡,所以必須導入 java.util.*;否則便不能使用Date

觀察packages.html文檔最開頭的部分(我已將其設為自己的默認起始頁),請選擇java.lang,再選System。這時可看到System類有幾個字段。若選擇out,就可知道它是一個static PrintStream對象。由於它是“靜態”的,所以不需要我們創建任何東西。out對象肯定是3,所以只需直接用它即可。我們能對這個out對象做的事情由它的類型決定:PrintStreamPrintStream在說明文字中以一個超鏈接的形式列出,這一點做得非常方便。所以假若單擊那個鏈接,就可看到能夠為PrintStream調用的所有方法。方法的數量不少,本書後面會詳細介紹。就目前來說,我們感興趣的只有println()。它的意思是“把我給你的東西打印到控制檯,並用一個新行結束”。所以在任何Java程序中,一旦要把某些內容打印到控制檯,就可條件反射地寫上System.out.println("內容")

類名與文件是一樣的。若象現在這樣創建一個獨立的程序,文件中的一個類必須與文件同名(如果沒這樣做,編譯器會及時作出反應)。類裡必須包含一個名為main()的方法,形式如下:

public static void main(String[] args) {

其中,關鍵字public意味著方法可由外部世界調用(第5章會詳細解釋)。main()的參數是包含了String對象的一個數組。args不會在本程序中用到,但需要在這個地方列出,因為它們保存了在命令行調用的參數。 程序的第一行非常有趣:

System.out.println(new Date());

請觀察它的參數:創建Date對象唯一的目的就是將它的值發送給println()。一旦這個語句執行完畢,Date就不再需要。隨之而來的“垃圾收集器”會發現這一情況,並在任何可能的時候將其回收。事實上,我們沒太大的必要關心“清除”的細節。

第二行調用了System.getProperties()。若用Web瀏覽器查看聯機用戶文檔,就可知道getProperties()System類的一個static方法。由於它是“靜態”的,所以不必創建任何對象便可調用該方法。無論是否存在該類的一個對象,static方法隨時都可使用。調用getProperties()時,它會將系統屬性作為Properties類的一個對象生成(注意Properties是“屬性”的意思)。隨後的的引用保存在一個名為pProperties引用裡。在第三行,大家可看到Properties對象有一個名為list()的方法,它將自己的全部內容都發給一個我們作為參數傳遞的PrintStream對象。

main() 的第四和第六行是典型的打印語句。注意為了打印多個String值,用加號(+)分隔它們即可。然而,也要在這裡注意一些奇怪的事情。在String對象中使用時,加號並不代表真正的“相加”。處理字符串時,我們通常不必考慮+的任何特殊含義。但是,Java的String類要受一種名為“運算符重載”的機制的制約。也就是說,只有在隨同String對象使用時,加號才會產生與其他任何地方不同的表現。對於字符串,它的意思是“連接這兩個字符串”。

但事情到此並未結束。請觀察下述語句:

System.out.println("Total Memory = "
+ rt.totalMemory()
+ " Free Memory = "
+ rt.freeMemory());

其中,totalMemory()freeMemory()返回的是數值,並非String對象。如果將一個數值“加”到一個字符串身上,會發生什麼情況呢?同我們一樣,編譯器也會意識到這個問題,並魔術般地調用一個方法,將那個數值(intfloat等等)轉換成字符串。經這樣處理後,它們當然能利用加號“加”到一起。這種“自動類型轉換”亦劃入“運算符重載”處理的範疇。

許多Java著作都在熱烈地辯論“運算符重載”(C++的一項特性)是否有用。目前就是反對它的一個好例子!然而,這最多隻能算編譯器(程序)的問題,而且只是對String對象而言。對於自己編寫的任何源代碼,都不可能使運算符“重載”。

通過為Runtime類調用getRuntime()方法,main()的第五行創建了一個Runtime對象。返回的則是指向一個Runtime對象的引用。而且,我們不必關心它是一個靜態對象,還是由new命令創建的一個對象。這是由於我們不必為清除工作負責,可以大模大樣地使用對象。正如顯示的那樣,Runtime可告訴我們與內存使用有關的信息。

2.8 註釋和嵌入文檔

(2)8 註釋和嵌入文檔

Java裡有兩種類型的註釋。第一種是傳統的、C語言風格的註釋,是從C++繼承而來的。這些註釋用一個 /* 起頭,隨後是註釋內容,並可跨越多行,最後用一個*/結束。注意許多程序員在連續註釋內容的每一行都用一個 * 開頭,所以經常能看到象下面這樣的內容:

/* 這是
* 一段註釋,
* 它跨越了多個行
*/

但請記住,進行編譯時,/**/之間的所有東西都會被忽略,所以上述註釋與下面這段註釋並沒有什麼不同:

/* 這是一段註釋,
它跨越了多個行 */

第二種類型的註釋也起源於C++。這種註釋叫作“單行註釋”,以一個 // 起頭,表示這一行的所有內容都是註釋。這種類型的註釋更常用,因為它書寫時更方便。沒有必要在鍵盤上尋找 / ,再尋找 * (只需按同樣的鍵兩次),而且不必在註釋結尾時加一個結束標記。下面便是這類註釋的一個例子:

// 這是一條單行註釋

2.8.1 註釋文檔

對於Java語言,最體貼的一項設計就是它並沒有打算讓人們為了寫程序而寫程序——人們也需要考慮程序的文檔化問題。對於程序的文檔化,最大的問題莫過於對文檔的維護。若文檔與代碼分離,那麼每次改變代碼後都要改變文檔,這無疑會變成相當麻煩的一件事情。解決的方法看起來似乎很簡單:將代碼同文檔“鏈接”起來。為達到這個目的,最簡單的方法是將所有內容都置於同一個文件。然而,為使一切都整齊劃一,還必須使用一種特殊的註釋語法,以便標記出特殊的文檔;另外還需要一個工具,用於提取這些註釋,並按有價值的形式將其展現出來。這些都是Java必須做到的。

用於提取註釋的工具叫作javadoc。它採用了部分來自Java編譯器的技術,查找我們置入程序的特殊註釋標記。它不僅提取由這些標記指示的信息,也將毗鄰註釋的類名或方法名提取出來。這樣一來,我們就可用最輕的工作量,生成十分專業的程序文檔。

javadoc輸出的是一個HTML文件,可用自己的Web瀏覽器查看。該工具允許我們創建和管理單個源文件,並生動生成有用的文檔。由於有了javadoc,所以我們能夠用標準的方法創建文檔。而且由於它非常方便,所以我們能輕鬆獲得所有Java庫的文檔。

2.8.2 具體語法

所有javadoc命令都只能出現於 /** 註釋中。但和平常一樣,註釋結束於一個 */ 。主要通過兩種方式來使用javadoc:嵌入的HTML,或使用“文檔標記”。其中,“文檔標記”(Doc tags)是一些以@開頭的命令,置於註釋行的起始處(但前導的*會被忽略)。

有三種類型的註釋文檔,它們對應於位於註釋後面的元素:類、變量或者方法。也就是說,一個類註釋正好位於一個類定義之前;變量註釋正好位於變量定義之前;而一個方法定義正好位於一個方法定義的前面。如下面這個簡單的例子所示:

/** 一個類註釋 */
public class docTest {
/** 一個變量註釋 */
public int i;
/** 一個方法註釋 */
public void f() {}
}

注意javadoc只能為public(公共)和protected(受保護)成員處理註釋文檔。private(私有)和“友好”(詳見5章)成員的註釋會被忽略,我們看不到任何輸出(也可以用-private標記包括private成員)。這樣做是有道理的,因為只有publicprotected成員才可在文件之外使用,這是客戶程序員的希望。然而,所有類註釋都會包含到輸出結果裡。

上述代碼的輸出是一個HTML文件,它與其他Java文檔具有相同的標準格式。因此,用戶會非常熟悉這種格式,可在您設計的類中方便地“漫遊”。設計程序時,請務必考慮輸入上述代碼,用javadoc處理一下,觀看最終HTML文件的效果如何。

2.8.3 嵌入HTML

javadoc將HTML命令傳遞給最終生成的HTML文檔。這便使我們能夠充分利用HTML的巨大威力。當然,我們的最終動機是格式化代碼,不是為了譁眾取寵。下面列出一個例子:

/**
* <pre>
* System.out.println(new Date());
* </pre>
*/

亦可象在其他Web文檔裡那樣運用HTML,對普通文本進行格式化,使其更具條理、更加美觀:

/**
* 您<em>甚至</em>可以插入一個列表:
* <ol>
* <li> 項目一
* <li> 項目二
* <li> 項目三
* </ol>
*/

注意在文檔註釋中,位於一行最開頭的星號會被javadoc丟棄。同時丟棄的還有前導空格。javadoc 會對所有內容進行格式化,使其與標準的文檔外觀相符。不要將<h1><hr>這樣的標題當作嵌入HTML使用,因為javadoc會插入自己的標題,我們給出的標題會與之衝撞。

所有類型的註釋文檔——類、變量和方法——都支持嵌入HTML。

2.8.4 @see:引用其他類

所有三種類型的註釋文檔都可包含@see標記,它允許我們引用其他類裡的文檔。對於這個標記,javadoc會生成相應的HTML,將其直接鏈接到其他文檔。格式如下:

@see 類名
@see 完整類名
@see 完整類名#方法名

每一格式都會在生成的文檔裡自動加入一個超鏈接的“See Also”(參見)條目。注意javadoc不會檢查我們指定的超鏈接,不會驗證它們是否有效。

2.8.5 類文檔標記

隨同嵌入HTML和@see引用,類文檔還可以包括用於版本信息以及作者姓名的標記。類文檔亦可用於“接口”目的(本書後面會詳細解釋)。

1. @version

格式如下:

@version 版本信息

其中,“版本信息”代表任何適合作為版本說明的資料。若在javadoc命令行使用了-version標記,就會從生成的HTML文檔裡提取出版本信息。

2. @author

格式如下:

@author 作者信息

其中,“作者信息”包括您的姓名、電子函件地址或者其他任何適宜的資料。若在javadoc命令行使用了-author標記,就會專門從生成的HTML文檔裡提取出作者信息。

可為一系列作者使用多個這樣的標記,但它們必須連續放置。全部作者信息會一起存入最終HTML代碼的單獨一個段落裡。

2.8.6 變量文檔標記

變量文檔只能包括嵌入的HTML以及@see引用。

2.8.7 方法文檔標記

除嵌入HTML和@see引用之外,方法還允許使用針對參數、返回值以及異常的文檔標記。

1. @param 格式如下:

@param 參數名 說明

其中,“參數名”是指參數列表內的標識符,而“說明”代表一些可延續到後續行內的說明文字。一旦遇到一個新文檔標記,就認為前一個說明結束。可使用任意數量的說明,每個參數一個。

2. @return

格式如下:

@return 說明

其中,“說明”是指返回值的含義。它可延續到後面的行內。

3. @exception

有關“異常”(Exception)的詳細情況,我們會在第9章講述。簡言之,它們是一些特殊的對象,若某個方法失敗,就可將它們“扔出”對象。調用一個方法時,儘管只有一個異常對象出現,但一些特殊的方法也許能產生任意數量的、不同類型的異常。所有這些異常都需要說明。所以,異常標記的格式如下:

@exception 完整類名 說明

其中,“完整類名”明確指定了一個異常類的名字,它是在其他某個地方定義好的。而“說明”(同樣可以延續到下面的行)告訴我們為什麼這種特殊類型的異常會在方法調用中出現。

4. @deprecated

這是Java 1.1的新特性。該標記用於指出一些舊功能已由改進過的新功能取代。該標記的作用是建議用戶不必再使用一種特定的功能,因為未來改版時可能摒棄這一功能。若將一個方法標記為@deprecated,則使用該方法時會收到編譯器的警告。

2.8.8 文檔示例

下面還是我們的第一個Java程序,只不過已加入了完整的文檔註釋:

92頁程序

第一行:

//: Property.java

採用了我自己的方法:將一個:作為特殊的記號,指出這是包含了源文件名字的一個註釋行。最後一行也用這樣的一條註釋結尾,它標誌著源代碼清單的結束。這樣一來,可將代碼從本書的正文中方便地提取出來,並用一個編譯器檢查。這方面的細節在第17章講述。

2.9 編碼樣式

一個非正式的Java編程標準是大寫一個類名的首字母。若類名由幾個單詞構成,那麼把它們緊靠到一起(也就是說,不要用下劃線來分隔名字)。此外,每個嵌入單詞的首字母都採用大寫形式。例如:

class AllTheColorsOfTheRainbow { // ...}

對於其他幾乎所有內容:方法、字段(成員變量)以及對象引用名稱,可接受的樣式與類樣式差不多,只是標識符的第一個字母採用小寫。例如:

class AllTheColorsOfTheRainbow {
int anIntegerRepresentingColors;
void changeTheHueOfTheColor(int newHue) {
// ...
}
// ...
}

當然,要注意用戶也必須鍵入所有這些長名字,而且不能輸錯。

2.10 總結

通過本章的學習,大家已接觸了足夠多的Java編程知識,已知道如何自行編寫一個簡單的程序。此外,對語言的總體情況以及一些基本思想也有了一定程度的認識。然而,本章所有例子的模式都是單線形式的“這樣做,再那樣做,然後再做另一些事情”。如果想讓程序作出一項選擇,又該如何設計呢?例如,“假如這樣做的結果是紅色,就那樣做;如果不是,就做另一些事情”。對於這種基本的編程方法,下一章會詳細說明在Java裡是如何實現的。

2.11 練習

(1) 參照本章的第一個例子,創建一個“Hello,World”程序,在屏幕上簡單地顯示這句話。注意在自己的類裡只需一個方法(main方法會在程序啟動時執行)。記住要把它設為static形式,並置入參數列表——即使根本不會用到這個列表。用javac編譯這個程序,再用java運行它。

(2) 寫一個程序,打印出從命令行獲取的三個參數。

(3) 找出Property.java第二個版本的代碼,這是一個簡單的註釋文檔示例。請對文件執行javadoc,並在自己的Web瀏覽器裡觀看結果。

(4) 以練習(1)的程序為基礎,向其中加入註釋文檔。利用javadoc,將這個註釋文檔提取為一個HTML文件,並用Web瀏覽器觀看。