Java 函數語言程式設計

2023-04-02 18:00:57

概述

背景

函數語言程式設計的理論基礎是阿隆佐·丘奇(Alonzo Church)於 1930 年代提出的 λ 演算(Lambda Calculus)。λ 演算是一種形式系統,用於研究函數定義、函數應用和遞迴。它為計算理論和電腦科學的發展奠定了基礎。隨著 Haskell(1990年)和 Erlang(1986年)等新一代函數語言程式設計語言的誕生,函數語言程式設計開始在實際應用中發揮作用。

函數式的價值

隨著硬體越來越便宜,程式的規模和複雜性都在呈線性的增長。這一切都讓程式設計工作變得困難重重。我們想方設法使程式碼更加一致和易懂。我們急需一種 語法優雅,簡潔健壯,高並行,易於測試和偵錯 的程式設計方式,這一切恰恰就是 函數語言程式設計(FP) 的意義所在。

函數式語言已經產生了優雅的語法,這些語法對於非函數式語言也適用。 例如:如今 Python,Java 8 都在吸收 FP 的思想,並且將其融入其中,你也可以這樣想:

OO(object oriented,物件導向)是抽象資料,FP(functional programming,函數 式程式設計)是抽象行為。

新舊對比

用傳統形式和 Java 8 的方法參照、Lambda 表示式分別演示。程式碼範例:

interface Strategy {
    String approach(String msg);
}

class Soft implements Strategy {
    public String approach(String msg) {
        return msg.toLowerCase() + "?";
    }
}

class Unrelated {
    static String twice(String msg) {
        return msg + " " + msg;
    }
}

public class Strategize {

    Strategy strategy;
    String msg;
    Strategize(String msg) {
        strategy = new Soft(); // [1] 構建預設的 Soft
        this.msg = msg;
    }

    void communicate() {
        System.out.println(strategy.approach(msg));
    }

    void changeStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public static void main(String[] args) {
        Strategy[] strategies = {
                new Strategy() { // [2] Java 8 以前的匿名內部類
                    public String approach(String msg) {
                        return msg.toUpperCase() + "!";
                    }
                },
                msg -> msg.substring(0, 5), // [3] 基於 Ldmbda 表示式,範例化 interface
                Unrelated::twice // [4] 基於 方法參照,範例化 interface
        };
        Strategize s = new Strategize("Hello there");
        s.communicate();
        for(Strategy newStrategy : strategies) {
            s.changeStrategy(newStrategy); // [5] 使用預設的 Soft 策略
            s.communicate(); // [6] 每次呼叫 communicate() 都會產生不同的行為
        }
    }
}

輸出結果:

hello there?
HELLO THERE!
Hello
Hello there Hello there

Lambda 表示式

Lambda 表示式是使用最小可能語法編寫的函數定義:(原則)

  1. Lambda 表示式產生函數,而不是類
  2. Lambda 語法儘可能少,這正是為了使 Lambda 易於編寫和使用

Lambda 用法:

interface Description {
    String brief();
}

interface Body {
    String detailed(String head);
}

interface Multi {
    String twoArg(String head, Double d);
}

public class LambdaExpressions {

    static Body bod = h -> h + " No Parens!"; // [1] 一個引數時,可以不需要擴充套件 (), 但這是一個特例
    static Body bod2 = (h) -> h + " More details"; // [2] 正常情況下的使用方式
    static Description desc = () -> "Short info"; // [3] 沒有引數的情況下的使用方式
    static Multi mult = (h, n) -> h + n; // [4] 多引數情況下的使用方式

    static Description moreLines = () -> { 
        // [5] 多行程式碼情況下使用 `{}` + `return` 關鍵字
        // (在單行的 Lambda 表示式中 `return` 是非法的)
        System.out.println("moreLines()");
        return "from moreLines()";
    };

    public static void main(String[] args) {
        System.out.println(bod.detailed("Oh!"));
        System.out.println(bod2.detailed("Hi!"));
        System.out.println(desc.brief());
        System.out.println(mult.twoArg("Pi! ", 3.14159));
        System.out.println(moreLines.brief());
    }
}

輸出結果:

Oh! No Parens!
Hi! More details
Short info
Pi! 3.14159
moreLines()
from moreLines()

