跳轉到內容

Java/條件語句圖形遞迴之道

來自 Wikibooks,開放世界中的開放書籍

條件語句、圖形和遞迴

[編輯 | 編輯原始碼]

模運算子

[編輯 | 編輯原始碼]

模運算子作用於整數(和整數表示式),並返回第一個運算元除以第二個運算元的餘數。在 Java 中,模運算子是百分號 %。語法與其他運算子完全相同

 int quotient = 7 / 3;
 int remainder = 7 % 3;

第一個運算子,整數除法,返回 2。第二個運算子返回 1。因此,7 除以 3 等於 2,餘數為 1。

模運算子非常有用。例如,你可以檢查一個數字是否可以被另一個數字整除:如果 x%y 為零,那麼 x 可以被 y 整除。

此外,你可以使用模運算子從數字中提取最右邊的數字或數字。例如,x % 10 返回 x 最右邊的數字(以 10 為基數)。類似地,x % 100 返回最後兩位數字。

條件執行

[編輯 | 編輯原始碼]

為了編寫有用的程式,我們幾乎總是需要能夠檢查某些條件並相應地改變程式的行為。條件語句給了我們這種能力。最簡單的形式是 if 語句

 if (x > 0)
   System.out.println ("x is positive");

括號中的表示式稱為條件。如果為真,則執行方括號中的語句。如果條件不為真,則不會發生任何事情。

條件可以包含任何比較運算子,有時稱為關係運算符

 x == y               // x equals y
 x != y               // x is not equal to y
 x > y                // x is greater than y
 x < y                // x is less than y
 x >= y               // x is greater than or equal to y
 x <= y               // x is less than or equal to y

一個常見的錯誤是使用單個 = 代替雙等號 ==。請記住,= 是賦值運算子,而 == 是比較運算子。此外,沒有像 =< 或 => 這樣的東西。

條件運算子兩側必須是相同型別。你只能比較 int 與 int,double 與 double。不幸的是,在這一點上,你根本無法比較字串!有一種方法可以比較字串,但我們將在接下來的幾章中討論它。

選擇性執行

[編輯 | 編輯原始碼]

條件執行的第二種形式是選擇性執行,其中有兩個可能性,條件決定執行哪一個。語法如下

 if (x%2==0)
   System.out.println ("x is even");
 else
   System.out.println ("x is odd");

如果 x 除以 2 的餘數為零,那麼我們知道 x 是偶數,此程式碼會列印一條訊息以表明這一點。如果條件為假,則執行第二個 print 語句。由於條件必須為真或假,因此兩個選擇中只有一個將被執行。

順便說一下,如果你認為你可能經常需要檢查數字的奇偶性,你可能想將此程式碼包裝在一個方法中,如下所示

 public static void printParity (int x)
   if (x%2==0)
     System.out.println ("x is even");
    else
     System.out.println ("x is odd");

現在,你有一個名為 printParity 的方法,它將為任何你提供的整數列印一條適當的訊息。在 main 中,你將如下呼叫此方法

 printParity (17);

始終記住,當你呼叫一個方法時,你無需宣告提供引數的型別。Java 可以確定它們的型別。你應該抵制編寫如下內容的誘惑

 int number = 17;
 printParity (int number);         // output: "x is odd"

鏈式條件語句

[編輯 | 編輯原始碼]

有時,你希望檢查一系列相關的條件並選擇其中一項操作。一種方法是將一系列 ifs 和 elses 連線起來

 if (x > 0)
   System.out.println ("x is positive");
 else if (x < 0)
   System.out.println ("x is negative");
 else
   System.out.println ("x is zero");

這些鏈可以根據你的需要無限延伸,但如果它們變得過於複雜,則可能難以閱讀。一種使它們更容易閱讀的方法是使用標準縮排,如這些示例所示。如果你保持所有語句和波浪號對齊,你犯語法錯誤的可能性更小,並且在發生錯誤時更容易找到它們。

巢狀條件語句

[編輯 | 編輯原始碼]

