跳轉到內容

Java 之道/迭代

來自華夏公益教科書

多重賦值

[編輯 | 編輯原始碼]

我還沒有對此多說,但在 Java 中,對同一個變數進行多次賦值是合法的。第二次賦值的效果是用新值替換變數的舊值。

 int fred = 5;
 System.out.print (fred);
 fred = 7;
 System.out.println (fred);

該程式的輸出為 57,因為第一次列印 fred 時,其值為 5,而第二次列印時其值為 7。

這種多重賦值是我將變數描述為值容器的原因。當您將值分配給變數時,您會更改容器的內容。

當對一個變數進行多次賦值時,尤其要注意區分賦值語句和等式語句。由於 Java 使用 = 符號進行賦值,因此很容易將像 a = b 這樣的語句解釋為等式語句。 (a==b) 是一個等式語句!

首先,等式是可交換的,而賦值不是。例如,在數學中,如果 a = bb = a。但在 Java 中,a = 7 是一個合法的賦值語句,而 7 = a;則不是。

此外,在數學中,等式語句始終為真。如果現在 a==b,那麼 a 將永遠等於 b。然而,在計算機程式中,變數本質上是不一致的。在 Java 中,賦值語句可以使兩個變數相等,但它們不必保持相等!

 int a = 5;
 int b = a;     // a and b are now equal
 a = 3;         // a and b are no longer equal

第三行更改了 a 的值,但沒有更改 b 的值,因此它們不再相等。在許多程式語言中,使用另一種符號進行賦值,例如 <- 或 :=,以避免這種混淆。

雖然多重賦值經常很有用,但您應該謹慎使用它。如果變數的值在程式的不同部分不斷變化,它會使程式碼難以閱讀和除錯。

計算機經常被用來做的事情之一就是自動執行重複的任務。重複相同或類似的任務而不犯錯誤是計算機擅長的,而人類則不擅長。

我們已經看到了使用遞迴來執行重複的程式,例如 nLines 和 countdown。這種型別的重複稱為迭代,Java 提供了一些語言特性,可以更輕鬆地編寫迭代程式。

我們將要介紹的兩個特性是 while 語句和 for 語句。

while 語句

[編輯 | 編輯原始碼]

使用 while 語句,我們可以重寫 countdown

 public static void countdown (int n)
 {
   while (n > 0)
   {
     System.out.println (n);
     n = n - 1;
   }
   System.out.println ("Blastoff!");
 }

您幾乎可以像讀英語一樣閱讀 while 語句。這意味著,當 n 大於零時,繼續列印 n 的值,然後將 n 的值減 1。當您得到零時,列印“Blastoff!”

更正式地說,while 語句的執行流程如下

  1. 計算括號中的條件,得到真或假。
  2. 如果條件為假,則退出 while 語句,並在下一條語句處繼續執行。
  3. 如果條件為真,則執行方括號之間的每個語句,然後返回步驟 1。

這種型別的流程稱為迴圈,因為第三步迴圈回到頂部。請注意,如果條件在第一次遍歷迴圈時為假,則迴圈內的語句將永遠不會執行。迴圈內的語句有時稱為迴圈體。

迴圈體應該更改一個或多個變數的值,以便最終條件變為假,迴圈終止。否則,迴圈將永遠重複,這稱為無限迴圈。計算機科學家永遠樂此不疲的一個觀察結果是,洗髮水上的說明“打泡、沖洗、重複”是一個無限迴圈。

對於 countdown,我們可以證明迴圈將終止,因為我們知道 n 的值是有限的,並且我們可以看到 n 的值在每次遍歷迴圈(每次迭代)時都會變小,因此最終我們必須得到零。在其他情況下,則不那麼容易判斷

 public static void sequence (int n)
 {
   while (n != 1)
   {
     System.out.println (n);
     if (n % 2 == 0)
     {
       n = n / 2;
     }
     else                   // n is odd
     {
       n = n * 3 + 1;
     }
   }
 }

該迴圈的條件為 n != 1,因此迴圈將持續進行,直到 n 為 1,這將使條件變為假。

