[譯]通往 Java 函數式編程的捷徑

以聲明式的思想在你的 Java 程序中使用函數式編程技術

Java™ 開發人員習慣於面向命令式和麪向對象的編程,由於這些特性自 Java 語言首次發佈以來一直受到支持。在 Java 8 中,咱們得到了一組新的強大的函數式特性和語法。函數式編程已經存在了數十年,與面向對象編程相比,函數式編程一般更加簡潔和達意,不易出錯,而且更易於並行化。因此有很好的理由將函數式編程特性引入到 Java 程序中。儘管如此,在使用函數式特性進行編程時,就如何設計你的代碼這一點上須要進行一些改變。前端

關於本文java

Java 8 是 Java 語言自誕生以來最重要的更新,它包含如此多的新特性,以致於你可能想知道應該從哪開始瞭解它。在本系列中,身爲做家和教育家的 Venkat Subramaniam 提供了一種符合 Java 語言習慣的 Java 8 學習方式。邀請你進行簡短的探索後,從新思考你認爲理所固然的 Java 一向用法和規範,同時逐漸將新技術和語法集成到你的程序中去。android

我認爲,以聲明式的思想而不是命令式的思想來編程,能夠更加輕鬆地向更加函數化的編程風格過渡。在 Java 8 idioms series 這個系列的第一篇文章中,我解釋了命令式、聲明式和函數式編程風格之間的異同。而後,我將向你展現如何使用聲明式的思想逐漸將函數式編程技術集成到你的 Java 程序中。ios

命令式風格(面向過程)

受命令式編程風格訓練的開發者習慣於告訴程序須要作什麼以及如何去作。這裏是一個簡單的例子:git

清單 1. 以命令式風格編寫的 findNemo 方法
import java.util.*;

public class FindNemo {
  public static void main(String[] args) {
    List<String> names = 
      Arrays.asList("Dory", "Gill", "Bruce", "Nemo", "Darla", "Marlin", "Jacques");

    findNemo(names);
  }                 
  
  public static void findNemo(List<String> names) {
    boolean found = false;
    for(String name : names) {
      if(name.equals("Nemo")) {
        found = true;
        break;
      }
    }
    
    if(found)
      System.out.println("Found Nemo");
    else
      System.out.println("Sorry, Nemo not found");
  }
}
複製代碼

方法 findNemo() 首先初始化一個可變變量 flag,也稱爲垃圾變量(garbage variable)。開發者常常會給予某些變量一個臨時性的名字,例如 fttemp 以代表它們根本不該該存在。在本例中,這些變量應該被命名爲 foundgithub

接下來,程序會循環遍歷給定的 names 列表,每次都會判斷當前遍歷的值是否和待匹配值相同。在這個例子中,待匹配值爲 Nemo,若是遍歷到的值匹配,程序會將標誌位設爲 true,並執行流程控制語句 "break" 跳出循環。編程

這是對於廣大 Java 開發者最熟悉的編程風格 —— 命令式風格的程序,所以你能夠定義程序的每一步:你告訴程序遍歷每個元素,和待匹配值進行比較,設置標誌位,以及跳出循環。命令式編程風格讓你能夠徹底控制程序,有的時候這是一件好事。可是,換個角度來看,你作了不少機器能夠獨立完成的工做,這勢必致使生產力降低。所以,有的時候,你能夠經過少作事來提升生產力。後端

聲明式風格

聲明式編程意味着你仍然須要告訴程序須要作什麼,可是你能夠將實現細節留給底層函數庫。讓咱們看看使用聲明式編程風格重寫清單 1 中的 findNemo 方法時會發生什麼:bash

清單 2. 以聲明式風格編寫的 findNemo 方法
public static void findNemo(List<String> names) {
  if(names.contains("Nemo"))
    System.out.println("Found Nemo");
  else
    System.out.println("Sorry, Nemo not found");
}
複製代碼

首先須要注意的是,此版本中沒有任何垃圾變量。你也不須要在遍歷集合中浪費精力。相反,你只須要使用內建的 contains() 方法來完成這項工做。你仍然要告訴程序須要作什麼,集合中是否包含咱們正在尋找的值,但此時你已經將細節交給底層的方法來實現了。函數式編程

在命令式編程風格的例子中,你控制了遍歷的流程,程序能夠徹底按照指令進行;在聲明式的例子中,只要程序可以完成工做,你徹底不須要關注它是如何工做的。contains() 方法的實現可能會有所不一樣,但只要結果符合你的指望,你就會對此感到滿意。更少的工做可以獲得相同的結果。

訓練本身以聲明式的編程風格來進行思考將更加輕鬆地向更加函數化的編程風格過渡。緣由在於,函數式編程風格是創建在聲明式風格之上的。聲明式風格的思惟可讓你逐漸從命令式編程轉換到函數式編程。

