Java 函數式編程

来源:https://www.cnblogs.com/xiao2shiqi/archive/2023/04/02/17280618.html
-Advertisement-
Play Games

概述 背景 函數式編程的理論基礎是阿隆佐·丘奇(Alonzo Church)於 1930 年代提出的 λ 演算(Lambda Calculus)。λ 演算是一種形式系統,用於研究函數定義、函數應用和遞歸。它為計算理論和電腦科學的發展奠定了基礎。隨著 Haskell(1990年)和 Erlang(1 ...


概述

背景

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Java 8 中提供了許多函數式介面,包括Function、Consumer、Supplier、Predicate 等等。這 4 個介面就是本篇將要分享的內容 ...
  • SpringCloud基本介紹 SpringCloud官方文檔 1.提出問題 先思考一個問題,沒有微服務技術,是不是程式員就不能開發大型項目? 是可以的,對大型項目進行模塊劃分,對各個模塊進行實現。但模塊之間更多地是以API調用完成,耦合度較高,不利於拓展和維護(在沒有微服務技術時,很多大型項目就已 ...
  • 電腦網路基礎,考驗一個程式員的基本功,也能更快的篩選出更優秀的人才。 說說TCP的三次握手 假設發送端為客戶端,接收端為服務端。開始時客戶端和服務端的狀態都是CLOSED。 第一次握手:客戶端向服務端發起建立連接請求,客戶端會隨機生成一個起始序列號x,客戶端向服務端發送的欄位中包含標誌位SYN=1 ...
  • 如果一個BEAN類上加了@Transactional,則預設的該類及其子類的公開方法均會開啟事務,但有時某些業務場景下某些公開的方法可能並不需要事務,那這種情況該如何做呢? 常規的做法: 針對不同的場景及事務傳播特性,定義不同的公開方法【哪怕是同一種業務】,併在方法上添加@Transactional ...
  • 一言以蔽之:重寫 equals 方法是為了比較對象的內容是否相等,重寫 hashCode 方法是為了保證對象在哈希表等數據結構中的正確性。 1、在 Java 中,如果一個類重寫了 equals 方法,則必須同時重寫 hashCode 方法。這是因為在 Java 中,對象的 hashCode 值用於在 ...
  • “林子雨大數據” 實驗3 HBase操作與介面編程 環境搭建 VM虛擬機和Ubuntu系統的安裝 在Windows中使用VirtualBox安裝Ubuntu虛擬機(2020年7月版本)_廈大資料庫實驗室博客 (xmu.edu.cn) Hadoop安裝(偽分散式) Hadoop3.1.3安裝教程_單機 ...
  • 準備一些操作(Action)? 到目前為止,我們主要通過聲明欄位和視圖來構建模塊。在任何真實的業務場景中,我們都希望將一些業務邏輯鏈接到操作按鈕。在我們的房地產示例中,我們希望能夠: 取消或將房產設置為已售出 接受或拒絕報價 有人可能會說,我們已經可以通過手動更改狀態來完成這些事情,但這並不太方便。 ...
  • 3 月的碎碎念,大致總結了商業人生、付費軟體、創業方向選擇、創業感性還是理性、如何解決複雜問題及如何成長這幾個方面的內容。 商業人生 商業人生需要試錯能力和快速信息收集與驗證校準; 商業邏輯需要試錯能力,收集各種渠道信息後整理決策。快速信息收集和驗證校準很重要。 付費軟體 付費軟體產品可以依托大平臺 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...