主頁 > 後端開發 > Java 函式式編程

Java 函式式編程

2023-04-03 07:17:27 後端開發

概述

背景

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/548950.html

標籤:其他

上一篇:請撰寫一個程式,使用兩個執行緒分別輸出數字和字母,要求輸出的結果為:1A2B3C4D5E6F7G8H9I10J。

下一篇:2023 海外工具站 3 月復盤

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more