除了連結之外,你還可以將一個條件語句巢狀在另一個條件語句中。我們可以將前面的示例寫成

 if (x == 0)
   System.out.println ("x is zero");
 else
   if (x > 0)
     System.out.println ("x is positive");
   else
     System.out.println ("x is negative");

現在有一個外部條件語句包含兩個分支。第一個分支包含一個簡單的 print 語句,但第二個分支包含另一個條件語句,它本身有兩個分支。幸運的是,這兩個分支都是 print 語句,雖然它們也可能都是條件語句。

再次注意,縮排有助於使結構清晰,但是,巢狀條件語句很快就會變得難以閱讀。通常情況下,最好避免它們。

另一方面,這種 **巢狀結構** 很常見,我們將在後面再次看到它,所以你最好習慣它。

return 語句

[編輯 | 編輯原始碼]

return 語句允許你在到達方法末尾之前終止方法的執行。使用它的一個原因是,如果你檢測到錯誤情況

 public static void printLogarithm (double x)
   if (x <= 0.0)
     System.out.println ("Positive numbers only, please.");
     return;
   
   double result = Math.log (x);
   System.out.println ("The log of x is " + result);

這定義了一個名為 printLogarithm 的方法,它接受一個名為 x 的 double 作為引數。它首先檢查 x 是否小於或等於零,如果是,則列印一條錯誤訊息,然後使用 return 退出方法。執行流程立即返回呼叫方,方法的其餘行不會被執行。

我在條件的右側使用了浮點數,因為左側有一個浮點數變數。

型別轉換

[編輯 | 編輯原始碼]

你可能想知道為什麼你可以使用類似“x 的對數是”+ result 這樣的表示式,因為其中一個運算元是字串,另一個是 double。好吧,在這種情況下,Java 正在代表我們執行智慧操作,在它執行字串連線之前,它會自動將 double 轉換為字串。

這種功能是設計程式語言時遇到的一個常見問題的一個例子,即形式主義與便利性之間存在衝突,形式主義要求形式語言應該具有簡單的規則,很少有例外,而便利性要求程式語言在實踐中易於使用。

大多數情況下,便利性獲勝,這對專家程式設計師來說通常是件好事(他們可以免受嚴格但笨拙的形式主義的困擾),但對初學者程式設計師來說是件壞事,他們經常對規則的複雜性和異常數量感到困惑。在這本書中,我試圖透過強調規則並省略許多異常來簡化問題。

但是,重要的是要知道,無論何時你嘗試“新增”兩個表示式,如果其中一個是字串,則 Java 將將另一個轉換為字串,然後執行字串連線。你認為將整數和浮點數進行運算會發生什麼?

畫板和圖形物件

[編輯 | 編輯原始碼]

為了在螢幕上繪製圖形,你需要兩個物件,一個畫板和一個圖形物件。

  • 畫板:畫板是一個視窗,它包含一個可以繪製圖形的空白矩形。Slate 類不是標準 Java 庫的一部分;它是我為本課程編寫的。
  • 圖形:圖形物件是我們用來繪製線條、圓圈等的物件。它是 Java 庫的一部分,因此它的文件位於 Sun 網站上。

與 Graphics 物件相關的 方法定義在內建的 Graphics 類中。與 Slates 相關的 方法定義在 Slate 類中,如附錄 slate 中所示。

Slate 類中的主要 方法是 makeSlate,它基本上做你預期的事情。它建立一個新的視窗,並返回一個 Slate 物件,你可以用它在程式的後面部分引用這個視窗。你可以在一個程式中建立多個 Slate。

 Slate slate = Slate.makeSlate (500, 500);

makeSlate 接受兩個引數,視窗的寬度和高度。因為它屬於另一個類,所以我們必須使用 *點符號* 指定類的名稱。

返回值被分配給一個名為 slate 的變數。類名(大寫 *S*)和變數名(小寫 *s*)之間沒有衝突。