總結:Lambda 表示式通常比匿名內部類產生更易讀的程式碼,因此我們將盡可能使用它們。

方法參照

方法參照由類名或者物件名,後面跟著 :: 然後跟方法名稱,

使用範例:

interface Callable { // [1] 單一方法的介面(重要)
    void call(String s);
}

class Describe {
    void show(String msg) { // [2] 符合 Callable 介面的 call() 方法實現
        System.out.println(msg);
    }
}

public class MethodReferences {
    static void hello(String name) { // [3] 也符合 call() 方法實現
        System.out.println("Hello, " + name);
    }

    static class Description {
        String about;

        Description(String desc) {
            about = desc;
        }

        void help(String msg) { // [4] 靜態類的非靜態方法
            System.out.println(about + " " + msg);
        }
    }

    static class Helper {
        static void assist(String msg) { // [5] 靜態類的靜態方法,符合 call() 方法
            System.out.println(msg);
        }
    }

    public static void main(String[] args) {
        Describe d = new Describe();
        Callable c = d::show; // [6] 通過方法參照建立 Callable 的介面實現
        c.call("call()"); // [7] 通過該範例 call() 方法呼叫 show() 方法

        c = MethodReferences::hello; // [8] 靜態方法的方法參照
        c.call("Bob");

        c = new Description("valuable")::help; // [9] 範例化物件的方法參照
        c.call("information");

        c = Helper::assist; // [10] 靜態方法的方法參照
        c.call("Help!");
    }
}

輸出結果:

call()
Hello, Bob
valuable information
Help!

Runnable 介面

使用 Lambda 和方法參照改變 Runnable 介面的寫法:

// 方法參照與 Runnable 介面的結合使用

class Go {
    static void go() {
        System.out.println("Go::go()");
    }
}

public class RunnableMethodReference {

    public static void main(String[] args) {

        new Thread(new Runnable() {
            public void run() {
                System.out.println("Anonymous");
            }
        }).start();

        new Thread(
                () -> System.out.println("lambda")
        ).start();

        new Thread(Go::go).start();		// 通過 方法參照建立 Runnable 實現的參照
    }
}

輸出結果:

Anonymous
lambda
Go::go()

未繫結的方法參照

使用未繫結的參照時,需要先提供物件:

// 未繫結的方法參照是指沒有關聯物件的普通方法
class X {
    String f() {
        return "X::f()";
    }
}

interface MakeString {
    String make();
}

interface TransformX {
    String transform(X x);
}

public class UnboundMethodReference {

    public static void main(String[] args) {
        // MakeString sp = X::f;       // [1] 你不能在沒有 X 物件引數的前提下呼叫 f(),因為它是 X 的方法
        TransformX sp = X::f;       // [2] 你可以首個引數是 X 物件引數的前提下呼叫 f(),使用未繫結的參照,函數式的方法不再與方法參照的簽名完全相同
        X x = new X();
        System.out.println(sp.transform(x));      // [3] 傳入 x 物件,呼叫 x.f() 方法
        System.out.println(x.f());      // 同等效果
    }
}

輸出結果:

X::f()
X::f()

我們通過更多範例來證明,通過未綁的方法參照和 interface 之間建立關聯:

package com.github.xiao2shiqi.lambda;

// 未繫結的方法與多引數的結合運用
class This {
    void two(int i, double d) {}
    void three(int i, double d, String s) {}
    void four(int i, double d, String s, char c) {}
}
interface TwoArgs {
    void call2(This athis, int i, double d);
}
interface ThreeArgs {
    void call3(This athis, int i, double d, String s);
}
interface FourArgs {
    void call4(
            This athis, int i, double d, String s, char c);
}

public class MultiUnbound {

    public static void main(String[] args) {
        TwoArgs twoargs = This::two;
        ThreeArgs threeargs = This::three;
        FourArgs fourargs = This::four;
        This athis = new This();
        twoargs.call2(athis, 11, 3.14);
        threeargs.call3(athis, 11, 3.14, "Three");
        fourargs.call4(athis, 11, 3.14, "Four", 'Z');
    }
}

建構函式參照

可以捕獲建構函式的參照,然後通過參照構建物件

