Java 函數語言程式設計

小二十七發表於2023-04-02

概述

背景

函數語言程式設計的理論基礎是阿隆佐·丘奇(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 中的巨大改進

相關文章