函數式編程風格

雖然函數式風格的編程老是聲明式的,可是簡單地使用聲明式風格編程並不等同與函數式編程。這是由於函數式編程時將聲明式編程和高階函數結合在了一塊兒。圖 1 顯示了命令式,聲明式和函數式編程風格之間的關係。

圖 1. 命令式、聲明式和函數式編程風格之間的關係

A logic diagram showing how the imperative, declarative, and functional programming styles differ and overlap.

Java 中的高階函數

在 Java 中,你能夠將對象傳遞給方法,在方法中建立對象,也能夠從方法中返回對象。同時你也能夠用函數作相同的事情。也就是說,你能夠將函數傳遞給方法,在方法中建立函數,也能夠從方法中返回函數。

在這種狀況下,方法是類的一部分(靜態或實例),可是函數能夠是方法的一部分,而且不能有意地與類或實例相關聯。一個能夠接收、建立、或者返回函數的方法或函數稱之爲高階函數

一個函數式編程的例子

採用新的編程風格須要改變你對程序的見解。這是一個從簡單例子的練習開始,到構建更加複雜程序的過程。

清單 3. 命令式編程風格下的 Map
import java.util.*;

public class UseMap {
  public static void main(String[] args) {
    Map<String, Integer> pageVisits = new HashMap<>();            
    
    String page = "https://agiledeveloper.com";
    
    incrementPageVisit(pageVisits, page);
    incrementPageVisit(pageVisits, page);
    
    System.out.println(pageVisits.get(page));
  }
  
  public static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {
    if(!pageVisits.containsKey(page)) {
       pageVisits.put(page, 0);
    }
    
    pageVisits.put(page, pageVisits.get(page) + 1);
  }
}
複製代碼

清單 3 中,main() 函數建立了一個 HashMap 來保存網站訪問次數。同時,incrementPageVisit() 方法增長了每次訪問給定頁面的計數。咱們將聚焦此方法。

以命令式編程風格寫的 incrementPageVisit() 方法:它的工做是爲給定頁面增長一個計數,並存儲在 Map 中。該方法不知道給定頁面是否已經有計數值,因此會先檢查計數值是否存在,若是不存在,會爲該頁面插入一個值爲"0"的計數值。而後再獲取該計數值,遞增它,並將新的計數值存儲在 Map 中。

以聲明式的方式思考須要你將方法的設計從 "how" 轉變到 "what"。當 incrementPageVisit() 方法被調用時,你須要將給定的頁面計數值初始化爲 1 或者計數值加 1。這就是 what

由於你是經過聲明式編程的,那麼下一步就是在 JDK 庫中尋找能夠完成這項工做且實現了 Map 接口的方法。換言之,你須要找到一個知道如何完成你指定任務的內建方法。

事實證實 merge() 方法很是適合你的而目的。清單 4 使用新的聲明式方法對清單 3 中的 incrementPageVisit() 方法進行修改。可是,在這種狀況下,你不只僅只是選擇更智能的方法來寫出更具聲明性風格的代碼,由於 merge() 是一個更高階的函數。因此說,新的代碼其實是一個體現函數式風格的很好的例子:

清單 4. 函數式編程風格下的 Map
public static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {
    pageVisits.merge(page, 1, (oldValue, value) -> oldValue + value); 
}
複製代碼

在清單 4 中,page 做爲第一個參數傳遞給 merge():map 中鍵對應的值將會被更新。第二個參數做爲初始值,若是 Map 中不存在指定鍵的值,那麼該值將會賦值給 Map 中鍵對應的值(在本例中爲"1")。第三個參數爲一個 lambda 表達式,接受當前 Map 中鍵對應的值和該函數中第二個參數對應的值做爲參數。lambda 表達式返回其參數的總和,實際上增長了計數值。(編者注:感謝 István Kovács 修正了代碼錯誤)

清單 4incrementPageVisit() 方法中的單行代碼與清單 3 中的多行代碼進行比較。雖然清單 4 中的程序是函數式編程風格的一個例子,但經過聲明性地思想去思考問題幫助可以咱們實現飛躍。

總結

在 Java 程序中採用函數式編程技術和語法有不少好處:代碼更簡潔,更富有表現力,移動部分更少,實現並行化更容易,而且一般比面向對象的代碼更易理解。 目前面臨的挑戰是,如何將你的思惟從絕大多數開發人員所熟悉的命令式編程風格轉變爲以聲明式的方式進行思考。

雖然函數式編程並無那麼簡單或直接,可是你能夠學習專一於你但願程序作什麼而不是如何作這件事,來取得巨大的飛躍。經過容許底層函數庫管理執行,你將逐漸直觀地瞭解用於構建函數式編程模塊的高階函數。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索