class Dog {
    String name;
    int age = -1; // For "unknown"
    Dog() { name = "stray"; }
    Dog(String nm) { name = nm; }
    Dog(String nm, int yrs) {
        name = nm;
        age = yrs;
    }
}

interface MakeNoArgs {
    Dog make();
}

interface Make1Arg {
    Dog make(String nm);
}

interface Make2Args {
    Dog make(String nm, int age);
}

public class CtorReference {
    public static void main(String[] args) {
        // 通過 ::new 關鍵字賦值給不同的介面,然後通過 make() 構建不同的範例
        MakeNoArgs mna = Dog::new; // [1] 將建構函式的參照交給 MakeNoArgs 介面
        Make1Arg m1a = Dog::new; // [2] …………
        Make2Args m2a = Dog::new; // [3] …………
        Dog dn = mna.make();
        Dog d1 = m1a.make("Comet");
        Dog d2 = m2a.make("Ralph", 4);
    }
}

總結

  • 方法參照在很大程度上可以理解為建立一個函數式介面的範例
  • 方法參照實際上是一種簡化 Lambda 表示式的語法糖,它提供了一種更簡潔的方式來建立一個函數式介面的實現
  • 在程式碼中使用方法參照時,實際上是在建立一個匿名實現類,參照方法實現並且覆蓋了介面的抽象方法
  • 方法參照大多用於建立函數式介面的實現

函數式介面

  • Lambda 包含型別推導
  • Java 8 引入 java.util.function 包,解決型別推導的問題

通過函數表示式建立 Interface:

// 使用 @FunctionalInterface 註解強制執行此 「函數式方法」 模式
@FunctionalInterface
interface Functional {
    String goodbye(String arg);
}

interface FunctionalNoAnn {
    String goodbye(String arg);
}

public class FunctionalAnnotation {
    // goodbye
    public String goodbye(String arg) {
        return "Goodbye, " + arg + "!";
    }

    public static void main(String[] args) {
        FunctionalAnnotation fa = new FunctionalAnnotation();

        // FunctionalAnnotation 沒有實現 Functional 介面,所以不能直接賦值
//        Functional fac = fa;      // Incompatible ?

        // 但可以通過 Lambda 將函數賦值給介面 (型別需要匹配)
        Functional f = fa::goodbye;
        FunctionalNoAnn fna = fa::goodbye;
        Functional fl = a -> "Goodbye, " + a;
        FunctionalNoAnn fnal = a -> "Goodbye, " + a;
    }
}

以上是自己建立 函數式介面的範例。

但在 java.util.function 包旨在建立一組完整的預定義介面,使得我們一般情況下不需再定義自己的介面。

java.util.function 的函數式介面的基本使用基本準測,如下

  1. 只處理物件而非基本型別,名稱則為 Function,Consumer,Predicate 等,引數通過泛型新增
  2. 如果接收的引數是基本型別,則由名稱的第一部分表示,如 LongConsumer, DoubleFunction,IntPredicate 等
  3. 如果返回值為基本型別,則用 To 表示,如 ToLongFunction 和 IntToLongFunction
  4. 如果返回值型別與引數型別一致,則是一個運運算元
  5. 如果接收兩個引數且返回值為布林值,則是一個謂詞(Predicate)
  6. 如果接收的兩個引數型別不同,則名稱中有一個 Bi

基本型別

下面列舉了基於 Lambda 表示式的所有不同 Function 變體的範例:

class Foo {}

class Bar {
    Foo f;
    Bar(Foo f) { this.f = f; }
}

class IBaz {
    int i;
    IBaz(int i) { this.i = i; }
}

class LBaz {
    long l;
    LBaz(long l) { this.l = l; }
}

class DBaz {
    double d;
    DBaz(double d) { this.d = d; }
}

public class FunctionVariants {
    // 根據不同引數獲得物件的函數表示式
    static Function<Foo, Bar> f1 = f -> new Bar(f);
    static IntFunction<IBaz> f2 = i -> new IBaz(i);
    static LongFunction<LBaz> f3 = l -> new LBaz(l);
    static DoubleFunction<DBaz> f4 = d -> new DBaz(d);
    // 根據物件型別引數,獲得基本資料型別返回值的函數表示式
    static ToIntFunction<IBaz> f5 = ib -> ib.i;
    static ToLongFunction<LBaz> f6 = lb -> lb.l;
    static ToDoubleFunction<DBaz> f7 = db -> db.d;
    static IntToLongFunction f8 = i -> i;
    static IntToDoubleFunction f9 = i -> i;
    static LongToIntFunction f10 = l -> (int)l;
    static LongToDoubleFunction f11 = l -> l;
    static DoubleToIntFunction f12 = d -> (int)d;
    static DoubleToLongFunction f13 = d -> (long)d;

    public static void main(String[] args) {
        // apply usage examples
        Bar b = f1.apply(new Foo());
        IBaz ib = f2.apply(11);
        LBaz lb = f3.apply(11);
        DBaz db = f4.apply(11);

        // applyAs* usage examples
        int i = f5.applyAsInt(ib);
        long l = f6.applyAsLong(lb);
        double d = f7.applyAsDouble(db);

        // 基本型別的相互轉換
        long applyAsLong = f8.applyAsLong(12);
        double applyAsDouble = f9.applyAsDouble(12);
        int applyAsInt = f10.applyAsInt(12);
        double applyAsDouble1 = f11.applyAsDouble(12);
        int applyAsInt1 = f12.applyAsInt(13.0);
        long applyAsLong1 = f13.applyAsLong(13.0);
    }
}

以下是用表格整理基本型別相關的函數式介面:

函數式介面 特徵 用途 方法名
Function<T, R> 接受一個引數,返回一個結果 將輸入引數轉換成輸出結果,如資料轉換或對映操作 R apply(T t)
IntFunction 接受一個 int 引數,返回一個結果 將 int 值轉換成輸出結果 R apply(int value)
LongFunction 接受一個 long 引數,返回一個結果 將 long 值轉換成輸出結果 R apply(long value)
DoubleFunction 接受一個 double 引數,返回一個結果 將 double 值轉換成輸出結果 R apply(double value)
ToIntFunction 接受一個引數,返回一個 int 結果 將輸入引數轉換成 int 輸出結果 int applyAsInt(T value)
ToLongFunction 接受一個引數,返回一個 long 結果 將輸入引數轉換成 long 輸出結果 long applyAsLong(T value)
ToDoubleFunction 接受一個引數,返回一個 double 結果 將輸入引數轉換成 double 輸出結果 double applyAsDouble(T value)
IntToLongFunction 接受一個 int 引數,返回一個 long 結果 將 int 值轉換成 long 輸出結果 long applyAsLong(int value)
IntToDoubleFunction 接受一個 int 引數,返回一個 double 結果 將 int 值轉換成 double 輸出結果 double applyAsDouble(int value)
LongToIntFunction 接受一個 long 引數,返回一個 int 結果 將 long 值轉換成 int 輸出結果 int applyAsInt(long value)
LongToDoubleFunction 接受一個 long 引數,返回一個 double 結果 將 long 值轉換成 double 輸出結果 double applyAsDouble(long value)
DoubleToIntFunction 接受一個 double 引數,返回一個 int 結果 將 double 值轉換成 int 輸出結果 int applyAsInt(double value)
DoubleToLongFunction 接受一個 double 引數,返回一個 long 結果 將 double 值轉換成 long 輸出結果 long applyAsLong(double value)

非基本型別

在使用函數介面時,名稱無關緊要——只要引數型別和返回型別相同。Java 會將你的方法對映到介面方法。範例:

import java.util.function.BiConsumer;

class In1 {}
class In2 {}

public class MethodConversion {

    static void accept(In1 in1, In2 in2) {
        System.out.println("accept()");
    }

    static void someOtherName(In1 in1, In2 in2) {
        System.out.println("someOtherName()");
    }

    public static void main(String[] args) {
        BiConsumer<In1, In2> bic;

        bic = MethodConversion::accept;
        bic.accept(new In1(), new In2());

        // 在使用函數介面時,名稱無關緊要——只要引數型別和返回型別相同。Java 會將你的方法對映到介面方法。
        bic = MethodConversion::someOtherName;
        bic.accept(new In1(), new In2());
    }
}

輸出結果:

accept()
someOtherName()