我們需要的下一個 方法是 getGraphics,它接受一個 Slate 物件,並建立一個 Graphics 物件,可以在上面進行繪製。你可以將 Graphics 物件看作一塊粉筆。

 Graphics g = Slate.getGraphics (slate);

使用名稱 g 是慣例,但我們可以將其命名為任何東西。

在 Graphics 物件上呼叫 方法

[編輯 | 編輯原始碼]

為了在螢幕上繪製東西,你需要在 graphics 物件上呼叫 方法。我們已經呼叫了大量的 方法,但這是我們第一次 *在物件上呼叫 方法*。語法類似於從另一個類呼叫 方法

 g.setColor (Color.black);
 g.drawOval (x, y, width, height);

物件的名稱出現在點號之前;方法的名稱出現在點號之後,後面跟著該方法的引數。在本例中,該方法接受一個引數,它是一個顏色。

setColor 更改當前顏色,在本例中為黑色。所有被繪製的東西都將是黑色的,直到我們再次使用 setColor。

Color.black 是 Color 類提供的一個特殊值,就像 Math.PI 是 Math 類提供的一個特殊值一樣。Color 提供了其他顏色的調色盤,包括

black     blue    cyan   darkGray   gray   lightGray
magenta	  orange  pink   red        white  yellow

為了在 Slate 上進行繪製,我們可以呼叫 Graphics 物件上的 draw 方法。例如

 g.drawOval (x, y, width, height);

drawOval 接受四個整數作為引數。這些引數指定一個邊界框,即橢圓將被繪製在其中的矩形(如圖所示)。邊界框本身不會被繪製;只有橢圓會被繪製。邊界框就像一個指南。邊界框總是水平或垂直方向;它們從不處於奇怪的角度。

邊界框

[編輯 | 編輯原始碼]

如果你仔細想想,有很多方法可以指定矩形的位置和大小。你可以給出中心或任何角點的位置,以及高度和寬度。或者,你可以給出相對角點的位置。選擇是任意的,但在任何情況下,它都需要相同數量的引數:四個。

按照慣例,指定邊界框的常用方法是給出左上角的位置以及寬度和高度。指定位置的常用方法是使用座標系。

你可能熟悉二維笛卡爾座標,其中每個位置由一個 x 座標(沿 x 軸的距離)和一個 y 座標標識。按照慣例,笛卡爾座標向右和向上增加,如圖所示。

令人討厭的是,計算機圖形系統通常使用笛卡爾座標的變體,其中原點位於螢幕或視窗的左上角,正 y 軸的方向向下。Java 遵循此慣例。

度量單位稱為畫素;典型的螢幕大約有 1000 個畫素寬。座標始終是整數。如果你想將浮點數用作座標,你必須將其四捨五入為整數(參見四捨五入部分)。


一個蹩腳的米老鼠

[編輯 | 編輯原始碼]

假設我們想要繪製米老鼠的圖片。我們可以使用我們剛剛繪製的橢圓作為臉,然後新增耳朵。在這樣做之前,最好將程式分解為兩個 方法。main 將建立 Slate 和 Graphics 物件,然後呼叫 draw,draw 完成實際的繪製工作。

 public static void main (String[] args)
   int width = 500;
   int height = 500;
   
   Slate slate = Slate.makeSlate (width, height);
   Graphics g = Slate.getGraphics (slate);
   
   g.setColor (Color.black);
   draw (g, 0, 0, width, height);
   
 public static void draw (Graphics g, int x, int y, int width, int height)
   g.drawOval (x, y, width, height);
   g.drawOval (x, y, width/2, height/2);
   g.drawOval (x+width/2, y, width/2, height/2);

draw 的引數是 Graphics 物件和一個邊界框。draw 呼叫 drawOval 三次,以繪製米老鼠的臉和兩個耳朵。下圖顯示了耳朵的邊界框。

 /-\ /-\
|  | |  |
 \ / \ /
  /---\
 |     |
  \___/

