banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

【轉載】解密 Java Lambda 表達式

title: 【轉載】解密 Java Lambda 表達式
date: 2021-08-14 08:35:12
comment: false
toc: true
category:

  • Java
    tags:
  • 轉載
  • Java
  • Lambda
  • 解密

本文轉載自:[譯] 解密 Java Lambda 表達式


解密 Java Lambda 表達式#

我似乎花了很多時間講解 Java 中的函數式編程。其實並沒有什麼深奧難懂的東西。為了使用某些函數的功能,你需要在函數中嵌套定義函數。為什麼那樣做?當你使用面向對象的方式進行開發,你已經使用了函數式編程的方法,只不過是以一種受控的方式使用。Java 中的多態就是通過保存若干個可以在子類中重寫的函數實現的。這樣,該類的其他函數可以調用被重寫的函數,即使外部函數沒有被重寫,它的行為也發生了改變。

我們來舉個多態的例子,並將它轉換為使用 lambda 表達式的函數。代碼如下:

@Slf4j
public class Application {
    static abstract class Pet {
        public abstract String vocalize();
        public void disturb() { log.info(vocalize()); }
    }
    static class Dog extends Pet {
        public String vocalize() { return "bark"; }
    }
    static class Cat extends Pet {
        public String vocalize() { return "meow"; }
    }
    public static void main(String [] args)  {
        Pet cat = new Cat();
        Pet dog = new Dog();
        cat.disturb();
        dog.disturb();
    }
}

這是非常經典的面向對象的例子,即 Dog 類和 Cat 類繼承實現 Pet 類的例子。聽起來很熟悉?在這個再簡單不過的例子中,如果你執行 disturb () 方法,程序會如何運行?結果是:貓和狗各自發出自己的叫聲。

但是,如果你需要一條蛇,該怎麼辦?你需要新建一個類,如果需要 1000 個類呢?每個類都需要模板文件。如果 Pet 接口只有一個簡單的方法,Java 也會把它看作一個函數。我把 Pet移出 disturb 接口(可能它起初不在這裡,它不是 Pet 的屬性)。如下所示:

@Slf4j
public class Application {
    interface Pet {
        String vocalize();
    }
    static void disturbPet(Pet p) {
        log.info(p.vocalize());
    }
    public static void main(String [] args)  {
        Pet cat = () -> "meow";
        Pet dog = () -> "bark";
        Pet snake = () -> "hiss";
        disturbPet(cat);
        disturbPet(dog);
        disturbPet(snake);
    }
}

這種古怪的語法 () -> something 令人大吃一驚。但是,它只是定義了一個函數,這個函數沒有輸入參數,有返回對象。由於 Pet 接口只有一個方法,開發者就可以通過這種方式來調用方法。從技術角度來看,它實現了 Pet 接口,重寫了 vocalize 函數。但對於我們的討論的主題來說,它是一個可以嵌入其他函數的函數。

由於 Supplier 接口可以替代 Pet 接口,這段代碼還可以進一步精簡。如下所示:

@Slf4j
public class Application {
    static void disturbPet(Supplier<String> petVocalization) {
        log.info(petVocalization.get());
    }
    public static void main(String [] args)  {
        disturbPet(() -> "meow");
        disturbPet(() -> "bark");
        disturbPet(() -> "hiss");
    }
}

由於 Supplier 是一個公共接口,它位於 java.util.function 包中。

這些 lambda 函數看起來像是我們憑空捏造的。但在它們的背後,我們是在利用單一函數去實現接口,並提供單一函數的具體實現。

我們來討論另一個公共函數 Consumer。它把某個值作為輸入參數,沒有返回值,本質上是消費了這個值。如果你已經使用了列表或流對象的 forEach 方法,在這裡你可以用 Consumer。我們會收集所有的寵物的叫聲,存入一個列表,再逐個調用。如下所示:

@Slf4j
public class Application {
    static void disturbPet(Supplier<String> petVocalization) {
        log.info(petVocalization.get());
    }
    public static void main(String [] args)  {
        List<Supplier<String>> yourPetVocalizations = List.of(
                () -> "bark",
                () -> "meow",
                () -> "hiss");
        yourPetVocalizations.forEach(v -> disturbPet(v));
    }
}