將方法參照應用於基於類的函數式介面(即那些不包含基本型別的函數式介面)

import java.util.Comparator;
import java.util.function.*;

class AA {}
class BB {}
class CC {}

public class ClassFunctionals {

    static AA f1() { return new AA(); }
    static int f2(AA aa1, AA aa2) { return 1; }
    static void f3 (AA aa) {}
    static void f4 (AA aa, BB bb) {}
    static CC f5 (AA aa) { return new CC(); }
    static CC f6 (AA aa, BB bb) { return new CC(); }
    static boolean f7 (AA aa) { return true; }
    static boolean f8 (AA aa, BB bb) { return true; }
    static AA f9 (AA aa) { return new AA(); }
    static AA f10 (AA aa, AA bb) { return new AA(); }

    public static void main(String[] args) {
        // 無引數,返回一個結果
        Supplier<AA> s = ClassFunctionals::f1;
        s.get();
        // 比較兩個物件,用於排序和比較操作
        Comparator<AA> c = ClassFunctionals::f2;
        c.compare(new AA(), new AA());
        // 執行操作,通常是副作用操作,不需要返回結果
        Consumer<AA> cons = ClassFunctionals::f3;
        cons.accept(new AA());
        // 執行操作,通常是副作用操作,不需要返回結果,接受兩個引數
        BiConsumer<AA, BB> bicons = ClassFunctionals::f4;
        bicons.accept(new AA(), new BB());
        // 將輸入引數轉換成輸出結果,如資料轉換或對映操作
        Function<AA, CC> f = ClassFunctionals::f5;
        CC cc = f.apply(new AA());
        // 將兩個輸入引數轉換成輸出結果,如資料轉換或對映操作
        BiFunction<AA, BB, CC> bif = ClassFunctionals::f6;
        cc = bif.apply(new AA(), new BB());
        // 接受一個引數,返回 boolean 值: 測試引數是否滿足特定條件
        Predicate<AA> p = ClassFunctionals::f7;
        boolean result = p.test(new AA());
        // 接受兩個引數,返回 boolean 值,測試兩個引數是否滿足特定條件
        BiPredicate<AA, BB> bip = ClassFunctionals::f8;
        result = bip.test(new AA(), new BB());
        // 接受一個引數,返回一個相同型別的結果,對輸入執行單一操作並返回相同型別的結果,是 Function 的特殊情況
        UnaryOperator<AA> uo = ClassFunctionals::f9;
        AA aa = uo.apply(new AA());
        // 接受兩個相同型別的引數,返回一個相同型別的結果,將兩個相同型別的值組合成一個新值,是 BiFunction 的特殊情況
        BinaryOperator<AA> bo = ClassFunctionals::f10;
        aa = bo.apply(new AA(), new AA());
    }
}

以下是用表格整理的非基本型別的函數式介面:

函數式介面 特徵 用途 方法名
Supplier 無引數,返回一個結果 獲取值或範例,工廠模式,延遲計算 T get()
Comparator 接受兩個引數,返回 int 值 比較兩個物件,用於排序和比較操作 int compare(T o1, T o2)
Consumer 接受一個引數,無返回值 執行操作,通常是副作用操作,不需要返回結果 void accept(T t)
BiConsumer<T, U> 接受兩個引數,無返回值 執行操作,通常是副作用操作,不需要返回結果,接受兩個引數 void accept(T t, U u)
Function<T, R> 接受一個引數,返回一個結果 將輸入引數轉換成輸出結果,如資料轉換或對映操作 R apply(T t)
BiFunction<T, U, R> 接受兩個引數,返回一個結果 將兩個輸入引數轉換成輸出結果,如資料轉換或對映操作 R apply(T t, U u)
Predicate 接受一個引數,返回 boolean 值 測試引數是否滿足特定條件 boolean test(T t)
BiPredicate<T, U> 接受兩個引數,返回 boolean 值 測試兩個引數是否滿足特定條件 boolean test(T t, U u)
UnaryOperator 接受一個引數,返回一個相同型別的結果 對輸入執行單一操作並返回相同型別的結果,是 Function 的特殊情況 T apply(T t)
BinaryOperator 接受兩個相同型別的引數,返回一個相同型別的結果 將兩個相同型別的值組合成一個新值,是 BiFunction 的特殊情況 T apply(T t1, T t2)

