上一篇教學討論了一些基本 Java Lambda 的語法和 Functional Interface 的概念,大家可能已經蠢蠢欲動,建立了一些 Functional Interface 並加上 @FunctionalInterface Annotation,正準備應用到新的項目上。

但我得要提醒大家,其實在大多數情況之下 (90% 以上),您是無須建立一個專門的 Functional Interface,因為 Java 8 很貼心,已經 pre-define 了一堆 built-in 的 Functional Interface 給大家隨意使用。

Java Lambda – java.util.function Package

Java Lambda 的設計原意是想大家用 Function (函數)的思維來設計和開發程序,而函數大致不外乎以下幾種形式:

Function Name Description
Predicate Evaluate a single input argument against the condition and return a boolean value
Consumer Accept a single input argument, perform some processing and return no result
Supplier Accept no input and produce (return) a result
Function Accept a single input argument, perform some processing and return one result
BiConsumer Accept 2 input arguments, perform some processing and return no result
BiFunction Accept 2 input arguments, perform some processing and return one result
UnaryOperator Special version of Function, in which the input argument and return result are of the same type
BinaryOperator Special version of BiFunction, in which the input arguments and return result are of the same type

有見及此,Java 8 的 java.util.function 這個 package 就已經定義了以上的函數,並提供了主要兩大種類的 Functional Interface。

  1. Primitive-typed:例如 IntPredicate、DoubleSupplier、LongConsumer 等。
  2. Generic:例如 Predicate<T>、Function<T, R>、BiConsumer<T, U>、UnaryOperator<T> 等。

Primitive-typed Functional Interface

舉一個例子說明,如果我們有一個 integer list,想找出並列印所有大於 10 的整數,我們首先寫一個 processIntList 的 method,如下:

public static void processIntList(List intList, 
        IntPredicate matcher, IntConsumer action) {
    if (intList != null) {
        for (int value : intList) {
            // SAM of IntPredicate is boolean test(int value)
            if (matcher.test(value)) {
            // SAM of IntConsumer is void accept(int value)
                action.accept(value);
            }
        }
    }
}

然後,我們就可以利用 Lambda Expression 去 invoke processIntList 逹成我們想要的效果,如下

// number -> number > 0 is the lambda for boolean IntPredicate.test(int value);
// number -> System.out.println(number) is the lambda for void IntConsumer.accept(int value);
processIntList(intList, number -> number > 10, number -> System.out.println(number));

如果,我們對程序的要求有所改變,例如現在我想把 intList 內的雙數用 log4j logger 輸出,我們可以完全不須要改變 processIntList 中的邏輯,只須調整我們的 Lambda Expression 就可以了,其改變如下

processIntList(intList, number -> number%2 == 0, number -> logger.info(number));

由上例可見,Lambda 給予我們編程的便利,我們可以把部份程序邏輯用 abstract function 來取代,給予 Method Caller 用 Lambda Expression 來提供實際執行的函數邏輯 ( function logic )。

Generics Functional Interface

如果我們要處理的 Object 並不是 Integer、Long、Double 這些 Primitive type,那怎麼辦呢?
不用擔心, java.util.function package 內的 Generics Functional Interface 足夠應付我們所有 Custom Java Type 的需求。
假設現在我們要把所有 IT architect 的人工加 10% ,我們首先設計我們的 adjustSalary method 如下

public static <T> void adjustSalary(List<T> staffList, Predicate<T> matcher, Consumer<T> action) {
    if (staffList != null) {
        for (T obj : staffList) {
            if (matcher.test(obj)) {
                action.accept(obj);
            }
        }
    }
}

您會發現 adjustSalary 的結構,除了換上了 Generics 外,基本上跟早前的 processIntList 沒有大分別,如要加 IT Architect 的人工,就得要配上以下的代碼:

adjustSalary(staffList, staff -> staff.getJobRole().equals("IT Architect"), 
    staff -> staff.setSalary(staff.getSalary() * 1.1));

Staff 這個 Java Type 會自動讓 JVM Type inference 並附上 Java Lambda Expression中,但前提當然是 staffList 要是一個 List<Staff> 的實例 ( Instance ),讓 JVM 有足夠的資訊去推斷 Lambda 中的 staff argument 其實是 Staff 的實例。

通過以上的例子,相信大家應該明白如何運用 java.util.function package 內的 built-in functional interface 。我建議大家在設計自己的 functional interface ,先看看 java.util.function 其實有沒有現成可用的 interface 並加以利用。因為使用 java.util.function 中的 built-in interface 有以下的好處

  1. 您無須額外建立一個 Java interface
  2. java.util.function interface 一定無可能改變其 functional interface 的用途
  3. java.util.function interface 是 Java 8 的標準,其設計原意和用途大家已有共識 ( commonly-understood ),方便日後技術人員溝通