如圖所示,左耳邊界框左上角的座標為 (x, y)。右耳的座標為 (x+width/2, y)。在這兩種情況下,耳朵的寬度和高度都是原始邊界框寬度和高度的一半。

請注意,耳盒的座標都是相對於原始邊界框的位置 (x 和 y) 和大小 (寬度和高度) 的。因此,我們可以使用 draw 在螢幕上的任何位置以任何大小繪製米老鼠(儘管很蹩腳)。作為練習,修改傳遞給 draw 的引數,使米老鼠的高度和寬度為螢幕的一半,並且居中。

其他繪圖命令

[編輯 | 編輯原始碼]

原型

drawLine
drawRect
fillOval
fillRect
prototype
interface

drawRect, drawLine

[編輯 | 編輯原始碼]

另一個與 drawOval 引數相同的繪圖命令是

 drawRect (int x, int y, int width, int height)

在這裡,我使用了一種標準格式來記錄 方法的名稱和引數。這些資訊有時被稱為 方法的介面或原型。檢視此原型,你可以判斷引數的型別以及(基於它們的名稱)推斷它們的功能。以下另一個示例

 drawLine (int x1, int y1, int x2, int y2)

使用引數名稱 x1、x2、y1 和 y2 意味著 drawLine 從點 (x1, y1) 到點 (x2, y2) 繪製一條線。

你可能想嘗試的另一個命令是

 drawRoundRect (int x, int y, int width, int height,
	        int arcWidth, int arcHeight)

前四個引數指定矩形的邊界框;其餘兩個引數指示角點應圓角的程度,指定角點圓弧的水平和垂直直徑。

fillRect, fillOval

[編輯 | 編輯原始碼]

這些命令還有 *填充* 版本,它們不僅繪製形狀的輪廓,而且還會填充它。介面是相同的;只有名稱被更改了

 fillOval (int x, int y, int width, int height)
 fillRect (int x, int y, int width, int height)
 fillRoundRect (int x, int y, int width, int height,
	        int arcWidth, int arcHeight)

沒有 fillLine 這樣的東西——它根本沒有意義,因為直線是一維的。

我在上一章中提到過,一個 方法呼叫另一個 方法是合法的,我們已經看到了幾個這樣的例子。我忘記提的是,一個 方法呼叫自身也是合法的。可能不明顯為什麼這是一件好事,但事實證明,這是程式可以做到的最神奇和最有趣的事情之一。

For example, look at the following method:
 public static void countdown (int n)
   if (n == 0)
     System.out.println ("Blastoff!");
   else
     System.out.println (n);
     countdown (n-1);

該方法名為 countdown,它接受一個整數作為引數。如果引數為零,它將列印單詞Blastoff。否則,它將列印該數字,然後呼叫名為 countdown 的方法(自身),並將 n-1 作為引數傳遞。

如果我們在 main 中像這樣呼叫此方法,會發生什麼?

 countdown (3);
  1. countdown 的執行從 n=3 開始,由於 n 不為零,它列印值 3,然後呼叫自身,並將 3-1 傳遞...
  2. countdown 的執行從 n=2 開始,由於 n 不為零,它列印值 2,然後呼叫自身,並將 2-1 傳遞...
  3. countdown 的執行從 n=1 開始,由於 n 不為零,它列印值 1,然後呼叫自身,並將 1-1 傳遞...
  4. countdown 的執行從 n=0 開始,由於 n 為零,它列印單詞Blastoff!然後返回。
  5. 獲得 n=1 的 countdown 返回。
  6. 獲得 n=2 的 countdown 返回。
  7. 獲得 n=3 的 countdown 返回。

然後你回到 main(多麼奇妙的旅程)。因此,總的輸出看起來像這樣

3
2
1
Blastoff!

作為第二個例子,讓我們再看看 newLine 和 threeLine 方法。

 public static void newLine ()
   System.out.println ("");
 
 public static void threeLine ()
   newLine ();  newLine ();  newLine ();

逐字