多引數函數式介面

java.util.functional 中的介面是有限的,如果需要 3 個引數函數的介面怎麼辦?自己建立就可以了,如下:

// 建立處理 3 個引數的函數式介面
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    
    R apply(T t, U u, V v);
}

驗證如下:

public class TriFunctionTest {
    static int f(int i, long l, double d) { return 99; }

    public static void main(String[] args) {
        // 方法參照
        TriFunction<Integer, Long, Double, Integer> tf1 = TriFunctionTest::f;
        // Lamdba 表示式
        TriFunction<Integer, Long, Double, Integer> tf2 = (i, l, d) -> 12;
    }
}

高階函數

高階函數(Higher-order Function)其實很好理解,並且在函數語言程式設計中非常常見,它有以下特點:

  1. 接收一個或多個函數作為引數
  2. 返回一個函數作為結果

先來看看一個函數如何返回一個函數:

import java.util.function.Function;

interface FuncSS extends Function<String, String> {}        // [1] 使用繼承,輕鬆建立屬於自己的函數式介面

public class ProduceFunction {
    // produce() 是一個高階函數:既函數的消費者,產生函數的函數
    static FuncSS produce() {
        return s -> s.toLowerCase();    // [2] 使用 Lambda 表示式,可以輕鬆地在方法中建立和返回一個函數
    }

    public static void main(String[] args) {
        FuncSS funcSS = produce();
        System.out.println(funcSS.apply("YELLING"));
    }
}

然後再看看,如何接收一個函數作為函數的引數:

class One {}
class Two {}

public class ConsumeFunction {
    static Two consume(Function<One, Two> onetwo) {
        return onetwo.apply(new One());
    }

    public static void main(String[] args) {
        Two two = consume(one -> new Two());
    }
}

總之,高階函數使程式碼更加簡潔、靈活和可重用,常見於 Stream 流式程式設計中

閉包

在 Java 中,閉包通常與 lambda 表示式和匿名內部類相關。簡單來說,閉包允許在一個函數內部存取和操作其外部作用域中的變數。在 Java 中的閉包實際上是一個特殊的物件,它封裝了一個函數及其相關的環境。這意味著閉包不僅僅是一個函數,它還攜帶了一個執行上下文,其中包括外部作用域中的變數。這使得閉包在存取這些變數時可以在不同的執行上下文中保持它們的值。

讓我們通過一個例子來理解 Java 中的閉包:

public class ClosureExample {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;

        // 這是一個閉包,因為它捕獲了外部作用域中的變數 a 和 b
        IntBinaryOperator closure = (x, y) -> x * a + y * b;

        int result = closure.applyAsInt(3, 4);
        System.out.println("Result: " + result); // 輸出 "Result: 110"
    }
}

需要注意的是,在 Java 中,閉包捕獲的外部變數必須是 final 或者是有效的 final(即在實際使用過程中保持不變)。這是為了防止在多執行緒環境中引起不可預測的行為和資料不一致。

函陣列合

函陣列合(Function Composition)意為 「多個函陣列合成新函數」。它通常是函數式 程式設計的基本組成部分。

先看 Function 函陣列合範例程式碼:

import java.util.function.Function;

public class FunctionComposition {
    static Function<String, String> f1 = s -> {
        System.out.println(s);
        return s.replace('A', '_');
    },
    f2 = s -> s.substring(3),
    f3 = s -> s.toLowerCase(),
    // 重點:使用函陣列合將多個函陣列合在一起
    // compose 是先執行引數中的函數,再執行呼叫者
    // andThen 是先執行呼叫者,再執行引數中的函數
    f4 = f1.compose(f2).andThen(f3);        

    public static void main(String[] args) {
        String s = f4.apply("GO AFTER ALL AMBULANCES");
        System.out.println(s);
    }
}

程式碼範例使用了 Function 裡的 compose() 和 andThen(),它們的區別如下:

  • compose 是先執行引數中的函數,再執行呼叫者
  • andThen 是先執行呼叫者,再執行引數中的函數

輸出結果:

AFTER ALL AMBULANCES
_fter _ll _mbul_nces

然後,再看一段 Predicate 的邏輯運算演示程式碼:

public class PredicateComposition {
    static Predicate<String>
            p1 = s -> s.contains("bar"),
            p2 = s -> s.length() < 5,
            p3 = s -> s.contains("foo"),
            p4 = p1.negate().and(p2).or(p3);    // 使用謂詞組合將多個謂詞組合在一起,negate 是取反,and 是與,or 是或

    public static void main(String[] args) {
        Stream.of("bar", "foobar", "foobaz", "fongopuckey")
                .filter(p4)
                .forEach(System.out::println);
    }
}

p4 通過函陣列合生成一個複雜的謂詞,最後應用在 filter() 中:

  • negate():取反值,內容不包含 bar
  • and(p2):長度小於 5
  • or(p3):或者包含 f3

輸出結果:

foobar
foobaz

java.util.function 中常用的支援函陣列合的方法,大致如下:

函數式介面 方法名 描述
Function<T, R> andThen 用於從左到右組合兩個函數,即:h(x) = g(f(x))
Function<T, R> compose 用於從右到左組合兩個函數,即:h(x) = f(g(x))
Consumer andThen 用於從左到右組合兩個消費者,按順序執行兩個消費者操作
Predicate and 用於組合兩個謂詞函數,返回一個新的謂詞函數,滿足兩個謂詞函數的條件
Predicate or 用於組合兩個謂詞函數,返回一個新的謂詞函數,滿足其中一個謂詞函數的條件
Predicate negate 用於對謂詞函數取反,返回一個新的謂詞函數,滿足相反的條件
UnaryOperator andThen 用於從左到右組合兩個一元操作符,即:h(x) = g(f(x))
UnaryOperator compose 用於從右到左組合兩個一元操作符,即:h(x) = f(g(x))
BinaryOperator andThen 用於從左到右組合兩個二元操作符,即:h(x, y) = g(f(x, y))

柯里化

柯里化(Currying)是函數語言程式設計中的一種技術,它將一個接受多個引數的函數轉換為一系列單引數函數。

讓我們通過一個簡單的 Java 範例來理解柯里化:

public class CurryingAndPartials {
    static String uncurried(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {
        // 柯里化的函數,它是一個接受多引數的函數
        Function<String, Function<String, String>> sum = a -> b -> a + b;
        System.out.println(uncurried("Hi ", "Ho"));

        // 通過鏈式呼叫逐個傳遞引數
        Function<String, String> hi = sum.apply("Hi ");
        System.out.println(hi.apply("Ho"));

        Function<String, String> sumHi = sum.apply("Hup ");
        System.out.println(sumHi.apply("Ho"));
        System.out.println(sumHi.apply("Hey"));
    }
}

輸出結果:

Hi Ho
Hi Ho
Hup Ho
Hup Hey

接下來我們新增層級來柯里化一個三引數函數:

import java.util.function.Function;

public class Curry3Args {
    public static void main(String[] args) {
        // 柯里化函數
        Function<String,
                Function<String,
                        Function<String, String>>> sum = a -> b -> c -> a + b + c;

        // 逐個傳遞引數
        Function<String, Function<String, String>> hi = sum.apply("Hi ");
        Function<String, String> ho = hi.apply("Ho ");
        System.out.println(ho.apply("Hup"));
    }
}

輸出結果:

Hi Ho Hup

在處理基本型別的時候,注意選擇合適的函數式介面:

import java.util.function.IntFunction;
import java.util.function.IntUnaryOperator;

public class CurriedIntAdd {
    public static void main(String[] args) {
        IntFunction<IntUnaryOperator> curriedIntAdd = a -> b -> a + b;
        IntUnaryOperator add4 = curriedIntAdd.apply(4);
        System.out.println(add4.applyAsInt(5));
    }
}

輸出結果:

9

總結

Lambda 表示式和方法參照並沒有將 Java 轉換成函數式語言,而是提供了對函數語言程式設計的支援(Java 的歷史包袱太重了),這些特性滿足了很大一部分的、羨慕 Clojure 和 Scala 這類更函數化語言的 Java 程式設計師。阻止了他們投奔向那些語言(或者至少讓他們在投奔之前做好準備)。總之,Lambdas 和方法參照是 Java 8 中的巨大改進