下表是一個簡單的總結各種 built-in functional interface 及其 Single Abstract Method (SAM) 的 method signature

Function Name Single Abstract Method Signature
Predicate<T> boolean test(T value)
Consumer<T> void accept(T value)
Supplier<T> T get()
Function<T, R> R apply(T value)
BiConsumer<T, U> void accept(T value1, U value2)
BiFunction<T, U, R> R apply(T value1, U value2)
UnaryOperator<T> T apply(T value)
BinaryOperator<T> T apply(T value1, T value 2)

Java Collection 改進

如果閣下已經是 Java Lambda 的高手,可能早已察覺我給予的例子並不是十分可取,原因是 processIntList 和 adjustSalary 根本不必要的,完全可以用 Java 8 中 Collection API 取代。

例如,processIntList 可以這樣完全用以下的 Collections API 取代:

intList.stream().filter(number -> number > 10).forEach(number -> System.out.println(number));

而 adjustSalary 則可以這樣完全被取代:

staffList.stream().filter(staff -> staff.getJobRole().equals("IT Architect"))
    .forEach(staff -> staff.setSalary(staff.getSalary() * 1.1));

無錯,但為了讓大家慢慢明白 Java Lambda ,我只好循序漸進地解釋,更何況上述涉及 Java Stream,又是另一大課題,唯有留待日後教學章節再行討論。

不過,除了 Stream 外,Java Collection API 為了 Java Lambda 亦加入了一些改進,例如

Newly added API Description
void forEach(Consumer<? super T> action) Equivalent to
for (T t : this)
action.accept(t);
boolean removeIf(Predicate<? super E> filter) Each element in the collection is evaluated against the given predicate filter. It the filter predicate is matched, the element is removed from the collection
Stream<E> stream() Return a sequential Stream with this collection as its source. To be discussed in latter tutorial
default Stream<E> parallelStream() Return a possibly parallel Stream with this collection as its source.

Gang Of Four – Template Method Design Pattern

最後,我想用我的個人見解作為這章的終結。
其實 Java Lambda 的設計並不是甚麼新的 Object-oriented software 的設計意念,其實早於 1995 年由 Gang Of Four 所著的 Design Patterns: Elements of Resuable Object-Oriented Software 中,已經有類似 Design Pattern。

Gang Of Four 的 Design Pattern 著作,多年來可以說是 Object-oriented software 的聖經,不少 Java Framework 如 Spring Framework、EJB 等都留下不少它的影子,而我覺得最能代表 Java Lambda 的,就是書中所提及的一種 behavioral pattern – Template Method

In software engineering, the template method pattern is a behavioral design pattern that defines the program skeleton of an algorithm in a method, called template method, which defers some steps to subclasses. It lets one redefine certain steps of an algorithm without changing the algorithm’s structure.

用上 adjustSalary 的例子,如果我們用 Template Method Design Pattern 來開發我們的程序,大可以先定義一個 Abstract Class 包括其主要的加薪邏輯,如下:

public abstract class AbstractSalaryAdjustment<T> {
    
    public final void adjustSalary(List<T> staffList) {
        if (staffList != null) {
            for (T obj : staffList) {
                if (eliglibleForAdjustment(obj)) {
                    adjust(obj);
                }
            }
        }   
    }
        
    protected abstract boolean eliglibleForAdjustment(T obj);
    
    protected abstract void adjust(T obj);
}

然後,我們就可以 extend 這個 Abstract Class 來提供加薪條件和幅度,如下:

public class ITArchitectSalaryAdjustment extends AbstractSalaryAdjustment<Staff> {

    @Override
    protected boolean eliglibleForAdjustment(Staff obj) {
        return obj.getJobRole().equals("IT Architect");
    }

    @Override
    protected void adjust(Staff obj) {
        obj.setSalary(obj.getSalary() * 1.1);
    }
}

這樣,我們就可以通過 ITArchitectSalaryAdjustment.adjustSalary(List<Staff> staffList) 來逹到相同的效果,由此可見,Java Lambda Expression 可以是說是讓我們更方便去實作 Template Method Design Pattern。

下篇再續,如有任何問題,請留言給我吧!謝謝!

Java Lambda 教學 Part 2
Tagged on:                 

2 thoughts on “Java Lambda 教學 Part 2

  • 22 6 月, 2018 at 2:46 下午
    Permalink

    Nice tutorial!
    Would like to know what company you are working for and what age are you.

    Reply

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

zh_HKChinese (Hong Kong)