現在,如果你添加一隻鳥,只需要在列表中加入 () -> “chirp”。注意:表達式 v -> disturbPet(v) 中第一個 v 兩邊不加括號。對包含單一參數的 lambda 表達式而言,括號可以不加。

OK,我展示的例子並不直觀。我的目的是從多態函數入手,引入 lambda 表達式的相關內容。何時可以實際使用 lambda 表達式?有一些例子,它們具有通用性,應當反復研究。這些例子也被納入了 Stream 類庫中。

這個例子比較直觀,我會獲取一系列的文件,刪除那些不以點作為開頭的文件,並獲取文件名和文件大小。首先需要從當前目錄獲取文件數組,並將它轉為 Stream 類型。我們可以使用 File 類實現:

File dir = new File(".");
Stream s = Arrays.stream(dir.listFiles());

由於目錄也是文件對象,我們可以對它執行某些文件對象的操作。同時,“.” 表示一個目錄,我們也可以調用 listFiles 方法。但它會返回一個數組,所以應當使用流來處理,我們需要使用 Arrays.stream 方法將數組轉為流對象。

現在,我們刪除以點開頭的文件,將 File 對象轉為一個由其名稱和大小組成的字符串,按字母順序排列,並寫入日誌。

public class Application {
    public static void main(String [] args)  {
        File dir = new File(".");
        Arrays.stream(dir.listFiles())
                .filter(f -> f.isFile())
                .filter(f -> !f.getName().startsWith("."))
                .map(f -> f.getName() + " " + f.length())
                .sorted()
                .forEach(s -> log.info(s));
    }
}

處理 lambda 表達式的兩個新方法是 filtermapfilter 方法相當於 Predicate 類型,map 方法相當於 Function 類型,它們都屬於 java.util.function 包。它們都提供了通用的操作對象的方法,Predicate 用於測試對象的某些特徵,Function 用於對象之間的轉換。

注意:我也考慮了一個文件的情況。如何處理目錄?如果使用遞歸到目錄中會如何?怎樣使用流對象處理它?有一種特殊的 map,它可以把一個內部流對象添加到外部流對象中。這意味著什麼?我們來看下面的例子:

public static Stream<File> getFiles(File file) {
     return Arrays.stream(file.listFiles())
             .filter(f -> !f.getName().startsWith("."))
             .flatMap(f -> {
                if(f.isDirectory()) {
                    return getFiles(f);
                } else {
                    return Stream.of(f);
                }
             });

 }

如你所見,如果文件對象是一個目錄,flatMap 方法中使用的 lambda 表達式執行了遞歸,如果不是,就返回文件對象本身。flatMap 方法的 lambda 表達式的返回值必須是某個流對象。所以在單一文件的情況下,我們需要使用 Stream.of() 實現返回值類型的匹配。也應該注意到,lambda 表達式包含於花括號內,所以如果需要返回某個對象,應該添加 return 語句。

為了使用 getFiles 方法,我們可以把它加入 main 方法。

public static void main(String [] args)  {
        File dir = new File(".");
        getFiles(dir)
                .map(f -> f.getAbsolutePath()
                     .substring(dir.getAbsolutePath().length())
                     + " " + f.length())
                .sorted()
                .forEach(s -> log.info(s));
    }

在沒有全路徑的情況下,我們必須通過某些機制獲取文件名中的相對路徑。但是現在,不需要這麼複雜了。

把其他函數作為參數的函數一般稱為 高階函數 。我們已經了解了幾種高階函數:forEach, filter, mapflatMap。它們中的每一個都代表了一種以不同於參數和返回值的抽象方式操作對象的方法。我們使用 lambda,就是要進行明確的操作。利用這種方式,我們還可以把多個操作串聯於一系列對象上,以便得到需要的結果。

我希望本文能向讀者揭示 lambda 函數的神秘面紗。我想,當第一次引入這個話題,它本身就有些嚇人。當然,它是從 Alonzo Church 的 lambda 演算借用過來的,但這又是另一個故事了。現在,你應該了解:使用這種簡單的語法,函數也可以憑空產生。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。


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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。