在每次迭代中,程式列印 n 的值,然後檢查它是偶數還是奇數。如果它是偶數,則將 n 的值除以 2。如果它是奇數,則將該值替換為 n * 3 + 1。例如,如果起始值(傳遞給 sequence 的引數)為 3,則生成的序列為:3、10、5、16、8、4、2、1。

由於 n 有時會增加,有時會減少,因此沒有明顯的證明表明 n 會最終達到 1,或者程式會終止。對於 n 的某些特定值,我們可以證明終止。例如,如果起始值為 2 的冪,則 n 的值在每次遍歷迴圈時都將為偶數,直到我們得到 1。前面的示例以這樣的序列結束,從 16 開始。

撇開特定值不談,有趣的問題是我們是否可以證明該程式對所有 n 值都會終止。到目前為止,還沒有人能夠證明或反駁它!

迴圈擅長的一件事是生成和打印表格資料。例如,在計算機尚未普及之前,人們不得不手工計算對數、正弦和餘弦以及其他常見數學函式。

為了簡化操作,人們會使用包含大量表格的書籍,您可以在其中找到各種函式的值。建立這些表格既緩慢又無聊,結果往往充滿了錯誤。

當計算機出現時,最初的反應之一是,這太好了!我們可以使用計算機生成表格,這樣就不會出現錯誤。事實證明這確實是正確的(大部分),但這目光短淺。此後不久,計算機(和計算器)變得無處不在,表格便變得過時了。

好吧,幾乎是過時了。事實證明,對於某些操作,計算機使用值表來獲得近似答案,然後執行計算以改進近似值。在某些情況下,基礎表格中存在錯誤,最著名的例子是原始英特爾奔騰用於執行浮點除法的表格。

雖然對數表不再像以前那麼有用,但它仍然是一個迭代的好例子。以下程式在左列列印一系列值,在右列列印其對數

 double x = 1.0;
 while (x < 10.0)
 {
   System.out.println (x + "   " + Math.log(x));
   x = x + 1.0;
 }

該程式的輸出為

1.0   0.0
2.0   0.6931471805599453
3.0   1.0986122886681098
4.0   1.3862943611198906
5.0   1.6094379124341003
6.0   1.791759469228055
7.0   1.9459101490553132
8.0   2.0794415416798357
9.0   2.1972245773362196

看看這些值,您能判斷出 log 函式預設使用的基數嗎?

由於 2 的冪在計算機科學中非常重要,因此我們通常希望找到以 2 為底的對數。為此,我們必須使用以下公式

_2 x = log_e x / log_e 2

將列印語句更改為

 System.out.println (x + "   " + Math.log(x) / Math.log(2.0));

得到

1.0   0.0
2.0   1.0
3.0   1.5849625007211563
4.0   2.0
5.0   2.321928094887362
6.0   2.584962500721156
7.0   2.807354922057604
8.0   3.0
9.0   3.1699250014423126

我們可以看到,1、2、4 和 8 是 2 的冪,因為它們以 2 為底的對數是整數。如果我們想找到其他 2 的冪的對數,我們可以這樣修改程式

 double x = 1.0;
 while (x < 100.0)
 {
   System.out.println (x + "   " + Math.log(x) / Math.log(2.0));
   x = x * 2.0;
 }

現在我們不是在每次遍歷迴圈時將某值加到 x 上,這會產生一個算術序列,而是將 x 乘以某值,這會產生一個幾何序列。結果是

1.0   0.0
2.0   1.0
4.0   2.0
8.0   3.0
16.0   4.0
32.0   5.0
64.0   6.0

對數表可能不再有用,但對於計算機科學家來說,瞭解 2 的冪非常重要!有空的時候,您應該記住 2 的冪,一直到 65536(即 2^16)。

二維表格

[編輯 | 編輯原始碼]

二維表格是一個表格,您可以在其中選擇一行和一列,然後讀取交點處的數值。乘法表就是一個很好的例子。假設您想列印 1 到 6 的乘法表。

一個好的開始方法是編寫一個簡單的迴圈,在同一行上列印 2 的倍數。

 int i = 1;
 while (i <= 6)
 {
   System.out.print (2 * i + "   ");
   i = i + 1;
 }
 System.out.println ("");

