「Java 進階」--Lambda & 函數式編程

前些年 Scala 大肆流行,打出來 Java 顛覆者的旗號,究其底氣來源,無非是函數式和麵向對象的“完美結合”,各式各樣的“語法糖”,但其過高的學習門檻,又給了新來者當頭一棒。

隨著 Java8 的發佈,Lambda 特性的引入,之前的焦灼局面是否有所轉變,讓我們一起揭開 Java 函數式編程的面紗:

  1. 面向對象 VS 函數式
  2. FunctionalInterface 和 Lambda
  3. 類庫的升級改造(默認方法、靜態方法、Stream、Optional)
  4. Lambda 下模式的進化
  5. Lambda 下併發程序

1. 面向對象 VS 函數式編程

一句話總結兩種的關係:面向對象編程是對數據進行抽象;而函數式編程是對行為進行抽象。

在現實世界中,數據和行為並存,程序也應如此,可喜可賀的是在 Java 世界中,兩者也開啟了融合之旅。

首先思考一個問題, 在 Java 編程中,我們如何進行行為傳遞,例如我們需要打印線程名稱和當前時間,並將該任務提交到線程池中運行,會有哪些方法?

方法 1:新建 class Task 實現 Runnable 接口

<code>    public class Task implements Runnable{        @Override        public void run() {            System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms");        }    }    executorService.submit(new Task());/<code> 

方法 2:匿名內部類實現 Runnable 接口

<code>    executorService.submit(new Runnable() {            @Override            public void run() {              System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms");            }        });/<code>

方法 3:使用 Lambda 表達式

<code>    executorService.submit(()-> System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms"));/<code>

方法 4:使用方法引用

<code>     private void print(){        System.out.println(Thread.currentThread().getName() + "-->" + System.currentTimeMillis() + "ms");    }    {        executorService.submit(this::print);    }/<code>

通過上面不同的行為傳遞方式,能夠比較直觀的體會到隨著函數式特性的引入,行為傳遞少了很多樣板代碼,增加了一絲靈活;可見Lambda表達式是一種緊湊的、傳遞行為的方式。

2. FunctionalInterface 和 Lambda

Java 函數式編程,只有兩個核心概念:

FunctionalInterface(函數接口)是隻有一個抽象方法的接口,用作 Lambda 表達式的類型。

Lambda 表達式,及要傳遞的行為代碼,更像是一個匿名函數(當然 java 中並沒有這個概念),將行為像數據那樣進行傳遞。

換個好理解但是不正規的說法,FunctionalInterface 為類型,Lambda 表達式為值;我們可以將一個 Lambda 表達式賦予一個符合 FunctionalInterface 要求的接口變量(局部變量、方法參數)。

2.1. Lambda 表達式

先看幾個 Lambda 表達式的例子:

<code>        // 不包含參數,用()表示沒有參數        // 表達式主體只有一個語句,可以省略{}        Runnable helloWord = () -> System.out.println("Hello World");        // 表達式主體由多個語句組成,不能省略{}        Runnable helloWords = () -> {            System.out.println("Hello");            System.out.println("Word");            System.out.println("Word");        };        // 表達式中只有一個參數,可以省略()        Consumer<string> infoConsumer = msg -> System.out.println("Hello " + msg);        // 表達式由多個參數組成,不可省略()        BinaryOperator<integer> add1 = (Integer i ,Integer j) -> i + j;        // 編譯器會進行類型推斷,在沒有歧義情況下可以省略類型聲明,但是不可省略()        BinaryOperator<integer> add2 = (i, j) -> i + j;/<integer>/<integer>/<string>/<code>

綜上可見,一個 Lambda 表達式主要由三部分組成:

  1. 參數列表
  2. 箭頭分隔符(->)
  3. 主體,單個表達式或語句塊

我們在使用匿名內部類時有一些限制:引用方法中的變量時,需要將變量聲明為 final,不能為其進行重新賦值,如下:

<code>        final String msg = "World";        Runnable print = new Runnable() {            @Override            public void run() {                System.out.println("Hello"  + msg);            }        };/<code>

在 Java8 中放鬆了這個限制,可以引用非 final 變量,但是該變量在既成事實上必須是 final 的,雖然無需將變量聲明為 final,在 Lambda 表達式中,也無法用作非最終態變量,及只能給該變量賦值一次(與用 final 聲明變量效果相同)。

2.2 FunctionalInterface

FunctionalInterface,只有一個抽象方法的接口就是函數式接口,接口中單一方法命名並不重要,只要方法簽名與 Lambda 表達式的類型匹配即可。

Java 內置了常用函數接口如下:

<code>1. Predicate參數類型:T返回值:boolean示例:Predicate<string> isAdmin = name -> "admin".equals(name);/<string>/<code>
<code>2. Consumer參數:T返回值:void示例:Consumer<string> print = msg -> System.out.println(msg);/<string>/<code>
<code>3. Function參數:T返回值:R示例:Function<long> toStr = value -> String.valueOf(value);/<long>/<code>
<code>4. Supplier參數:none返回值:T示例:Supplier<date> now = () -> new Date();/<date>/<code>
<code>5. UnaryOperator參數:T返回值:T示例:UnaryOperator<boolean> negation = value -> !value.booleanValue();/<boolean>/<code>
<code>6. BinaryOperator參數:(T, T)返回值:T示例:BinaryOperator<integer> intDouble = (i, j) -> i + j;/<integer>/<code>
<code>7. Runnable參數:none返回值:void示例:Runnable helloWord = () -> System.out.println("Hello World");/<code>
<code>8. Callable參數:nont返回值:T示例:Callable<date> now1 = () -> new Date();/<date>/<code> 

當然我們也可以根據需求自定義函數接口,為了保證接口的有效性,可以在上面添加 @FunctionalInterface 註解,該註解會強制 javac 檢測一個接口是否符合函數式接口的規範,例如:

<code>    @FunctionalInterface    interface CustomFunctionalInterface{        void print(String msg);    }    CustomFunctionalInterface cfi= msg -> System.out.println(msg);/<code>

2.3 方法引用

Lambda 表達式一種常用方法便是直接調用其他方法,針對這種情況,Java8 提供了一個簡寫語法,及方法引用,用於重用已有方法。

凡是可以使用 Lambda 表達式的地方,都可以使用方法引用。

方法應用的標準語法為 ClassName::methodName,雖然這是一個方法,但不需要再後面加括號,因為這裡並不直接調用該方法。

<code>    Function<user> f1 = user->user.getName();    Function<user> f2 = User::getName;    Supplier<user> s1 = ()->new User();    Supplier<user> s2 = User::new;    Function<integer> sa1 = count -> new User[count];    Function<integer> sa2 = User[]::new;/<integer>/<integer>/<user>/<user>/<user>/<user>/<code>

方法引用主要分為如下幾種類型:

  • 靜態方法引用:className::methodName
  • 實例方法引用:instanceName::methodName
  • 超類實體方法引用:supper::mehtodName
  • 構造函數方法引用:className::new
  • 數組構造方法引用:ClassName[]::new

2.4 類型推斷

類型推斷,是 Java7 就引入的目標類型推斷的擴展,在 Java8 中對其進行了改善,程序員可以省略 Lambda 表達式中的所有參數類型,Javac 會根據 Lambda 表達式式上下文信息自動推斷出參數的正確類型。

大多數情況下 javac 能夠準確的完成類型推斷,但由於 Lambda 表達式與函數名無關,只與方法簽名相關,因此會出現類型對推斷失效的情況,這時可以使用手工類型轉換幫助 javac 進行正確的判斷。

<code>    // Supplier<string>, Callable<string> 具有相同的方法簽名    private void print(Supplier<string> stringSupplier){        System.out.println("Hello " + stringSupplier.get());    }    private void print(Callable<string> stringCallable){        try {            System.out.println("Hello " + stringCallable.call());        } catch (Exception e) {            e.printStackTrace();        }    }    {        // Error, 因為兩個print同時滿足需求        print(()->"World");// 使用類型轉換,為編譯器提供更多信息        print((Supplier<string>) ()->"World");        print((Callable<string>) ()-> "world");    }/<string>/<string>/<string>/<string>/<string>/<string>/<code>

3. 類庫的升級改造

Java8 另一個變化是引入了 默認方法 和接口的 靜態方法 ,自此以後 Java 接口中方法也可以包含代碼體了。

3.1 默認方法

**默認方法允許接口方法定義默認實現,而所有子類都將擁有該方法及實現。**使其能夠在不改變子類實現的情況下(很多時候我們無法拿到子類的源碼),為所有子類添加新的功能,從而最大限度的保證二進制接口的兼容性。

默認方法的另一個優勢是該方法是可選的,子類可以根據不同的需求 Override 默認實現,為其提供擴展性保證。

其中 Collection 中的 forEach,stream 功能都是通過該技術統一添加到接口中的。

<code>    // Collection 中的forEache實現    default void forEach(Consumer super T> action) {        Objects.requireNonNull(action);        for (T t : this) {            action.accept(t);        }    }    // Collection中的stream實現    default Stream stream() {        return StreamSupport.stream(spliterator(), false);    }/<code>

從上可見,默認方法的寫法也是比較簡單的,只需在方法聲明中添加 defalut 關鍵字,然後提供方法的默認實現即可。

和類不同,接口中沒有成員變量,因此默認方法只能通過調用子類的方法來修改子類本身,避免了對子類的實現做出各種假設。

3.1.1 默認方法與子類

添加默認方法特性後,方法的重寫規則也發生了變化,具體的場景如下:

a. 沒有重寫

沒有重寫是最簡單的情況,子類調用該方法的時候,自然繼承了默認方法。

<code>    interface Parent{        default void welcome(){            System.out.println("Parent");        }    }    // 調用Parent中的welcome, 輸入"Parent"    class ParentNotImpl implements Parent{    }/<code>

b. 子接口重寫

子接口對父接口中的默認方法進行了重新,其子類方法被調用時,執行子接口中的默認方法

<code>     interface Parent{        default void welcome(){            System.out.println("Parent");        }    }    interface ChildInterface extends Parent{        @Override        default void welcome(){            System.out.println("ChildInterface");        }    }    // 執行ChildInterface中的welcome, 輸入 "ChildInterface"    class ChildImpl implements ChildInterface{    }/<code>

c. 類重寫

一旦類中重寫了默認方法,優先選擇類中定義的方法,如果存在多級類繼承,遵循類繼承邏輯。

<code>     interface Parent{        default void welcome(){            System.out.println("Parent");        }    }    interface ChildInterface extends Parent{        @Override        default void welcome(){            System.out.println("ChildInterface");        }    }    //執行子類中的welcome方法,輸出"ChildImpl"    class ChildImpl1 implements ChildInterface{        @Override        public void welcome(){            System.out.println("ChildImpl");        }    }/<code>
3.1.2 多重繼承

接口允許多重繼承,因此有可能會碰到兩個接口包含簽名相同的默認方法的情況,此時 javac 並不明確應該繼承哪個接口中的方法,因此會導致編譯出錯,這時需要在類中實現該方法,如果想調用特定父接口中的默認方法,可以使用 ParentInterface.super.method() 的方式來指明具體的接口。

<code>    interface Parent1 {        default void print(){            System.out.println("parent1");        }    }    interface Parent2{        default void print(){            System.out.println("parent2");        }    }    class Child implements Parent1, Parent2{        @Override        public void print() {            System.out.println("self");            Parent1.super.print();            Parent2.super.print();        }    }/<code>

現在的接口提供了某種形式上的多繼承功能,然而多重繼承存在很多詬病。很多人認為多重繼承的問題在於對象狀態的繼承,而不是代碼塊的繼承,默認方法避免了狀態的繼承,也因此避免了 C++ 中多重繼承最大的缺點。

接口和抽象類之間還是有明顯的區別。接口允許多重繼承,卻沒有成員變量;抽象類可以繼承成員變量,卻不能多重繼承。

從某種角度出發,Java 通過接口默認方法實現了代碼多重繼承,通過類實現了狀態單一繼承。

3.1.3 三定律

如果對默認方法的工作原理,特別是在多重繼承下的行為沒有把握,可以通過下面三條簡單定律幫助大家。

  1. 類勝於方法。 如果在繼承鏈中有方法體或抽象的方法聲明,那麼就可以忽略接口中定義的方法。
  2. 子類勝於父類。 如果一個接口繼承另一個接口,且兩個接口都定義了一個默認方法,那麼子接口中定義的方法勝出。
  3. 沒有規則三。 如果上面兩條規則不適用,子類要麼實現該方法,要麼將該方法聲明為抽象方法。

3.2 接口靜態方法

人們在編程過程中積累了這樣一條經驗,創建一個包含很多靜態方法的一個類。很多時候類是一個放置工具方法的好地方,比如 Java7 引入的 Objects 類,就包含很多工具方法,這些方法不是屬於具體的某個類。

如果一個方法有充分的語義原因和某個概念相關,那麼就應該講該方法和相關的類或接口放在一起,而不是放到另一個工具類中,這非常有助於更好的組織代碼。

在接口中定義靜態方法,只需使用 static 關鍵字進行描述即可,例如 Stream 接口中的 of 方法。

<code>    /**     * Returns a sequential {@code Stream} containing a single element.     *     * @param t the single element     * @param  the type of stream elements     * @return a singleton sequential stream     */    public static Stream of(T t) {        return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);    }/<code>

3.3 Stream

Stream 是 Java8 中最耀眼的亮點,它使得程序員得以站在更高的抽象層次對集合進行操作。

Stream 是用函數式編程方式在集合類上進行復雜操作的工具。

3.3.1. 從外部迭代到內部迭代

Java 程序員使用集合時,一個通用模式就是在集合上進行迭代,然後處理返回的每一個元素,儘管這種操作可行但存在幾個問題:

  • 大量的樣板代碼
  • 模糊了程序本意
  • 串行化執行

常見集合遍歷如下:

<code>     // 常見寫法1,不推薦使用    public void printAll1(List<string> msg){        for (int i=0; i< msg.size(); i++){            String m = msg.get(i);            System.out.println(m);        }    }    // Java5之前,正確寫法,過於繁瑣    public void printAll2(List<string> msg){        Iterator<string> iterator = msg.iterator();        while (iterator.hasNext()){            String m = iterator.next();            System.out.println(m);        }    }    // Java5之後,加強for循環,採用語法糖,簡化for循環,內部轉化為Iterator方式    public void printAll3(List<string> msg){        for (String m : msg){            System.out.println(m);        }    }/<string>/<string>/<string>/<string>/<code>

整個迭代過程,通過顯示的調用 Iterator 對象的 hasNext 和 next 方法完成整個迭代,這成為外部迭代。

「Java 進階」--Lambda & 函數式編程

另一種方式成為內部迭代,及將操作行為作為參數傳遞給 Stream,在 Stream 內部完成迭代操作。

<code>     // Java8中,使用Stream進行內部迭代操作    public void printAll4(List<string> msg){        msg.stream().forEach(System.out::println);    }/<string>/<code>

內部迭代:

「Java 進階」--Lambda & 函數式編程

3.3.2. 惰性求值 VS 及早求值

Stream 中存在兩類方法,不產生值的方法稱為惰性方法;從 Stream 中產生值的方法叫做及早求值方法。

判斷一個方法的類別很簡單:如果返回值是 Stream,那麼就是惰性方法;如果返回值是另一個值或為空,那麼就是及早求值方法。

惰性方法返回的 Stream 對象不是一個新的集合,而是創建新集合的配方,Stream 本身不會做任何迭代操作,只有調用及早求值方法時,才會開始真正的迭代。

整個過程與 Builder 模式有共通之處,惰性方法負責對 Stream 進行裝配(設置 builder 的屬性),調用及早求值方法時(調用 builder 的 build 方法),按照之前的裝配信息進行迭代操作。

常見 Stream 操作:

3.3.2.1 collect(toList())

及早求值方法:

collect(toList()) 方法由 Stream 裡面的值生成一個列表,是一個及早求值操作。

collect 的功能不僅限於此,它是一個非常強大的結構。

<code>     @Data    class User{        private String name;    }    public List<string> getNames(List<user> users){        List<string> names = new ArrayList<>();        for (User user : users){            names.add(user.getName());        }        return names;    }    public List<string> getNamesUseStream(List<user> users){      // 方法引用      //return users.stream().map(User::getName).collect(toList());        // lambda表達式        return users.stream().map(user -> user.getName()).collect(toList());    }/<user>/<string>/<string>/<user>/<string>/<code>

3.3.2.2. count、max、min

及早求值方法:

Stream 上最常用的操作之一就是求總數、最大值和最小值,count、max 和 min 足以解決問題。

<code>    public Long getCount(List<user> users){        return users.stream().filter(user -> user != null).count();    }    // 求最小年齡    public Integer getMinAge(List<user> users){        return users.stream().map(user -> user.getAge()).min(Integer::compareTo).get();    }    // 求最大年齡    public Integer getMaxAge(List<user> users){        return users.stream().map(user -> user.getAge()).max(Integer::compareTo).get();    }/<user>/<user>/<user>/<code>

min 和 max 入參是一個 Comparator 對象,用於元素之間的比較,返回值是一個 Optional,它代表一個可能不存在的值,如果 Stream 為空,那麼該值不存在,如果不為空,該值存在。通過 get 方法可以獲取 Optional 中的值。

3.3.2.3 findAny、findFirst

及早求值方法:

兩個函數都以Optional為返回值,用於表示是否找到。

<code>    public Optional<user> getAnyActiveUser(List<user> users){        return users.stream()                .filter(user -> user.isActive())                .findAny();    }    public Optional<user> getFirstActiveUser(List<user> users){        return users.stream()                .filter(user -> user.isActive())                .findFirst();    }/<user>/<user>/<user>/<user>/<code>

3.3.2.4 allMatch、anyMatch、noneMatch

及早求值方法:

均以 Predicate 作為輸入參數,對集合中的元素進行判斷,並返回最終的結果。

<code>    // 所有用戶是否都已激活    boolean allMatch = users.stream().allMatch(user -> user.isActive());    // 是否有激活用戶    boolean anyMatch = users.stream().anyMatch(user -> user.isActive());    // 是否所有用戶都沒有激活    boolean noneMatch = users.stream().noneMatch(user -> user.isActive());/<code>

3.3.2.6. forEach

及早求值:

以 Consumer 為參數,對 Stream 中複合條件的對象進行操作。

<code>    public void printActiveName(List<user> users){        users.stream()                .filter(user -> user.isActive())                .map(user -> user.getName())                .forEach(name -> System.out.println(name));    }/<user>/<code>

3.3.2.7 reduce

及早求值方法:

reduce 操作可以實現從一組值中生成一個值,之前提到的 count、min、max 方法因為比較通用,單獨提取成方法,事實上,這些方法都是通過 reduce 完成的。

下圖展示的是對 stream 進行求和的過程,以 0 為起點,每一步都將 stream 中的元素累加到 accumulator 中,遍歷至最後一個元素,accumulator 就是所有元素值的和。

「Java 進階」--Lambda & 函數式編程

3.3.2.8. filter

惰性求值方法:

以 Predicate 作為參數(相當於 if 語句),對 Stream 中的元素進行過濾,只有複合條件的元素才能進入下面的處理流程。

處理流程如下:

「Java 進階」--Lambda & 函數式編程

<code>    public List<user> getActiveUser(List<user> users){        return users.stream()                .filter(user -> user.isActive())                .collect(toList());    }/<user>/<user>/<code>

3.3.2.9 map

及早求值方法: 以 Function 作為參數,將 Stream 中的元素從一種類型轉換成另外一種類型。

處理過程如下:

「Java 進階」--Lambda & 函數式編程

<code>    public List<string> getNames(List<user> users){        return users.stream()                .map(user -> user.getName())                .collect(toList());    }/<user>/<string>/<code>

3.3.2.10 peek

Stream 提供的是內迭代,有時候為了功能調試,需要查看每個值,同時能夠繼續操作流,這時就會用到 peek 方法。

<code>    public void printActiveName(List<user> users){        users.stream()                .filter(user -> user.isActive())                .peek(user -> System.out.println(user.isActive()))                .map(user -> user.getName())                .forEach(name -> System.out.println(name));    }/<user>/<code>

3.3.2.11 其他

針對集合 Stream 還提供了許多功能強大的操作,暫不一一列舉,簡單彙總一下。

  • distinct:進行去重操作
  • sorted:進行排序操作
  • limit:限定結果輸出數量
  • skip:跳過 n 個結果,從 n+1 開始輸出

3.4 Optional

Java 程序中出現最多的異常就是 NullPointerException,沒有之一。Optional 的出現力求改變這一狀態。

Optional 對象相當於值的容器,而該值可以通過 get 方法獲取,同時 Optional 提供了很多函數用於對值進行操作,從而最大限度的避免 NullPointerException 的出現。

Optional 與 Stream 的用法基本類型,所提供的方法同樣分為惰性和及早求值兩類,惰性方法主要用於流程組裝,及早求值用於最終計算。

3.4.1 of

使用工廠方法 of,可以從一個值中創建一個 Optional 對象,如果值為 null,會報 NullPointerException。

<code>    Optional<string> dataOptional = Optional.of("a");    String data = dataOptional.get(); // data is "a"    Optional<string> dataOptional = Optional.of(null);    String data = dataOptional.get(); // throw NullPointerException/<string>/<string>/<code>
3.4.2 empty

工廠方法 empty,可以創建一個不包含任何值的 Optional 對象。

<code>    Optional<string> dataOptional = Optional.empty();    String data = dataOptional.get(); //throw NoSuchElementException/<string>/<code>
3.4.3 ofNullable

工廠方法 ofNullable,可將一個空值轉化成 Optional。

<code>     public static  Optional ofNullable(T value) {        return value == null ? empty() : of(value);    }/<code>
3.4.4 get、orElse、orElseGet、orElseThrow

直接求值方法,用於獲取 Optional 中值,避免空指針異常的出現。

<code>    Optional<string> dataOptional = Optional.of("a");    dataOptional.get(); // 獲取Optional中的值, 不存在會拋出NoSuchElementException    dataOptional.orElse("b"); //獲取Optional中的值,不存在,直接返回"B"    dataOptional.orElseGet(()-> String.valueOf(System.currentTimeMillis())); //獲取Optional中的值,不存在,對Supplier進行計算,並返回計算結果    dataOptional.orElseThrow(()-> new XXXException()); //獲取Optional中的值,不存在,拋出自定義異常/<string>/<code>
3.4.5 isPresent、ifPresent

直接求值方法,isPresent 用於判斷 Optional 中是否有值,ifPresent 接收 Consumer 對象,當 Optional 有值的情況下執行。

<code>    Optional<string> dataOptional = Optional.of("a");    String value = null;    if (dataOptional.isPresent()){        value = dataOptional.get();    }else {        value = "";    }    //等價於    String value2 = dataOptional.orElse("");    // 當Optional中有值的時候執行    dataOptional.ifPresent(v->System.out.println(v));/<string>/<code>
3.4.6 map

惰性求值方法。map 與 Stream 中的用法基本相同,用於對 Optional 中的值進行映射處理,從而避免了大量 if 語句嵌套,多個 map 組合成鏈,只需對最終的結果進行操作,中間過程中如果存在 null 值,之後的 map 不會執行。

<code>    @Data    static class Order{        private Name owner;    }    @Data    static class User{        private Name name;    }    @Data    static class Name{        String firstName;        String midName;        String lastName;    }    private String getFirstName(Order order){        if (order == null){            return "";        }        if (order.getOwner() == null){            return "";        }        if (order.getOwner().getFirstName() == null){            return "";        }        return order.getOwner().getFirstName();    }    private String getFirstName(Optional<order> orderOptional){        return orderOptional.map(order -> order.getOwner())                .map(user->user.getFirstName())                .orElse("");    }/<order>/<code>
3.4.7 filter

惰性求值,對 Optional 中的值進行過濾,如果 Optional 為 empty,直接返回 empty;如果 Optional 中存在值,則對值進行驗證,驗證通過返回原 Optional,驗證不通過返回 empty。

<code>     public Optional filter(Predicate super T> predicate) {        Objects.requireNonNull(predicate);        if (!isPresent())            return this;        else            return predicate.test(value) ? this : empty();    }/<code>

4. Lambda 下模式的進化

設計模式是人們熟悉的一種設計思路,他是軟件架構中解決通用問題的模板,將解決特定問題的最佳實踐固定下來,但設計模式本身會比較複雜,包含多個接口、若干個實現類,應用過程相對繁瑣,這也是影響其應用的原因之一。

Lambda 表達式大大簡化了 Java 中行為傳遞的問題,對於很多行為式設計模式而言,減少了不少構建成本。

4.1 命令模式

命令者是一個對象,其封裝了調用另一個方法的實現細節,命令者模式使用該對象可以編寫根據運行時條件,順序調用方法的一般性代碼。

大多數命令模式中的命令對象,其實是一種行為的封裝,甚至是對其他對象內部行為的一種適配,這種情況下,Lambda 表達式並有了用武之地。

<code>    interface Command{        void act();    }    interface Editor{        void open();        void write(String data);        void save();    }    class CommandRunner{        private List<command> commands = new ArrayList<>();        public void run(Command command){            command.act();            this.commands.add(command);        }        public void redo(){            this.commands.forEach(Command::act);        }    }    class OpenCommand implements Command{        private final Editor editor;        OpenCommand(Editor editor) {            this.editor = editor;        }        @Override        public void act() {            this.editor.open();        }    }    class WriteCommand implements Command{        private final Editor editor;        private final String data;        WriteCommand(Editor editor, String data) {            this.editor = editor;            this.data = data;        }        @Override        public void act() {            editor.write(this.data);        }    }    class SaveCommand implements Command{        private final Editor editor;        SaveCommand(Editor editor) {            this.editor = editor;        }        @Override        public void act() {            this.editor.save();        }    }    public void useCommand(){        CommandRunner commandRunner = new CommandRunner();        Editor editor = new EditorImpl();        String data1 = "data1";        String data2 = "data2";        commandRunner.run(new OpenCommand(editor));        commandRunner.run(new WriteCommand(editor, data1));        commandRunner.run(new WriteCommand(editor, data2));        commandRunner.run(new SaveCommand(editor));    }    public void useLambda(){        CommandRunner commandRunner = new CommandRunner();        Editor editor = new EditorImpl();        String data1 = "data1";        String data2 = "data2";        commandRunner.run(()->editor.open());        commandRunner.run(()->editor.write(data1));        commandRunner.run(()->editor.write(data2));        commandRunner.run(()->editor.save());    }    class EditorImpl implements Editor{        @Override        public void open() {        }        @Override        public void write(String data) {        }        @Override        public void save() {        }    }/<command>/<code>

從代碼中可見,Lambda 表達式的應用,減少了創建子類的負擔,增加了代碼的靈活性。

4.2 策略模式

策略模式能夠在運行時改變軟件的算法行為,其核心的實現思路是,使用不同的算法來解決同一個問題,然後將這些算法封裝在一個統一的接口背後。

可見策略模式也是一種行為行為傳遞的模式。

「Java 進階」--Lambda & 函數式編程

<code>    interface CompressionStrategy{        OutputStream compress(OutputStream outputStream) throws IOException;    }    class GzipBasedCompressionStrategy implements CompressionStrategy{        @Override        public OutputStream compress(OutputStream outputStream) throws IOException {            return new GZIPOutputStream(outputStream);        }    }    class ZipBasedCompressionStrategy implements CompressionStrategy{        @Override        public OutputStream compress(OutputStream outputStream) throws IOException {            return new ZipOutputStream(outputStream);        }    }    class Compressor{        private final CompressionStrategy compressionStrategy;        Compressor(CompressionStrategy compressionStrategy) {            this.compressionStrategy = compressionStrategy;        }        public void compress(Path inFile, File outFile) throws IOException {            try (OutputStream outputStream = new FileOutputStream(outFile)){                Files.copy(inFile, this.compressionStrategy.compress(outputStream));            }        }    }    {        Compressor gzipCompressor = new Compressor(new GzipBasedCompressionStrategy());        gzipCompressor.compress(in,out);        Compressor ziCompressor = new Compressor(new ZipBasedCompressionStrategy());        ziCompressor.compress(in,out);    }    {        Compressor gzipCompressor = new Compressor(GZIPOutputStream::new);        gzipCompressor.compress(in,out);        Compressor ziCompressor = new Compressor(ZipOutputStream::new);        ziCompressor.compress(in,out);    }/<code>

4.3 觀察者模式

觀察者模式中,被觀察者持有觀察者的一個列表,當被觀察者的狀態發送變化時,會通知觀察者。

對於一個觀察者來說,往往是對一個行為的封裝。

<code>    interface NameObserver{        void onNameChange(String oName, String nName);    }    @Data    class User {        private final List<nameobserver> nameObservers = new ArrayList<>();        @Setter(AccessLevel.PRIVATE)        private String name;        public void updateName(String nName){            String oName = getName();            setName(nName);            nameObservers.forEach(nameObserver -> nameObserver.onNameChange(oName, nName));        }        public void addObserver(NameObserver nameObserver){            this.nameObservers.add(nameObserver);        }    }    class LoggerNameObserver implements NameObserver{        @Override        public void onNameChange(String oName, String nName) {            System.out.println(String.format("old Name is %s, new Name is %s", oName, nName));        }    }    class NameChangeNoticeObserver implements NameObserver{        @Override        public void onNameChange(String oName, String nName) {            notic.send(String.format("old Name is %s, new Name is %s", oName, nName));        }    }    {        User user = new User();        user.addObserver(new LoggerNameObserver());        user.addObserver(new NameChangeNoticeObserver());        user.updateName("張三");    }    {        User user = new User();        user.addObserver((oName, nName) ->                System.out.println(String.format("old Name is %s, new Name is %s", oName, nName)));        user.addObserver((oName, nName) ->                notic.send(String.format("old Name is %s, new Name is %s", oName, nName)));        user.updateName("張三");    }/<nameobserver>/<code>

4.4 模板方法模式

模板方法將整體算法設計成一個抽象類,他有一系列的抽象方法,代表方法中可被定製的步驟,同時這個類中包含一些通用代碼,算法的每一個變種都由具體的類實現,他們重新抽象方法,提供相應的實現。

模板方法,實際是行為的一種整合,內部大量用到行為的傳遞。 先看一個標準的模板方法:

<code>    interface UserChecker{        void check(User user);    }    abstract class AbstractUserChecker implements UserChecker{        @Override        public final void check(User user){            checkName(user);            checkAge(user);        }        abstract void checkName(User user);        abstract void checkAge(User user);    }    class SimpleUserChecker extends AbstractUserChecker {        @Override        void checkName(User user) {            Preconditions.checkArgument(StringUtils.isNotEmpty(user.getName()));        }        @Override        void checkAge(User user) {            Preconditions.checkArgument(user.getAge() != null);            Preconditions.checkArgument(user.getAge().intValue() > 0);            Preconditions.checkArgument(user.getAge().intValue() < 150);        }    }    {        UserChecker userChecker = new SimpleUserChecker();        userChecker.check(new User());    }    class LambdaBaseUserChecker implements UserChecker{        private final List<consumer>> userCheckers = Lists.newArrayList();        public LambdaBaseUserChecker(List<consumer>>userCheckers){            this.userCheckers.addAll(userCheckers);        }        @Override        public void check(User user){            this.userCheckers.forEach(userConsumer -> userConsumer.accept(user));        }    }    {        UserChecker userChecker = new LambdaBaseUserChecker(Arrays.asList(                user -> Preconditions.checkArgument(StringUtils.isNotEmpty(user.getName())),                user -> Preconditions.checkArgument(user.getAge() != null),                user -> Preconditions.checkArgument(user.getAge().intValue() > 0),                user -> Preconditions.checkArgument(user.getAge().intValue() < 150)        ));        userChecker.check(new User());    }    @Data    class User{        private String name;        private Integer age;    }/<consumer>/<consumer>/<code>

在看一個 Spring JdbcTemplate,如果使用 Lambda 進行簡化:

<code>    public JdbcTemplate jdbcTemplate;    public User getUserById(Integer id){        return jdbcTemplate.query("select id, name, age from tb_user where id = ?", new PreparedStatementSetter() {            @Override            public void setValues(PreparedStatement preparedStatement) throws SQLException {                preparedStatement.setInt(1, id);            }        }, new ResultSetExtractor<user>() {            @Override            public User extractData(ResultSet resultSet) throws SQLException, DataAccessException {                User user = new User();                user.setId(resultSet.getInt("id"));                user.setName(resultSet.getString("name"));                user.setAge(resultSet.getInt("age"));                return user;            }        });    }    public User getUserByIdLambda(Integer id){        return jdbcTemplate.query("select id, name, age from tb_user where id = ?",                preparedStatement -> preparedStatement.setInt(1, id),                resultSet -> {                    User user = new User();                    user.setId(resultSet.getInt("id"));                    user.setName(resultSet.getString("name"));                    user.setAge(resultSet.getInt("age"));                    return user;                });    }    @Data    class User {        private Integer id;        private String name;        private Integer age;    }/<user>/<code>

5. Lambda 下併發程序

併發與並行:

  • 併發是兩個任務共享時間段,並行是兩個任務同一時間發生。
  • 並行化是指為了縮短任務執行的時間,將任務分解為幾個部分,然後並行執行,這和順序執行的工作量是一樣的,區別是多個 CPU 一起來幹活,花費的時間自然減少了。
  • 數據並行化。數據並行化是指將數據分為塊,為每塊數據分配獨立的處理單元。
「Java 進階」--Lambda & 函數式編程

5.1 並行化流操作

並行化流操作是 Stream 提供的一個特性,只需改變一個方法調用,就可以讓其擁有並行操作的能力。

如果已經存在一個 Stream 對象,調用他的 parallel 方法就能讓其並行執行。

如果已經存在一個集合,調用 parallelStream 方法就能獲取一個擁有並行執行能力的 Stream。

並行流主要解決如何高效使用多核 CPU 的事情。

<code>    @Data    class Account{        private String name;        private boolean active;        private Integer amount;    }    public int getActiveAmount(List<account> accounts){        return accounts.parallelStream()                .filter(account -> account.isActive())                .mapToInt(account -> account.getAmount())                .sum();    }    public int getActiveAmount2(List<account> accounts){        return accounts.stream()                .parallel()                .filter(account -> account.isActive())                .mapToInt(Account::getAmount)                .sum();    }/<account>/<account>/<code>

並行流底層使用 fork/join 框架,fork 遞歸式的分解問題,然後每個段並行執行,最終有 join 合併結果,返回最後的值。

「Java 進階」--Lambda & 函數式編程

5.2 阻塞 IO VS 非阻塞 IO

BIO VS NIO

BIO 阻塞式 IO,是一種通用且容易理解的方式,與程序交互時通常都符合這種順序執行的方式,但其主要的缺陷在於每個 socket 會綁定一個 Thread 進行操作,當長鏈過多時會消耗大量的 Server 資源,從而導致其擴展性性下降。

NIO 非阻塞 IO,一般指的是 IO 多路複用,可以使用一個線程同時對多個 socket 的讀寫進行監控,從而使用少量線程服務於大量 Socket。

由於客戶端開發的簡便性,大多數的驅動都是基於 BIO 實現,包括 MySQL、Redis、Mongo 等;在服務器端,由於其高性能的要求,基本上是 NIO 的天下,以最大限度的提升系統的可擴展性。

由於客戶端存在大量的 BIO 操作,我們的客戶端線程會不停的被 BIO 阻塞,以等待操作返回值,因此線程的效率會大打折扣。

「Java 進階」--Lambda & 函數式編程

如上圖,線程在 IO 與 CPU 之間不停切換,走走停停,同時線程也沒有辦法釋放,一直等到任務完成。

5.3 Future

構建併發操作的另一種方案便是 Future,Future 是一種憑證,調用方法不是直接返回值,而是返回一個 Future 對象,剛創建的 Future 為一個空對象,由後臺線程執行耗時操作,並在結束時將結果寫回到 Future 中。

當調用 Future 對象的 get 方法獲取值時,會有兩個可能,如果後臺線程已經運行完成,則直接返回;如果後臺線程沒有運行完成,則阻塞調用線程,知道後臺線程運行完成或超時。

使用 Future 方式,可以以並行的方式運行多個子任務。

當主線程需要調用比較耗時的操作時,可以將其放在輔助線程中執行,並在需要數據的時候從 future 中獲取,如果輔助線程已經運行完成,則立即拿到返回的結果,如果輔助線程還沒有運行完成,則主線程等待,並在完成時獲取結果。

「Java 進階」--Lambda & 函數式編程

一種常見的場景是在Controller中從多個Service中獲取結果,並將其封裝成一個View對象返回給前端用於顯示,假設需要從三個接口中獲取結果,每個接口的平均響應時間是20ms,那按照串行模式,總耗時為sum(i1, i2, i3) = 60ms;如果按照Future併發模式將加載任務交由輔助線程處理,總耗時為max(i1, i2, i3 ) = 20ms, 大大減少了系統的響應時間。

<code>     private ExecutorService executorService = Executors.newFixedThreadPool(20);    private User loadUserByUid(Long uid){       sleep(20);       return new User();    }    private Address loadAddressByUid(Long uid){        sleep(20);        return new Address();    }    private Account loadAccountByUid(Long uid){        sleep(20);        return new Account();    }    /**     * 總耗時 sum(LoadUser, LoadAddress, LoadAccount) = 60ms     * @param uid     * @return     */    public View getViewByUid1(Long uid){        User user = loadUserByUid(uid);        Address address = loadAddressByUid(uid);        Account account = loadAccountByUid(uid);        View view = new View();        view.setUser(user);        view.setAddress(address);        view.setAccount(account);        return view;    }    /**     * 總耗時 max(LoadUser, LoadAddress, LoadAccount) = 20ms     * @param uid     * @return     * @throws ExecutionException     * @throws InterruptedException     */    public View getViewByUid(Long uid) throws ExecutionException, InterruptedException {        Future<user> userFuture = executorService.submit(()->loadUserByUid(uid));        Future<address> addressFuture = executorService.submit(()->loadAddressByUid(uid));        Future<account> accountFuture = executorService.submit(()->loadAccountByUid(uid));        View view = new View();        view.setUser(userFuture.get());        view.setAddress(addressFuture.get());        view.setAccount(accountFuture.get());        return view;    }    private void sleep(long time){        try {            TimeUnit.MILLISECONDS.sleep(time);        } catch (InterruptedException e) {            e.printStackTrace();        }    }    @Data    class View{        private User user;        private Address address;        private Account account;    }    class User{    }    class Address{    }    class Account{    }/<account>/<address>/<user>/<code>

Future 方式存在一個問題,及在調用 get 方法時會阻塞主線程,這是資源的極大浪費,我們真正需要的是一種不必調用 get 方法阻塞當前線程,就可以操作 future 對象返回的結果。

上例中只是子任務能夠拆分並能並行執行的一種典型案例,在實際開發過程中,我們會遇到更多、更復雜的場景,比如:

  • 將兩個 Future 結果合併成一個,同時第二個又依賴於第一個的結果
  • 等待 Future 集合中所有記錄的完成
  • 等待 Future 集合中的最快的任務完成
  • 定義任務完成後的操作

對此,我們引入了 CompletableFuture 對象。

5.4 CompletableFuture

  • CompletableFuture 結合了 Future 和回調兩種策略,以更好的處理事件驅動任務。
  • CompletableFuture 與Stream 的設計思路一致,通過註冊 Lambda 表達式,把高階函數鏈接起來,從而定製更復雜的處理流程。

CompletableFuture 提供了一組函數用於定義流程,其中包括:

5.4.1 創建函數

CompletableFuture 提供了一組靜態方法用於創建 CompletableFuture 實例:

<code>public static  CompletableFuture completedFuture(U value)// :使用已經創建好的值,創建 CompletableFuture 對象。public static CompletableFuture<void> runAsync(Runnable runnable)// 基於 Runnable 創建 CompletableFuture 對象,返回值為 Void,及沒有返回值public static CompletableFuture<void> runAsync(Runnable runnable, Executor executor)// 基於 Runnable 和自定義線程池創建 CompletableFuture 對象,返回值為 Void,及沒有返回值public static  CompletableFuture supplyAsync(Supplier supplier)// 基於 Supplier 創建 CompletableFuture 對象,返回值為 Upublic static  CompletableFuture supplyAsync(Supplier supplier, Executor executor)   // 基於 Supplier 和自定義線程池創建 CompletableFuture 對象,返回值為 U 
/<void>/<void>
/<code>

以 Async 結尾並且沒有指定 Executor 的方法會使用 ForkJoinPool.commonPool() 作為它的線程池執行異步代碼。

方法的參數類型都是函數式接口,所以可以使用 Lambda 表達式實現異步任務。

5.4.2 計算結果完成後

當 CompletableFuture 計算完成或者計算過程中拋出異常時進行回調。

<code>public CompletableFuture whenComplete(BiConsumer super T,? super Throwable> action)public CompletableFuture whenCompleteAsync(BiConsumer super T,? super Throwable> action)public CompletableFuture whenCompleteAsync(BiConsumer super T,? super Throwable> action, Executor executor)public CompletableFuture     exceptionally(Function<throwable> fn)/<throwable>/<code>

Action 的類型是 BiConsumer super T,? super Throwable> 它可以處理正常的計算結果,或者異常情況。

方法不以 Async 結尾,意味著 Action 使用相同的線程執行,而 Async 可能會使用其他線程執行(如果是使用相同的線程池,也可能會被同一個線程選中執行)。

exceptionally 針對異常情況進行處理,當原始的 CompletableFuture 拋出異常的時候,就會觸發這個 CompletableFuture 的計算。

下面一組方法雖然也返回 CompletableFuture 對象,但是對象的值和原來的 CompletableFuture 計算的值不同。當原先的 CompletableFuture 的值計算完成或者拋出異常的時候,會觸發這個 CompletableFuture 對象的計算,結果由 BiFunction 參數計算而得。因此這組方法兼有 whenComplete 和轉換的兩個功能。

<code>public  CompletableFuture handle(BiFunction super T,Throwable,? extends U> fn)public  CompletableFuture handleAsync(BiFunction super T,Throwable,? extends U> fn)public  CompletableFuture handleAsync(BiFunction super T,Throwable,? extends U> fn, Executor executor)/<code>
5.4.3 轉化函數

轉化函數類似於 Stream 中的惰性求助函數,主要對 CompletableFuture 的中間結果進行流程定製。

<code>public  CompletableFuture thenApply(Function super T,? extends U> fn)public  CompletableFuture thenApplyAsync(Function super T,? extends U> fn)public  CompletableFuture thenApplyAsync(Function super T,? extends U> fn, Executor executor)/<code>

通過函數完成對 CompletableFuture 中的值得轉化,Async 在線的線程池中處理,Executor 可以自定義線程池。

5.4.4 純消費函數

上面的方法當計算完成的時候,會生成新的計算結果 (thenApply, handle),或者返回同樣的計算結果 whenComplete,CompletableFuture 還提供了一種處理結果的方法,只對結果執行 Action,而不返回新的計算值,因此計算值為 Void。

<code>public CompletableFuture<void> thenAccept(Consumer super T> action)public CompletableFuture<void> thenAcceptAsync(Consumer super T> action)public CompletableFuture<void> thenAcceptAsync(Consumer super T> action, Executor executor)/<void>/<void>/<void>/<code>

其他的參數類型與之前的含義一致,不同的是函數接口 Consumer,這個接口只有輸入,沒有返回值。

thenAcceptBoth 以及相關方法提供了類似的功能,當兩個 CompletionStage 都正常完成計算的時候,就會執行提供的 action,它用來組合另外一個異步的結果。

<code>public  CompletableFuture<void> thenAcceptBoth(CompletionStage   extends U> other, BiConsumer super T,? super U> action)    public    CompletableFuture<void> thenAcceptBothAsync(CompletionStage extends   U> other, BiConsumer super T,? super U> action)    public    CompletableFuture<void> thenAcceptBothAsync(CompletionStage extends   U> other, BiConsumer super T,? super U> action, Executor executor)/<void>/<void>/<void>/<code>
5.4.5. 組合函數

組合函數主要應用於後續計算需要 CompletableFuture 計算結果的場景。

<code>public  CompletableFuture thenCompose(Function super T,? extends CompletionStage> fn)public  CompletableFuture thenComposeAsync(Function super T,? extends CompletionStage> fn)public  
CompletableFuture thenComposeAsync(Function super T,? extends CompletionStage> fn, Executor executor)
/<code>

這一組方法接受一個 Function 作為參數,這個 Function 的輸入是當前的 CompletableFuture 的計算值,返回結果將是一個新的 CompletableFuture,這個新的 CompletableFuture 會組合原來的 CompletableFuture 和函數返回的 CompletableFuture。因此它的功能類似:

<code>A +–> B +—> C /<code>

下面的一組方法 thenCombine 用來複合另外一個 CompletionStage 的結果。兩個 CompletionStage 是並行執行的,它們之間並沒有先後依賴順序,other 並不會等待先前的 CompletableFuture 執行完畢後再執行,當兩個 CompletionStage 全部執行完成後,統一調用 BiFunction 函數,計算最終的結果。

<code>public  CompletableFuture thenCombine(CompletionStage   extends U> other, BiFunction super T,? super U,? extends V> fn)public  CompletableFuture thenCombineAsync(CompletionStage   extends U> other, BiFunction super T,? super U,? extends V> fn)public  CompletableFuture thenCombineAsync(CompletionStage   extends U> other, BiFunction super T,? super U,? extends V> fn,   Executor executor)/<code>
5.4.6. Either

Either 系列方法不會等兩個 CompletableFuture 都計算完成後執行計算,而是當任意一個 CompletableFuture 計算完成的時候就會執行。

<code>public CompletableFuture<void> acceptEither(CompletionStage extends T> other, Consumer super T> action)public CompletableFuture<void> acceptEitherAsync(CompletionStage extends T> other, Consumer super T> action)public CompletableFuture<void> acceptEitherAsync(CompletionStage extends T> other, Consumer super T> action, Executor executor)public  CompletableFuture applyToEither(CompletionStage extends T> other, Function super T,U> fn)public  CompletableFuture applyToEitherAsync(CompletionStage extends T> other, Function super T,U> fn)public  CompletableFuture applyToEitherAsync(CompletionStage extends T> other, Function super T,U> fn, Executor executor)/<void>/<void>/<void>/<code>
5.4.7 輔助方法

輔助方法主要指 allOf 和 anyOf,這兩個靜態方法用於組合多個 CompletableFuture。

<code>public static CompletableFuture<void> allOf(CompletableFuture>... cfs)// allOf方法是當所有的CompletableFuture都執行完後執行計算。public static CompletableFuture<object> anyOf(CompletableFuture>... cfs)// anyOf方法是當任意一個CompletableFuture執行完後就會執行計算。/<object>/<void>/<code>


分享到:


相關文章: