概述 背景 函數式編程的理論基礎是阿隆佐·丘奇(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 表達式是使用最小可能語法編寫的函數定義:(原則)
- Lambda 表達式產生函數,而不是類
- 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
的函數式介面的基本使用基本準測,如下
- 只處理對象而非基本類型,名稱則為 Function,Consumer,Predicate 等,參數通過泛型添加
- 如果接收的參數是基本類型,則由名稱的第一部分表示,如 LongConsumer, DoubleFunction,IntPredicate 等
- 如果返回值為基本類型,則用 To 表示,如 ToLongFunction 和 IntToLongFunction
- 如果返回值類型與參數類型一致,則是一個運算符
- 如果接收兩個參數且返回值為布爾值,則是一個謂詞(Predicate)
- 如果接收的兩個參數類型不同,則名稱中有一個 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)其實很好理解,並且在函數式編程中非常常見,它有以下特點:
- 接收一個或多個函數作為參數
- 返回一個函數作為結果
先來看看一個函數如何返回一個函數:
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 中的巨大改進