第一行初始化一個名為 i 的變數,它將充當迴圈計數器。隨著迴圈執行,i 的值從 1 增加到 6,然後當 i 為 7 時,迴圈終止。在每次遍歷迴圈時,我們列印 2*i 的值,後面跟三個空格。由於我們使用的是 print 命令而不是 println,因此所有輸出都出現在同一行上。

正如我在列印部分中提到的,在某些環境中,print 的輸出會儲存起來,直到呼叫 println 才會顯示。如果程式終止,而您忘記呼叫 println,則您可能永遠不會看到儲存的輸出。

該程式的輸出為

2   4   6   8   10   12

到目前為止,一切都很好。下一步是封裝和泛化。

封裝和泛化

[編輯 | 編輯原始碼]

封裝通常是指將一段程式碼包裝在一個方法中,這樣就可以利用方法的各種優勢。我們在第 alternative 節的 printParity 和第 boolean 節的 isSingleDigit 中看到了封裝的兩個例子。

泛化是指將一些特定的東西,比如列印 2 的倍數,變成更一般的,比如列印任何整數的倍數。

以下是一個封裝了上一節迴圈並泛化它以列印 n 的倍數的方法。

 public static void printMultiples (int n)
 {
   int i = 1;
   while (i <= 6)
   {
     System.out.print (n * i + "   ");
     i = i + 1;
   }
   System.out.println ();
 }

要進行封裝,我所要做的就是新增第一行,它聲明瞭方法名、引數和返回值型別。要進行泛化,我所要做的就是用引數 n 替換值 2。

如果我用引數 2 呼叫此方法,我會得到與之前相同的輸出。如果引數為 3,輸出將是

3   6   9   12   15   18

如果引數為 4,輸出將是

4   8   12   16   20   24

現在你可能已經猜到我們將如何列印乘法表:我們將用不同的引數重複呼叫 printMultiples。事實上,我們將使用另一個迴圈來遍歷行。

 int i = 1;
 while (i <= 6)
 {
   printMultiples (i);
   i = i + 1;
 }

首先,請注意這個迴圈與 printMultiples 中的迴圈有多麼相似。我所做的只是用方法呼叫替換了列印語句。

該程式的輸出為

1   2   3   4   5   6
2   4   6   8   10   12
3   6   9   12   15   18
4   8   12   16   20   24
5   10   15   20   25   30
6   12   18   24   30   36

這是一個(略顯混亂的)乘法表。如果你不喜歡這種混亂,Java 提供了一些方法,可以讓你更好地控制輸出的格式,但我在這裡就不再贅述了。

在上一節中,我提到了方法的各種優勢。現在你可能想知道這些優勢到底是什麼。以下是一些方法有用的原因

  • 透過給一系列語句命名,使你的程式更易於閱讀和除錯。
  • 將一個長程式分成多個方法,可以讓你分離程式的不同部分,獨立地除錯它們,然後將它們組合成一個整體。
  • 方法有利於遞迴和迭代。
  • 設計良好的方法通常對許多程式都有用。一旦你編寫並除錯了一個方法,你就可以重複使用它。

更多封裝

[編輯 | 編輯原始碼]

為了再次演示封裝,我將上一節的程式碼包裝在一個方法中

 public static void printMultTable ()
 {
   int i = 1;
   while (i <= 6)
   {
     printMultiples (i);
     i = i + 1;
   }
 }

我演示的過程是一個常見的開發計劃。你透過在 main 或其他地方新增程式碼行來逐步開發程式碼,然後在程式碼執行後,將其提取出來幷包裝在一個方法中。

這樣做的好處是,你開始編寫程式碼時,有時並不知道如何將程式分成方法。這種方法允許你邊設計邊開發。

區域性變數

[編輯 | 編輯原始碼]

現在你可能想知道我們如何在 printMultiples 和 printMultTable 中使用同一個變數 i。我之前不是說過你只能宣告一個變數一次嗎?難道在一個方法改變變數的值不會導致問題嗎?

這兩個問題的答案都是,因為 printMultiples 中的 i 和 printMultTable 中的 i 不是同一個變數。它們有相同的名稱,但它們不指向相同的儲存位置,改變其中一個的值不會影響另一個。

