Java 之道/結果方法
我們使用的一些內建方法,例如 Math 函式,已經產生了結果。也就是說,呼叫方法的效果是生成一個新值,我們通常將其分配給一個變數或用作表示式的部分。例如
double e = Math.exp (1.0);
double height = radius * Math.sin (angle);
但到目前為止,我們編寫的所有方法都是 void 方法;也就是說,不返回值的方法。當呼叫 void 方法時,它通常單獨在一行,沒有賦值
nLines (3);
g.drawOval (0, 0, width, height);
在本章中,我們將編寫返回值的方法,我將稱之為結果方法,因為沒有更好的名字。第一個例子是 area,它接受一個 double 作為引數,並返回半徑為給定值的圓的面積
public static double area (double radius)
double area = Math.PI * radius * radius;
return area;
你應該注意到的第一件事是方法定義的開頭不同。我們看到的是 public static double,而不是 public static void,它表示 void 方法,它表示此方法的返回值將具有 double 型別。我還沒有解釋 public static 的含義,但請耐心等待。
另外,請注意,最後一行是 return 語句的另一種形式,它包含一個返回值。該語句表示,立即從該方法返回,並使用以下表達式作為返回值。你提供的表示式可以任意複雜,因此我們可以更簡潔地編寫該方法
public static double area (double radius)
return Math.PI * radius * radius;
另一方面,像 area 這樣的臨時變數通常可以使除錯更容易。無論哪種情況,return 語句中表達式的型別都必須與方法的返回型別匹配。換句話說,當你宣告返回型別為 double 時,你承諾該方法最終將生成一個 double。如果你嘗試不帶表示式返回,或者表示式型別錯誤,編譯器會提醒你。
有時在條件的每個分支中使用多個 return 語句是有用的
public static double absoluteValue (double x)
if (x < 0)
return -x;
else
return x;
由於這些 return 語句在備用條件中,因此只執行其中一個。雖然在方法中使用多個 return 語句是合法的,但你應該記住,一旦執行其中一個,方法就會終止,不再執行任何後續語句。
在 return 語句之後出現的程式碼,或者任何其他無法執行的地方,都稱為死程式碼。一些編譯器會在你的程式碼中出現死程式碼時提醒你。
如果你在條件中放置 return 語句,那麼你必須保證程式的每條可能路徑都遇到 return 語句。例如
public static double absoluteValue (double x)
if (x < 0)
return -x;
else if (x > 0)
return x; // WRONG!!
此程式是非法的,因為如果 x 恰好為 0,那麼這兩個條件都不會為真,方法將結束而不會遇到 return 語句。典型的編譯器訊息將是 absoluteValue 中需要 return 語句,這在已經存在兩個 return 語句的情況下是一個令人困惑的訊息。
此時,你應該能夠檢視完整的 Java 方法並判斷它們的作用。但可能還不清楚如何編寫它們。我將建議一種我稱為增量開發的技術。
例如,假設你要找到兩點之間的距離,由座標給出。根據通常的定義
distance = sqrt((x_2 - x_1)^2 + (y_2 - y_1)^2)
第一步是考慮距離方法在 Java 中應該是什麼樣子。換句話說,輸入(引數)是什麼,輸出(返回值)是什麼。
在這種情況下,兩點是引數,用四個 double 表示它們是自然的,儘管我們稍後會看到 Java 中有一個 Point 物件,我們可以使用它。返回值是距離,它將具有 double 型別。
我們已經可以編寫該方法的概要
public static double distance
(double x1, double y1, double x2, double y2)
return 0.0;
語句 return 0.0; 是一個佔位符,對於編譯程式是必需的。顯然,在這個階段,程式沒有做任何有用的事情,但編譯它是值得的,這樣我們就可以在使程式更復雜之前識別任何語法錯誤。
為了測試新方法,我們必須用示例值呼叫它。在 main 中的某個地方,我會新增
double dist = distance (1.0, 2.0, 4.0, 6.0);
我選擇這些值是為了使水平距離為 3,垂直距離為 4;這樣,結果將為 5(3-4-5 三角形的斜邊)。當你測試方法時,知道正確答案是有用的。
一旦我們檢查了方法定義的語法,我們就可以開始逐行新增程式碼。在每次增量更改之後,我們重新編譯並執行程式。這樣,在任何時候,我們都知道錯誤一定在最後新增的那一行。
計算的下一步是找到 x_1 和 x_2 之間的差,以及 y_1 和 y_2 之間的差。我將把這些值儲存在名為 dx 和 dy 的臨時變數中。
public static double distance
(double x1, double y1, double x2, double y2)
double dx = x2 - x1;
double dy = y2 - y1;
System.out.println ("dx is " + dx);
System.out.println ("dy is " + dy);
return 0.0;
我添加了列印語句,它們將讓我在繼續之前檢查中間值。正如我提到的,我已經知道它們應該分別是 3.0 和 4.0。
當方法完成時,我將刪除列印語句。這樣的程式碼稱為腳手架,因為它有助於構建程式,但不是最終產品的一部分。有時最好保留腳手架,但將其註釋掉,以防以後需要。
開發的下一步是對 dx 和 dy 求平方。我們可以使用 Math.pow 方法,但簡單快捷的方法是將每一項乘以自身。
public static double distance
(double x1, double y1, double x2, double y2)
double dx = x2 - x1;
double dy = y2 - y1;
double dsquared = dx*dx + dy*dy;
System.out.println ("dsquared is " + dsquared);
return 0.0;
同樣,我將在此時編譯並執行程式,並檢查中間值(應該為 25.0)。
最後,我們可以使用 Math.sqrt 方法來計算並返回結果。
public static double distance
(double x1, double y1, double x2, double y2)
double dx = x2 - x1;
double dy = y2 - y1;
double dsquared = dx*dx + dy*dy;
double result = Math.sqrt (dsquared);
return result;
然後在 main 中,我們應該列印並檢查結果的值。
隨著你獲得更多程式設計經驗,你可能會發現自己一次編寫和除錯不止一行程式碼。然而,這種增量開發過程可以為你節省很多除錯時間。
該過程的關鍵方面是
- 從一個可執行的程式開始,進行小的增量更改。在任何時候,如果出現錯誤,你都會知道錯誤的確切位置。
- 使用臨時變數來儲存中間值,以便你可以列印和檢查它們。
- 程式執行後,你可能想要刪除一些腳手架,或者將多個語句合併到複合表示式中,但前提是不會使程式難以閱讀。
正如你所料,一旦你定義了一個新方法,你就可以將其用作表示式的部分,並且可以使用現有方法構建新方法。例如,如果有人給你兩個點,圓心和圓周上的一個點,並要求你求出圓的面積怎麼辦?
假設圓心座標儲存在變數 xc 和 yc 中,圓周上的一個點座標儲存在 xp 和 yp 中。第一步是找到圓的半徑,即兩點之間的距離。幸運的是,我們有一個名為 distance 的方法可以做到這一點。
double radius = distance (xc, yc, xp, yp);
第二步是根據半徑計算圓的面積並返回。
double area = area (radius);
return area;
將所有步驟封裝到一個方法中,我們得到以下程式碼:
public static double fred
(double xc, double yc, double xp, double yp)
double radius = distance (xc, yc, xp, yp);
double area = area (radius);
return area;
這個方法的名字是 fred,可能看起來很奇怪。我會在下節解釋原因。
臨時變數 radius 和 area 對開發和除錯很有用,但一旦程式正常工作,我們可以透過組合方法呼叫使它更簡潔:
public static double fred (double xc, double yc, double xp, double yp)
return area (distance (xc, yc, xp, yp));
方法過載
[edit | edit source]在上一節中,你可能已經注意到 fred 和 area 執行了類似的功能——計算圓的面積——但接受不同的引數。對於 area,我們必須提供半徑;對於 fred,我們提供兩個點座標。
如果兩個方法做相同的事情,給它們相同的名稱是自然而然的。換句話說,如果 fred 被稱為 area 會更有意義。
在 Java 中,使用相同名稱的多個方法,稱為方法過載,是合法的,只要每個版本接受不同的引數。因此,我們可以繼續重新命名 fred:
public static double area (double x1, double y1, double x2, double y2)
return area (distance (xc, yc, xp, yp));
當你呼叫過載方法時,Java 會根據你提供的引數來確定你要呼叫哪個版本。如果你寫:
double x = area (3.0);
Java 會查詢名為 area 的方法,該方法接受一個 double 型別的引數,因此它會使用第一個版本,並將引數解釋為半徑。如果你寫:
double x = area (1.0, 2.0, 4.0, 6.0);
Java 會使用 area 的第二個版本。更令人驚歎的是,area 的第二個版本實際上呼叫了第一個版本。
許多內建的 Java 命令都是過載的,這意味著它們有不同的版本,接受不同數量或型別的引數。例如,print 和 println 有接受任何型別單個引數的版本。在 Math 類中,abs 有一個針對 double 的版本,還有一個針對 int 的版本。
儘管方法過載是一個有用的功能,但應該謹慎使用。如果你試圖除錯一個方法版本,而意外地呼叫了另一個版本,你可能會讓自己陷入混亂。
實際上,這提醒了我除錯的黃金法則之一:確保你正在檢視的程式版本是正在執行的程式版本!有時你可能會發現自己對程式進行一個接一個的更改,但每次執行時都看到相同的結果。這是一個警告訊號,表明由於某種原因,你沒有執行你認為你正在執行的程式版本。要檢查,請插入一個 print 語句(列印的內容無關緊要),並確保程式的行為相應改變。
布林表示式
[edit | edit source]我們已經看到的大多數操作產生的結果與其運算元型別相同。例如,+ 運算子接受兩個 int 併產生一個 int,或接受兩個 double 併產生一個 double,等等。
關係運算符
[edit | edit source]我們遇到的例外是關係運算符,它們比較 int 和 float,並返回 true 或 false。true 和 false 是 Java 中的特殊值,它們共同構成一種稱為布林型別的型別。你可能還記得,當我定義一種型別時,我說它是一組值。對於 int、double 和 String 來說,這些集合非常大。對於布林值來說,就不是那麼大了。
布林表示式和變數的工作方式與其他型別的表示式和變數相同:
boolean fred;
fred = true;
boolean testResult = false;
第一個示例是一個簡單的變數宣告;第二個示例是一個賦值,第三個示例是宣告和賦值的組合,有時稱為初始化。true 和 false 值是 Java 中的關鍵字,因此它們可能顯示為不同的顏色,具體取決於你的開發環境。
初始化
[edit | edit source]正如我提到的,條件運算子的結果是一個布林值,因此你可以將比較的結果儲存在一個變數中:
boolean evenFlag = (n
boolean positiveFlag = (x > 0); // true if x is positive
然後在以後將其用作條件語句的一部分:
if (evenFlag)
System.out.println ("n was even when I checked it");
以這種方式使用的變數通常稱為標誌,因為它標誌著某個條件的存在或不存在。
邏輯運算子
[edit | edit source]Java 中有三個邏輯運算子:AND、OR 和 NOT,分別用符號 &&、|| 和 ! 表示。這些運算子的語義(含義)與其在英語中的含義相似。例如,(x > 0) && (x < 10) 僅當 x 大於零 AND 小於 10 時才為真。
語義
[edit | edit source]evenFlag n3 == 0 為真,如果其中任何一個條件為真,即如果 evenFlag 為真 OR 該數字能被 3 整除。
最後,NOT 運算子的作用是對布林表示式取反或反轉,因此 !evenFlag 為真,如果 evenFlag 為假——如果該數字為奇數。
巢狀結構
[edit | edit source]邏輯運算子通常提供一種簡化巢狀條件語句的方法。例如,如何使用單個條件編寫以下程式碼?
if (x > 0)
if (x < 10)
System.out.println ("x is a positive single digit.");
布林方法
[edit | edit source]方法可以像其他任何型別一樣返回布林值,這在將複雜測試隱藏在方法內部時通常很方便。例如:
public static boolean isSingleDigit (int x)
if (x >= 0 && x < 10)
return true;
else
return false;
這個方法的名字是 isSingleDigit。通常將布林方法命名為聽起來像是非問題的方法。返回值型別為 boolean,這意味著每個 return 語句都必須提供一個布林表示式。
程式碼本身很簡單,雖然它比需要的要長一些。請記住,表示式 x >= 0 && x < 10 的型別為布林值,因此直接返回它,並避免使用 if 語句是完全沒有問題的:
public static boolean isSingleDigit (int x)
return (x >= 0 && x < 10);
在 main 中,你可以像往常一樣呼叫此方法:
boolean bigFlag = !isSingleDigit (17);
System.out.println (isSingleDigit (2));
第一行僅當 17 不是一位數時,才將值 true 賦給 bigFlag。第二行列印 true,因為 2 是一個一位數。是的,println 也過載以處理布林值。
布林方法最常見的用途是在條件語句中:
if (isSingleDigit (x))
System.out.println ("x is little");
else
System.out.println ("x is big");
更多遞迴
[edit | edit source]現在我們有了返回值的函式,你可能會想知道,我們已經擁有了一個完整的程式語言,我的意思是,任何可以計算的東西都可以用這種語言表達。任何編寫過的程式都可以使用我們到目前為止使用的語言特性進行重寫(實際上,我們需要一些命令來控制鍵盤、滑鼠、磁碟、邪惡的機器人等等,但這只是所有)。
圖靈,艾倫
[edit | edit source]證明這一論點是一個非平凡的練習,首先由艾倫·圖靈完成,他是第一批計算機科學家之一(嗯,有些人會爭辯說他是數學家,但許多早期的計算機科學家都是從數學家開始的)。因此,它被稱為圖靈論題。如果你修了計算理論課程,你將有機會看到證明。
為了讓你瞭解到目前為止我們所學習的工具可以做什麼,讓我們來看一些評估遞迴定義的數學函式的方法。遞迴定義類似於迴圈定義,因為定義包含對所定義事物的引用。真正的迴圈定義通常沒有用
frabjuous:一個形容詞,用來描述一些很棒的東西。
如果你在字典裡看到了這個定義,你可能會感到厭煩。另一方面,如果你查了數學函式階乘的定義,你可能會得到類似這樣的東西
eqnarray* && 0! = 1 && n! = n (n-1)! eqnarray*
(階乘通常用符號表示,不要與 Java 邏輯運算子 ! 混淆,它表示 NOT。)這個定義說 0 的階乘是 1,任何其他值的階乘是乘以的階乘。所以是 3 乘以,是 2 乘以,是 1 乘以。把它們都加起來,我們得到等於 3 乘以 2 乘以 1 乘以 1,也就是 6。
如果你可以寫出某事物的遞迴定義,你通常可以寫一個 Java 程式來評估它。第一步是確定這個函式的引數是什麼,返回值型別是什麼。經過一番思考,你應該得出結論,階乘函式接受一個整數作為引數,並返回一個整數
public static int factorial (int n)
如果引數恰好為零,我們只需要返回 1
public static int factorial (int n)
if (n == 0)
return 1;
否則,這是有趣的部分,我們必須進行遞迴呼叫以找到的階乘,然後將其乘以。
public static int factorial (int n)
if (n == 0)
return 1;
else
int recurse = factorial (n-1);
int result = n * recurse;
return result;
如果我們檢視這個程式的執行流程,它類似於上一章中的 nLines。如果我們用值 3 呼叫階乘函式
過程
- 由於 3 不為零,我們採用第二個分支並計算的階乘...
- 由於 2 不為零,我們採用第二個分支並計算的階乘...
- 由於 1 不為零,我們採用第二個分支並計算的階乘...
- 由於 0 為零,我們採用第一個分支並立即返回 1,而無需進行任何進一步的遞迴呼叫。
- 返回值(1)乘以 n,即 1,並將結果返回。
- 返回值(1)乘以 n,即 2,並將結果返回。
- 返回值(2)乘以 n,即 3,並將結果 6 返回給 main 或任何呼叫了 factorial(3)的人。
注意,在階乘函式的最後一個例項中,區域性變數 recurse 和 result 不存在,因為當 n=0 時,建立它們的程式碼分支不會執行。
跟蹤執行流程是閱讀程式的一種方法,但正如你在上一節中看到的,它很快就會變得像迷宮一樣。另一種方法是我稱之為“信念的飛躍”。當你遇到一個方法呼叫時,與其跟蹤執行流程,不如假設該方法能正確工作並返回適當的值。
實際上,當你使用內建方法時,你已經實踐了這種信念的飛躍。當你呼叫 Math.cos 或 drawOval 時,你不會檢查這些方法的實現。你只是假設它們能正常工作,因為編寫內建類的人都是優秀的程式設計師。
同樣地,當你呼叫你自己的方法時,也是如此。例如,在布林值那一節中,我們編寫了一個名為 isSingleDigit 的方法,用於確定一個數字是否介於 0 到 9 之間。一旦我們透過測試和檢查程式碼,確信該方法是正確的,我們就可以使用該方法,而無需再檢視程式碼。
遞迴程式也是如此。當你遇到遞迴呼叫時,與其跟蹤執行流程,不如假設遞迴呼叫能正常工作(產生正確的結果),然後問問自己:假設我可以找到的階乘,我是否可以計算的階乘?在這種情況下,很明顯,你可以透過乘以來計算。
當然,在還沒有完成編寫程式的情況下就假設該方法能正常工作,這有點奇怪,但這正是它被稱為“信念的飛躍”的原因!
在前面的例子中,我使用了臨時變數來詳細說明步驟,並使程式碼更容易除錯,但我可以節省幾行程式碼
public static int factorial (int n)
if (n == 0)
return 1;
else
return n * factorial (n-1);
從現在起,我傾向於使用更簡潔的版本,但我建議你在開發程式碼時使用更明確的版本。當你完成程式碼時,如果你有靈感,可以把它壓縮一下。
在階乘函式之後,遞迴定義的經典數學函式是斐波那契數列,它有以下定義
eqnarray* && fibonacci(0) = 1 && fibonacci(1) = 1 && fibonacci(n) = fibonacci(n-1) + fibonacci(n-2); eqnarray*
翻譯成 Java 程式碼,就是
public static int fibonacci (int n)
if (n == 0 || n == 1)
return 1;
else
return fibonacci (n-1) + fibonacci (n-2);
如果你試圖跟蹤執行流程,即使對於相當小的 n 值,你的腦袋也會爆炸。但根據信念的飛躍,如果我們假設這兩個遞迴呼叫(是的,你可以進行兩次遞迴呼叫)能正常工作,那麼很明顯,我們可以透過將它們加在一起得到正確的結果。
- 返回值型別方法宣告中指示方法返回的值型別的那一部分。
- 返回值方法呼叫作為結果提供的值。
- 死程式碼程式中永遠不會執行的部分,通常是因為它出現在 return 語句之後。
- 腳手架程式開發期間使用的程式碼,但不是最終版本的一部分。
- void一種特殊的返回值型別,指示一個 void 方法;也就是說,一個不返回值的方法。
- 過載擁有多個具有相同名稱但引數不同的方法。當您呼叫過載方法時,Java 會透過檢視您提供的引數來確定使用哪個版本。
- 布林值一種型別的變數,只能包含兩個值 true 和 false。
- 標誌一個變數(通常是布林值),用於記錄條件或狀態資訊。
- 條件運算子一種運算子,用於比較兩個值,並生成一個布林值,指示運算元之間的關係。
- 邏輯運算子一種運算子,用於組合布林值並生成布林值。
- 初始化一條語句,同時宣告一個新變數併為其賦值。