天天看點

傳統 for 循環的函數式替代方案

盡管 for 循環包含許多可變部分,但許多開發人員仍非常熟悉它,并會不假思索地使用它。從 Java?? 8 開始,我們有多個強大的新方法可幫助簡化複雜疊代。在本文中,您将了解如何使用 IntStream 方法 range、iterate 和 limit 來疊代範圍和跳過範圍中的值。您還将了解新的 takeWhile 和 dropWhile 方法(即将在 Java 9 中引入)。

-----------------來自小馬哥的故事

for 循環的麻煩

在 Java 語言的第 1 個版本中就開始引入了傳統的 for 循環,它的更簡單的變體 for-each 是在 Java 5 中引入的。大部分開發人員更喜歡使用 for-each 執行日常疊代,但對于疊代一個範圍或跳過範圍中的值等操作,他們仍會使用 for。

or 循環非常強大,但它包含太多可變部分。甚至在列印 get set 提示的最簡單任務中,也可以看出這一點:

清單 1. 完成一個簡單任務的複雜代碼:

System.out.print("Get set...");
  for(int i = 1; i < 4; i++) {
    System.out.print(i + "...");
  }           

複制

在清單 1 中,我們從 1 開始循環處理索引變量 i,将它限制到小于 4 的值。請注意,for 循環需要我們告訴循環是遞增的。在本例中,我們還選擇了前遞增而不是後遞增。

清單 1 中沒有太多代碼,但比較繁瑣。Java 8 提供了一種更簡單、更優雅的替代方法:IntStream 的 range 方法。以下是列印清單 1 中的相同 get set 提示的 range方法:

清單 2. 完成一個簡單任務的簡單代碼:

System.out.print("Get set...");
  IntStream.range(1, 4)
    .forEach(i -> System.out.print(i + "..."));           

複制

在清單 2 中,我們看到并沒有顯著減少代碼量,但降低了它的複雜性。這樣做有兩個重要原因:

  1. 不同于 for,range 不會強迫我們初始化某個可變變量。
  2. 疊代會自動執行,是以我們不需要像循環索引一樣定義增量。

在語義上,最初的 for 循環中的變量 i 是一個可變變量。了解 range 和類似方法的價值對了解該設計的結果很有幫助。

可變變量與參數

for 循環中定義的變量 i 是單個變量,它會在每次對循環執行疊代時發生改變。range 示例中的變量 i 是Lambda表達式的參數,是以它在每次疊代中都是一個全新的變量。這是一個細微差別,但決定了兩種方法的不同。以下示例有助于闡明這一點。

清單 3 中的 for 循環想在一個内部類中使用索引變量:

清單 3. 在内部類中使用索引變量:

ExecutorService executorService = Executors.newFixedThreadPool(10);
 
      for(int i = 0; i < 5; i++) {
        int temp = i;
 
        executorService.submit(new Runnable() {
          public void run() {
            //If uncommented the next line will result in an error
            //System.out.println("Running task " + i); 
            //local variables referenced from an inner class must be final or effectively final
 
            System.out.println("Running task " + temp); 
          }
        });
      }
 
      executorService.shutdown();           

複制

我們有一個匿名的内部類實作了 Runnable 接口。我們想在 run 方法中通路索引變量 i,但編譯器不允許這麼做。

作為此限制的解決辦法,我們可以建立一個局部臨時變量,比如 temp,它是索引變量的一個副本。每次新的疊代都會建立變量 temp。在 Java 8 以前,我們需要将該變量标記為 final。從 Java 8 開始,可以将它視為實際的最終結果,因為我們不會再更改它。無論如何,由于事實上索引變量是一個在疊代中改變的變量,for 循環中就會出現這個額外變量。

現在嘗試使用 range 函數解決同一個問題。

清單 4. 在内部類中使用Lambda參數:

ExecutorService executorService = Executors.newFixedThreadPool(10);
                       
     IntStream.range(0, 5)
       .forEach(i -> 
         executorService.submit(new Runnable() {
           public void run() {
             System.out.println("Running task " + i); 
           }
         }));
 
     executorService.shutdown();           

複制

在作為一個參數被Lambda表達式接受後,索引變量 i 的語義與循環索引變量有所不同。與清單 3 中手動建立的 temp 非常相似,這個 i 參數在每次疊代中都表現為一個全新的變量。它是實際最終變量,因為我們不會在任何地方更改它的值。是以,我們可以直接在内部類的上下文中使用它 — 且不會有任何麻煩。

因為 Runnable 是一個函數接口,是以我們可以輕松地将匿名的内部類替換為Lambda表達式,比如:

清單 5. 将内部類替換為Lambda表達式:

IntStream.range(0, 5)
       .forEach(i -> 
         executorService.submit(() -> System.out.println("Running task " + i)));           

複制

顯然,對于相對簡單的疊代,使用 range 代替 for 具有一定優勢,但 for 的特殊價值展現在于它能處理更複雜的疊代場景。讓我們看看 range 和其他 Java 8 方法孰優孰劣。

封閉範圍

建立 for 循環時,可以将索引變量封閉在一個範圍内,比如:

清單 6. 一個具有封閉範圍的 for 循環:

for(int i = 0; i <= 5; i++) {}           

複制

索引變量 i 接受值 0、1、……5。無需使用 for,我們可以使用 rangeClosed 方法。在本例中,我們告訴 IntStream 将最後一個值限制在該範圍内:

清單 7. rangeClosed 方法:

IntStream.rangeClosed(0, 5)           

複制

疊代此範圍時,我們會獲得包含邊界值 5 在内的值。

跳過值

對于基本循環,range 和 rangeClosed 方法是 for 的更簡單、更優雅的替代方法,但是如果想跳過一些值該怎麼辦?在這種情況下,for 對前期工作的需求使該運算變得非常容易。在清單 8 中,for 循環在疊代期間快速跳過兩個值:

清單 8. 使用 for 跳過值:

int total = 0;
for(int i = 1; i <= 100; i = i + 3) {
  total += i;
}           

複制

清單 8 中的循環在 1 到 100 内對每次讀到的第三個值作求和計算 — 這種複雜運算可使用 for 輕松完成。能否也使用 range 解決此問題?

首先,可以考慮使用 IntStream 的 range 方法,再結合使用 filter 或 map。但是,所涉及的工作比使用 for 循環要多。一種更可行的解決方案是結合使用 iterate 和 limit:

清單 9. 使用 limit 的疊代:

IntStream.iterate(1, e -> e + 3)
  .limit(34)
  .sum()           

複制

iterate 方法很容易使用;它隻需擷取一個初始值即可開始疊代。作為第二參數傳入的Lambda表達式決定了疊代中的下一個值。這類似于清單 8,我們将一個表達式傳遞給 for 循環來遞增索引變量的值。但是,在本例中有一個陷阱。不同于 range 和 rangeClosed,沒有參數來告訴 iterate 方法何時停止疊代。如果我們沒有限制該值,疊代會一直進行下去。

如何解決這個問題?

我們對 1 到 100 之間的值感興趣,而且想從 1 開始跳過兩個值。稍加運算,即可确定給定範圍中有 34 個符合要求的值。是以我們将該數字傳遞給 limit 方法。

此代碼很有效,但過程太複雜:提前執行數學運算不那麼有趣,而且它限制了我們的代碼。如果我們決定跳過 3 個值而不是 2 個值,該怎麼辦?我們不僅需要更改代碼,結果也很容易出錯。我們需要有一個更好的方法。

takeWhile 方法

Java 9 中即将引入的 takeWhile 是一個新方法,它使得執行有限制的疊代變得更容易。使用 takeWhile,可以直接表明隻要滿足想要的條件,疊代就應該繼續執行。以下是使用 takeWhile 實作清單 9 中的疊代的代碼。

清單 10. 有條件的疊代:

IntStream.iterate(1, e -> e + 3)
     .takeWhile(i -> i <= 100) //available in Java 9
     .sum()           

複制

無需将疊代限制到預先計算的次數,我們使用提供給 takeWhile 的條件,動态确定何時終止疊代。與嘗試預先計算疊代次數相比,這種方法簡單得多,而且更不容易出錯。

與 takeWhile 方法相反的是 dropWhile,它跳過滿足給定條件前的值,這兩個方法都是 JDK 中非常需要的補充方法。takeWhile 方法類似于 break,而 dropWhile 則類似于 continue。從 Java 9 開始,它們将可用于任何類型的 Stream。

逆向疊代

與正向疊代相比,逆向疊代同樣非常簡單,無論使用傳統的 for 循環還是 IntStream。

以下是一個逆向的 for 循環疊代:

清單 11. 使用 for 的逆向疊代:

for(int i = 7; i > 0; i--) {           

複制

range 或 rangeClosed 中的第一個參數不能大于第二個參數,是以我們無法使用這兩種方法來執行逆向疊代。但可以使用 iterate 方法:

清單 12. 使用 iterate 的逆向疊代:

IntStream.iterate(7, e -> e - 1)
     .limit(7)           

複制

将一個Lambda表達式作為參數傳遞給 iterate 方法,該方法對給定值進行遞減,以便沿相反方向執行疊代。我們使用 limit 函數指定我們希望在逆向疊代期間看到總共多少個值。如有必要,還可以使用 takeWhile 和 dropWhile 方法來動态調整疊代流。

結束語

盡管傳統 for 循環非常強大,但它有些過于複雜。Java 8 和 Java 9 中的新方法可幫助簡化疊代,甚至是簡化複雜的疊代。方法 range、iterate 和 limit 的可變部分較少,這有助于提高代碼效率。這些方法還滿足了 Java 的一個長期以來的要求,那就是局部變量必須聲明為 final,然後才能從内部類通路它。将一個可變索引變量更換為實際的 final 參數隻有很小的語義差别,但它減少了大量垃圾變量。最終您會得到更簡單、更優雅的代碼。

本文由 小馬哥 創作,采用 知識共享署名4.0 國際許可協定進行許可

本站文章除注明轉載/出處外,均為本站原創或翻譯,轉載前請務必署名