在方法定義中宣告的變數稱為區域性變數,因為它們是其所在方法的區域性變數。你不能從方法外部訪問區域性變數,並且你可以自由地擁有多個具有相同名稱的變數,只要它們不在同一個方法中即可。

在不同的方法中使用不同的變數名稱通常是一個好主意,以避免混淆,但也有很好的理由重複使用名稱。例如,通常使用 i、j 和 k 作為迴圈變數。如果你僅僅因為在其他地方使用過它們而避免在一個方法中使用它們,你可能會使程式更難閱讀。

更多泛化

[編輯 | 編輯原始碼]

作為泛化的另一個例子,想象一下,你想要一個程式,它可以列印任意大小的乘法表,而不僅僅是 6x6 的表。你可以給 printMultTable 新增一個引數

 public static void printMultTable (int high)
 {
   int i = 1;
   while (i <= high)
   {
     printMultiples (i);
     i = i + 1;
   }
 }

我用引數 high 替換了值 6。如果我用引數 7 呼叫 printMultTable,我會得到

1   2   3   4   5   6
2   4   6   8   10   12
3   6   9   12   15   18
4   8   12   16   20   24
5   10   15   20   25   30
6   12   18   24   30   36
7   14   21   28   35   42

這樣就可以了,但我可能希望表格是正方形的(行和列的數量相同),這意味著我必須給 printMultiples 新增另一個引數,以指定表格應該有多少列。

為了惹你生氣,我也將這個引數命名為 high,這說明不同的方法可以具有相同名稱的引數(就像區域性變數一樣)

 public static void printMultiples (int n, int high)
 {
   int i = 1;
   while (i <= high)
   {
     System.out.print (n*i + "   ");
     i = i + 1;
   }
   newLine ();
 }

 public static void printMultTable (int high)
 {
   int i = 1;
   while (i <= high)
   {
     printMultiples (i, high);
     i = i + 1;
   }
 }
<syntaxhighlight lang="java">

Notice that when I added a new parameter, I had to change the first line of the method (the interface or prototype), and I also had to change the place where the method is invoked in printMultTable. As expected, this program generates a square 7x7 table:
 1   2   3   4   5   6   7
 2   4   6   8   10   12   14
 3   6   9   12   15   18   21
 4   8   12   16   20   24   28
 5   10   15   20   25   30   35
 6   12   18   24   30   36   42
 7   14   21   28   35   42   49

When you generalize a method appropriately, you often find that the resulting program has capabilities you did not intend. For example, you might notice that the multiplication table is symmetric, because n*i==i*n, so all the entries in the table appear twice. You could save ink by printing only half the table. To do that, you only have to change one line of printMultTable. Change:
<syntaxhighlight lang="java">
 printMultiples (i, high);
<syntaxhighlight>

to:
<syntaxhighlight lang="java">
 printMultiples (i, i);

然後你會得到

1
2   4
3   6   9
4   8   12   16
5   10   15   20   25
6   12   18   24   30   36
7   14   21   28   35   42   49

我會讓你自己弄清楚它是如何工作的。

術語表

[編輯 | 編輯原始碼]
  • 迴圈 一條語句,它會重複執行,直到滿足某個條件。
  • 無限迴圈 條件始終為真的迴圈。
  • 主體 迴圈內的語句。
  • 迭代 迴圈主體的一次遍歷(執行),包括條件的評估。
  • 封裝 將一個大型複雜程式劃分為元件(如方法),並隔離這些元件(例如,使用區域性變數)。
  • 區域性變數 在方法內部宣告的變數,並且只在該方法記憶體在。區域性變數不能從其所在方法的外部訪問,也不會干擾任何其他方法。
  • 泛化 用適當的一般性東西(如變數或引數)替換不必要的特定性(如常量值)。泛化使程式碼更靈活,更易於重用,有時甚至更易於編寫。
  • 開發計劃 開發程式的過程。在本章中,我演示了一種基於開發簡單、特定功能的程式碼,然後對其進行封裝和泛化的開發風格。在距離節中,我演示了一種稱為增量開發的技術。在後面的章節中,我會介紹其他開發風格。
華夏公益教科書