Java/面向物件程式設計之道
programming language language!programming programming style object-oriented programming functional programming procedural programming programming!object-oriented programming!functional programming!procedural
世界上有許多程式語言,程式設計風格(有時稱為正規化)也幾乎一樣多。本書中出現的三個風格是過程式、函式式和麵向物件。雖然 Java 通常被認為是一種面嚮物件語言,但可以使用任何風格編寫 Java 程式。我在本書中展示的風格幾乎是過程式的。現有的 Java 程式和內建的 Java 包以三種風格的混合形式編寫,但它們比本書中的程式更傾向於面向物件。
很難定義什麼是面向物件程式設計,但以下是一些它的特徵
專案符號
物件定義(類)通常對應於相關的現實世界物件。例如,在第 deck 章中,建立 Deck 類是邁向面向物件程式設計的一步。
大多數方法是物件方法(您在物件上呼叫的方法),而不是類方法(您只是呼叫的方法)。到目前為止,我們編寫的所有方法都是類方法。在本章中,我們將編寫一些物件方法。
與面向物件程式設計最相關的語言特性是繼承。我將在本章後面介紹繼承。
專案符號
inheritance
最近,面向物件程式設計變得非常流行,有些人聲稱它在各個方面都優於其他風格。我希望透過向您介紹各種風格,我已經為您提供了理解和評估這些主張所需的工具。
object method method!object class method method!class static
Java 中有兩種型別的方法,稱為類方法和物件方法。到目前為止,我們編寫的每種方法都是類方法。類方法由第一行中的關鍵字 static 標識。任何沒有關鍵字 static 的方法都是物件方法。
雖然我們沒有編寫任何物件方法,但我們已經呼叫了一些。每當您在 _某個物件_ 上呼叫方法時,它就是一個物件方法。例如,drawOval 是我們在 g 上呼叫的物件方法,g 是一個 Graphics 物件。此外,我們在字串章節中呼叫的字串方法也是物件方法。
Graphics class!Graphics
任何可以用類方法編寫的東西也可以用物件方法編寫,反之亦然。有時使用其中一種方法比另一種方法更自然。出於很快就會變得清晰的原因,物件方法通常比相應的類方法更短。
current object object!current this
當您在物件上呼叫方法時,該物件成為當前物件。在方法內部,您可以透過名稱引用當前物件的例項變數,而無需指定物件名稱。
constructor
此外,您可以使用關鍵字 this 引用當前物件。我們已經看到它在建構函式中使用。實際上,您可以將建構函式視為一種特殊型別的物件方法。
complex number Complex class!Complex arithmetic!complex
作為本章剩餘部分的執行示例,我們將考慮一個用於複數的類定義。複數在數學和工程的許多分支中都很有用,許多計算是使用複數運算執行的。複數是實部和虛部的總和,通常寫成 _的形式,其中_ 是實部,_ 是虛部,表示 -1 的平方根。因此,_。
以下是用於名為 Complex 的新物件型別的新類定義
逐字類 Complex
// instance variables double real, imag;
// constructor public Complex () this.real = 0.0; this.imag = 0.0;
// constructor public Complex (double real, double imag) this.real = real; this.imag = imag;
逐字
這裡應該沒有什麼令人驚訝的。例項變數是兩個包含實部和虛部的雙精度數。兩個建構函式是常見的型別:一個不接受引數並將預設值分配給例項變數,另一個接受與例項變數相同的引數。正如我們之前所見,關鍵字 this 用於引用正在初始化的物件。
instance variable variable!instance constructor
在 main 中,或在任何我們想要建立 Complex 物件的地方,我們都可以選擇先建立物件然後設定例項變數,或者同時進行這兩項操作
逐字
Complex x = new Complex (); x.real = 1.0; x.imag = 2.0; Complex y = new Complex (3.0, 4.0);
逐字
operator!Complex method!function pure function
讓我們看一下我們可能想要對複數執行的一些操作。複數的絕對值定義為 _。abs 方法是一個純函式,計算絕對值。寫成類方法,它看起來像這樣
逐字
// class method public static double abs (Complex c) return Math.sqrt (c.real * c.real + c.imag * c.imag);
逐字
此版本的 abs 計算 c 的絕對值,即它作為引數接收的 Complex 物件。下一個版本的 abs 是一個物件方法;它計算當前物件的絕對值(呼叫該方法的物件)。因此,它不接收任何引數
逐字
// object method public double abs () return Math.sqrt (real*real + imag*imag);
逐字
我刪除了關鍵字 static 以表明這是一個物件方法。此外,我還消除了不必要的引數。在方法內部,我可以透過名稱引用例項變數 real 和 imag,而無需指定物件。Java 隱式地知道我正在引用當前物件的例項變數。如果我想明確表示,我可以使用關鍵字 this
逐字
// object method public double abs () return Math.sqrt (this.real * this.real + this.imag * this.imag);
逐字
但這會更長,而且實際上並沒有更清楚。要呼叫此方法,我們在物件上呼叫它,例如
逐字
Complex y = new Complex (3.0, 4.0); double result = y.abs();
逐字
我們可能想要對複數執行的另一個操作是加法。您可以透過新增實部和新增虛部來新增複數。寫成類方法,它看起來像這樣
逐字
public static Complex add (Complex a, Complex b) return new Complex (a.real + b.real, a.imag + b.imag);
逐字
要呼叫此方法,我們將兩個運算元作為引數傳遞
逐字
Complex sum = add (x, y);
逐字
寫成物件方法,它只接受一個引數,它會將其新增到當前物件
逐字
public Complex add (Complex b) return new Complex (real + b.real, imag + b.imag);
逐字
同樣,我們可以隱式地引用當前物件的例項變數,但要引用 b 的例項變數,我們必須使用點表示法明確地命名 b。要呼叫此方法,您在其中一個運算元上呼叫它,並將另一個運算元作為引數傳遞。
dot notation
逐字
Complex sum = x.add (y);
逐字
從這些示例中您可以看到,當前物件 (this) 可以替代其中一個引數。因此,當前物件有時被稱為隱式引數。
修飾符
modifier method!modifier
作為另一個示例,我們將看一下 conjugate,它是一個修改方法,它將 Complex 數轉換為它的複共軛。的複共軛是 _。
作為類方法,它看起來像這樣
逐字
public static void conjugate (Complex c) c.imag = -c.imag;
逐字
作為物件方法,它看起來像這樣
逐字
public void conjugate () imag = -imag;
逐字
現在你應該開始感覺到,將一種方法轉換為另一種方法是一個機械的過程。經過一些練習,你就能做到不假思索地進行轉換,這很好,因為你不應該被限制在編寫一種方法或另一種方法。你應該對兩種方法都熟悉,這樣你就可以選擇最適合你正在編寫的操作的方法。
例如,我認為 add 應該被寫成類方法,因為它是對兩個運算元的對稱操作,並且兩個運算元都作為引數出現是合理的。在其中一個運算元上呼叫該方法並將另一個運算元作為引數傳遞似乎很奇怪。
另一方面,應用於單個物件的簡單操作可以最簡潔地寫成物件方法(即使它們接受一些額外的引數)。
toString 方法
[edit | edit source]toString method!toString
有兩種物件方法在許多物件型別中很常見:toString 和 equals。toString 將物件轉換為一些合理的字串表示形式,可以打印出來。equals 用於比較物件。
當你使用 print 或 println 列印一個物件時,Java 會檢查你是否提供了一個名為 toString 的物件方法,如果有,它會呼叫它。如果沒有,它會呼叫 toString 的預設版本,該版本會產生 Section printobject 中描述的輸出。
以下是 toString 在 Complex 類中的可能樣子
逐字
public String toString () return real + " + " + imag + "i";
逐字
toString 的返回值型別是 String,當然,它不接受任何引數。你可以像往常一樣呼叫 toString
逐字
Complex x = new Complex (1.0, 2.0); String s = x.toString ();
逐字
或者你可以透過 print 間接呼叫它
逐字
System.out.println (x);
逐字
每當你將一個物件傳遞給 print 或 println 時,Java 都會呼叫該物件的 toString 方法並列印結果。在這種情況下,輸出是 1.0 + 2.0i。
如果虛部為負,此版本的 toString 看起來不太好。作為練習,請修正它。
equals 方法
[edit | edit source]equals method!equals
當你使用 == 運算子比較兩個物件時,你實際上是在問,“這兩個東西是同一個物件嗎?” 也就是說,這兩個物件是否指向記憶體中的同一位置。
對於許多型別來說,這不是相等性的適當定義。例如,如果兩個複數的實部相等,且虛部相等,那麼這兩個複數相等。
type!object
當你建立一個新的物件型別時,你可以透過提供一個名為 equals 的物件方法來提供你自己的相等性定義。對於 Complex 類,它看起來像這樣
逐字
public boolean equals (Complex b) return (real == b.real && imag == b.imag);
逐字
按照慣例,equals 始終是一個物件方法。返回值型別必須是 boolean。
Object 類中 equals 的文件提供了一些你在制定自己的相等性定義時應該牢記的指導方針
引用
equals 方法實現了一個等價關係
equality identity
專案符號
它是自反的:對於任何引用值 x,x.equals(x) 應該返回 true。
它是對稱的:對於任何引用值 x 和 y,如果且僅當 y.equals(x) 返回 true 時,x.equals(y) 應該返回 true。
它是傳遞的:對於任何引用值 x、y 和 z,如果 x.equals(y) 返回 true 且 y.equals(z) 返回 true,那麼 x.equals(z) 應該返回 true。
它是一致的:對於任何引用值 x 和 y,x.equals(y) 的多次呼叫始終返回 true 或始終返回 false。
對於任何引用值 x,x.equals(null) 應該返回 false。
專案符號
引用
我提供的 equals 定義滿足除一個之外的所有條件。哪一個?作為練習,請修正它。
從另一個物件方法中呼叫物件方法
method!invoking
正如你所料,從另一個物件方法中呼叫物件方法是合法的且常見的。例如,要規範化一個複數,你用絕對值(兩個部分都)除以它。這為什麼有用可能並不明顯,但確實有用。
讓我們將 normalize 方法寫成物件方法,並將其設為修飾符。
逐字
public void normalize () double d = this.abs(); real = real/d; imag = imag/d;
逐字
第一行透過在當前物件上呼叫 abs 來查詢當前物件的絕對值。在這種情況下,我明確地命名了當前物件,但我可以省略它。如果你在一個物件方法內呼叫另一個物件方法,Java 會假設你在當前物件上呼叫它。
作為練習,請將 normalize 重寫為純函式。然後將其重寫為類方法。
奇異性和錯誤
[edit | edit source]method!object method!class overloading
如果你在同一個類定義中既有物件方法又有類方法,很容易混淆。組織類定義的一種常見方法是將所有建構函式放在開頭,然後是所有物件方法,最後是所有類方法。
你可以在同一個類定義中擁有同名物件方法和類方法,只要它們的引數數量和型別不同即可。與其他型別的過載一樣,Java 會根據你提供 的引數來決定呼叫哪個版本。
static
現在我們知道了 static 關鍵字的含義,你可能已經明白 main 是一個類方法,這意味著在呼叫它時沒有“當前物件”。
current object this instance variable variable!instance
由於類方法中沒有當前物件,因此使用 this 關鍵字是錯誤的。如果你嘗試這樣做,你可能會收到類似以下的錯誤訊息:“未定義變數:this”。此外,你不能在不使用點表示法並提供物件名稱的情況下引用例項變數。如果你嘗試這樣做,你可能會收到“無法對非靜態變數進行靜態引用……”的錯誤訊息。這不是最好的錯誤訊息之一,因為它使用了一些非標準語言。例如,它指的是“非靜態變數”,實際上是指“例項變數”。但一旦你明白了它的意思,你就知道它的意思了。
繼承
[edit | edit source]inheritance
與面向物件程式設計最常關聯的語言特性是繼承。繼承是指定義一個新類的能力,該新類是先前定義的類(包括內建類)的修改版本。
此特性的主要優點是,你可以在不修改現有類的情況下向現有類新增新的方法或例項變數。這對於內建類特別有用,因為即使你想要修改它們,你也無法修改它們。
繼承被稱為“繼承”的原因是,新類繼承了現有類中的所有例項變數和方法。擴充套件這個比喻,現有類有時被稱為父類。
可繪製矩形
[edit | edit source]Rectangle class!Rectangle drawable
作為繼承的一個例子,我們將採用現有的 Rectangle 類並使其“可繪製”。也就是說,我們將建立一個名為 DrawableRectangle 的新類,該類將擁有 Rectangle 的所有例項變數和方法,以及一個名為 draw 的附加方法,該方法將接收一個 Graphics 物件作為引數並繪製矩形。
類定義看起來像這樣
逐字 import java.awt.*;
class DrawableRectangle extends Rectangle
public void draw (Graphics g) g.drawRect (x, y, width, height);
逐字
是的,整個類定義實際上就這麼多。第一行匯入 java.awt 包,這是 Rectangle 和 Graphics 所在的地方。
AWT import statement!import
下一行表明 DrawableRectangle 繼承自 Rectangle。extends 關鍵字用於標識父類。
其餘部分是 draw 方法的定義,該方法引用了例項變數 x、y、width 和 height。引用那些沒有出現在此類定義中的例項變數可能看起來很奇怪,但請記住,它們是從父類繼承的。
要建立和繪製一個 DrawableRectangle,你可以使用以下程式碼
逐字
public static void draw
(Graphics g, int x, int y, int width, int height)
DrawableRectangle dr = new DrawableRectangle (); dr.x = 10; dr.y = 10; dr.width = 200; dr.height = 200; dr.draw (g);
逐字
draw 的引數是一個 Graphics 物件和繪圖區域的邊界框(而不是矩形的座標)。
對於一個沒有建構函式的類使用 new 命令可能看起來很奇怪。DrawableRectangle 繼承了其父類的預設建構函式,所以這裡沒有問題。
constructor
我們可以設定 dr 的例項變數並在其上呼叫方法,方法如常。當我們呼叫 draw 時,Java 會呼叫我們在 DrawableRectangle 中定義的方法。如果我們在 dr 上呼叫 grow 或其他一些 Rectangle 方法,Java 會知道使用父類中定義的方法。
class hierarchy Object parent class class!parent
在 Java 中,所有類都擴充套件自其他類。最基本的類稱為 Object。它不包含任何例項變數,但它確實提供了 equals 和 toString 方法等。
許多類擴充套件了 Object,包括我們編寫的大多數類以及許多內建類,如 Rectangle。任何沒有明確指定父類的類預設繼承自 Object。
然而,一些繼承鏈更長。例如,Slate 擴充套件了 Frame(見附錄 slate),Frame 擴充套件了 Window,Window 擴充套件了 Container,Container 擴充套件了 Component,Component 擴充套件了 Object。無論鏈條有多長,Object 都是所有類的最終父類。
Java 中的所有類都可以組織成一個稱為類層次結構的“家族樹”。Object 通常出現在頂部,所有“子”類都在下面。例如,如果您檢視 Frame 的文件,您將看到構成 Frame 家譜的那部分層次結構。
object-oriented design
繼承是一個強大的功能。一些在沒有繼承的情況下會很複雜的程式可以用它簡潔、簡單地編寫。此外,繼承可以促進程式碼重用,因為您可以自定義內建類的行為,而無需修改它們。
另一方面,繼承可能會使程式難以閱讀,因為有時不清楚在呼叫方法時在哪裡可以找到定義。例如,您可以對 Slate 呼叫的方法之一是 getBounds。您可以找到 getBounds 的文件嗎?事實證明,getBounds 在 Slate 的父類的父類的父類的父類中定義。
此外,許多可以使用繼承完成的事情可以用幾乎同樣優雅的方式(甚至更優雅)在沒有繼承的情況下完成。
描述
[物件方法:] 在物件上呼叫並對該物件進行操作的方法,該物件在 Java 中由關鍵字 this 或英文中的“當前物件”引用。物件方法沒有關鍵字 static。
[類方法:] 帶有關鍵字 static 的方法。類方法不會在物件上呼叫,並且它們沒有當前物件。
[當前物件:] 呼叫物件方法的物件。在方法內部,當前物件由 this 引用。
[this:] 引用當前物件的關鍵字。
[隱式:] 任何未說出來或暗示的東西。在物件方法中,您可以隱式地(不命名物件)引用例項變數。
[顯式:] 任何完整拼寫出來的東西。在類方法中,對例項變數的所有引用都必須是顯式的。
object method class method current object this implicit explicit