雖然這些方法有效,但如果我想列印 2 個或 106 個換行符,它們將幫不上忙。更好的選擇是

 public static void nLines (int n)
   if (n > 0)
     System.out.println ("");
     nLines (n-1);

這個程式非常相似;只要 n 大於零,它就列印一個換行符,然後呼叫自身以列印 n-1 個額外的換行符。因此,列印的換行符總數為 1 + (n-1),通常約為 n。

方法呼叫自身的過程稱為遞迴,這樣的方法被稱為遞迴方法。

遞迴方法的堆疊圖

[edit | edit source]

在上一章中,我們使用堆疊圖來表示方法呼叫期間程式的狀態。相同型別的圖表可以更容易地解釋遞迴方法。

請記住,每次呼叫方法時,它都會建立一個新的方法例項,該例項包含方法的區域性變數和引數的新版本。

有一個 main 例項和四個 countdown 例項,每個例項都有不同的引數 n 值。堆疊的底部,n=0 的 countdown 是基本情況。它不會進行遞迴呼叫,因此沒有更多的 countdown 例項。

main 的例項是空的,因為 main 沒有引數或區域性變數。作為練習,為 nLines 繪製一個堆疊圖,呼叫引數 n=4。


約定和神聖法則

[edit | edit source]

在過去的幾個部分中,我多次使用短語按約定來表示設計決策,這些決策在某種意義上是任意的,因為沒有明顯的理由用一種方式而不是另一種方式,而是由約定決定的。

在這些情況下,熟悉約定並使用它對你來說是有利的,因為它將使你的程式更容易被其他人理解。同時,區分(至少)三種類型的規則很重要

  • 神聖法則 這是我用來表示由於邏輯或數學的某些基本原理而成立的規則,並且在任何程式語言(或其他形式系統)中都成立。例如,不可能用少於四條資訊來指定邊界框的位置和大小。另一個例子是整數加法是可交換的。這是加法定義的一部分,與 Java 無關。
  • Java 規則 這些是 Java 的語法和語義規則,你不能違反它們,因為生成的程式將無法編譯或執行。有些是任意的;例如,+ 符號代表加法和字串連線這一事實。其他規則反映了編譯或執行過程的底層限制。例如,你必須指定引數的型別,但不是引數。
  • 樣式和約定 很多規則沒有被編譯器強制執行,但對於編寫正確、可除錯和可修改以及其他人可以閱讀的程式來說至關重要。例如,縮排和花括號的位置,以及命名變數、方法和類的約定。

隨著我們的學習,我將嘗試指出各種事物屬於哪一類,但你可能想不時地思考一下。

雖然我正在談論這個話題,你可能已經發現,類的名稱總是以大寫字母開頭,但變數和方法以小寫字母開頭。如果名稱包含多個單詞,你通常將每個單詞的第一個字母大寫,例如 newLine 和 printParity。這些規則屬於哪一類?

詞彙表

[edit | edit source]
  • 模數 對整數起作用的運算子,當一個數字除以另一個數字時產生餘數。在 Java 中,它用百分號()表示。
  • 條件語句 一塊語句,這些語句是否執行取決於某個條件。
  • 鏈式 一種將多個條件語句按順序連線起來的方法。
  • 巢狀 將一個條件語句放在另一個條件語句的一個或兩個分支內。
  • 座標 指定二維圖形視窗中位置的變數或值。
  • 畫素 測量座標的單位。
  • 邊界框 指定矩形區域座標的常用方法。
  • 型別轉換 將一種型別轉換為另一種型別的運算子。在 Java 中,它顯示為括號中的型別名稱,如 (int)。
  • 介面 對方法所需引數及其型別的描述。
  • 原型 使用類似 Java 語法來描述方法介面的一種方法。
  • 遞迴 呼叫正在執行的相同方法的過程。
  • 無限遞迴 一種遞迴呼叫自身,但永遠不會到達基本情況的方法。通常的結果是 StackOverflowException。
  • 分形 一種遞迴定義的影像,因此影像的每個部分都是整體的較小版本。
華夏公益教科書