一文帶你了解Java中的函數(shù)式編程
概述
背景
函數(shù)式編程的理論基礎(chǔ)是阿隆佐·丘奇(Alonzo Church)于 1930 年代提出的 λ 演算(Lambda Calculus)。λ 演算是一種形式系統(tǒng),用于研究函數(shù)定義、函數(shù)應(yīng)用和遞歸。它為計(jì)算理論和計(jì)算機(jī)科學(xué)的發(fā)展奠定了基礎(chǔ)。隨著 Haskell(1990年)和 Erlang(1986年)等新一代函數(shù)式編程語(yǔ)言的誕生,函數(shù)式編程開(kāi)始在實(shí)際應(yīng)用中發(fā)揮作用。
函數(shù)式的價(jià)值
隨著硬件越來(lái)越便宜,程序的規(guī)模和復(fù)雜性都在呈線性的增長(zhǎng)。這一切都讓編程工作變得困難重重。我們想方設(shè)法使代碼更加一致和易懂。我們急需一種 語(yǔ)法優(yōu)雅,簡(jiǎn)潔健壯,高并發(fā),易于測(cè)試和調(diào)試 的編程方式,這一切恰恰就是 函數(shù)式編程(FP) 的意義所在。
函數(shù)式語(yǔ)言已經(jīng)產(chǎn)生了優(yōu)雅的語(yǔ)法,這些語(yǔ)法對(duì)于非函數(shù)式語(yǔ)言也適用。 例如:如今 Python,Java 8 都在吸收 FP 的思想,并且將其融入其中,你也可以這樣想:
OO(object oriented,面向?qū)ο螅┦浅橄髷?shù)據(jù),F(xiàn)P(functional programming,函數(shù) 式編程)是抽象行為。
新舊對(duì)比
用傳統(tǒng)形式和 Java 8 的方法引用、Lambda 表達(dá)式分別演示。代碼示例:
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] 構(gòu)建默認(rèn)的 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 以前的匿名內(nèi)部類
public String approach(String msg) {
return msg.toUpperCase() + "!";
}
},
msg -> msg.substring(0, 5), // [3] 基于 Ldmbda 表達(dá)式,實(shí)例化 interface
Unrelated::twice // [4] 基于 方法引用,實(shí)例化 interface
};
Strategize s = new Strategize("Hello there");
s.communicate();
for(Strategy newStrategy : strategies) {
s.changeStrategy(newStrategy); // [5] 使用默認(rèn)的 Soft 策略
s.communicate(); // [6] 每次調(diào)用 communicate() 都會(huì)產(chǎn)生不同的行為
}
}
}
輸出結(jié)果:
hello there?
HELLO THERE!
Hello
Hello there Hello there
Lambda 表達(dá)式
Lambda 表達(dá)式是使用最小可能語(yǔ)法編寫的函數(shù)定義:(原則)
- Lambda 表達(dá)式產(chǎn)生函數(shù),而不是類
- Lambda 語(yǔ)法盡可能少,這正是為了使 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] 一個(gè)參數(shù)時(shí),可以不需要擴(kuò)展 (), 但這是一個(gè)特例
static Body bod2 = (h) -> h + " More details"; // [2] 正常情況下的使用方式
static Description desc = () -> "Short info"; // [3] 沒(méi)有參數(shù)的情況下的使用方式
static Multi mult = (h, n) -> h + n; // [4] 多參數(shù)情況下的使用方式
static Description moreLines = () -> {
// [5] 多行代碼情況下使用 `{}` + `return` 關(guān)鍵字
// (在單行的 Lambda 表達(dá)式中 `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());
}
}
輸出結(jié)果:
Oh! No Parens!
Hi! More details
Short info
Pi! 3.14159
moreLines()
from moreLines()
總結(jié):Lambda 表達(dá)式通常比匿名內(nèi)部類產(chǎn)生更易讀的代碼,因此我們將盡可能使用它們。
方法引用
方法引用由類名或者對(duì)象名,后面跟著 :: 然后跟方法名稱,
使用示例:
interface Callable { // [1] 單一方法的接口(重要)
void call(String s);
}
class Describe {
void show(String msg) { // [2] 符合 Callable 接口的 call() 方法實(shí)現(xiàn)
System.out.println(msg);
}
}
public class MethodReferences {
static void hello(String name) { // [3] 也符合 call() 方法實(shí)現(xiàn)
System.out.println("Hello, " + name);
}
static class Description {
String about;
Description(String desc) {
about = desc;
}
void help(String msg) { // [4] 靜態(tài)類的非靜態(tài)方法
System.out.println(about + " " + msg);
}
}
static class Helper {
static void assist(String msg) { // [5] 靜態(tài)類的靜態(tài)方法,符合 call() 方法
System.out.println(msg);
}
}
public static void main(String[] args) {
Describe d = new Describe();
Callable c = d::show; // [6] 通過(guò)方法引用創(chuàng)建 Callable 的接口實(shí)現(xiàn)
c.call("call()"); // [7] 通過(guò)該實(shí)例 call() 方法調(diào)用 show() 方法
c = MethodReferences::hello; // [8] 靜態(tài)方法的方法引用
c.call("Bob");
c = new Description("valuable")::help; // [9] 實(shí)例化對(duì)象的方法引用
c.call("information");
c = Helper::assist; // [10] 靜態(tài)方法的方法引用
c.call("Help!");
}
}
輸出結(jié)果:
call()
Hello, Bob
valuable information
Help!
Runnable 接口
使用 Lambda 和方法引用改變 Runnable 接口的寫法:
// 方法引用與 Runnable 接口的結(jié)合使用
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(); // 通過(guò) 方法引用創(chuàng)建 Runnable 實(shí)現(xiàn)的引用
}
}
輸出結(jié)果:
Anonymous
lambda
Go::go()
未綁定的方法引用
使用未綁定的引用時(shí),需要先提供對(duì)象:
// 未綁定的方法引用是指沒(méi)有關(guān)聯(lián)對(duì)象的普通方法
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] 你不能在沒(méi)有 X 對(duì)象參數(shù)的前提下調(diào)用 f(),因?yàn)樗?X 的方法
TransformX sp = X::f; // [2] 你可以首個(gè)參數(shù)是 X 對(duì)象參數(shù)的前提下調(diào)用 f(),使用未綁定的引用,函數(shù)式的方法不再與方法引用的簽名完全相同
X x = new X();
System.out.println(sp.transform(x)); // [3] 傳入 x 對(duì)象,調(diào)用 x.f() 方法
System.out.println(x.f()); // 同等效果
}
}
輸出結(jié)果:
X::f()
X::f()
我們通過(guò)更多示例來(lái)證明,通過(guò)未綁的方法引用和 interface 之間建立關(guān)聯(lián):
package com.github.xiao2shiqi.lambda;
// 未綁定的方法與多參數(shù)的結(jié)合運(yùn)用
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');
}
}
構(gòu)造函數(shù)引用
可以捕獲構(gòu)造函數(shù)的引用,然后通過(guò)引用構(gòu)建對(duì)象
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) {
// 通過(guò) ::new 關(guān)鍵字賦值給不同的接口,然后通過(guò) make() 構(gòu)建不同的實(shí)例
MakeNoArgs mna = Dog::new; // [1] 將構(gòu)造函數(shù)的引用交給 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);
}
}
總結(jié)
- 方法引用在很大程度上可以理解為創(chuàng)建一個(gè)函數(shù)式接口的實(shí)例
- 方法引用實(shí)際上是一種簡(jiǎn)化 Lambda 表達(dá)式的語(yǔ)法糖,它提供了一種更簡(jiǎn)潔的方式來(lái)創(chuàng)建一個(gè)函數(shù)式接口的實(shí)現(xiàn)
- 在代碼中使用方法引用時(shí),實(shí)際上是在創(chuàng)建一個(gè)匿名實(shí)現(xiàn)類,引用方法實(shí)現(xiàn)并且覆蓋了接口的抽象方法
- 方法引用大多用于創(chuàng)建函數(shù)式接口的實(shí)現(xiàn)
函數(shù)式接口
- Lambda 包含類型推導(dǎo)
- Java 8 引入
java.util.function包,解決類型推導(dǎo)的問(wèn)題
通過(guò)函數(shù)表達(dá)式創(chuàng)建 Interface:
// 使用 @FunctionalInterface 注解強(qiáng)制執(zhí)行此 “函數(shù)式方法” 模式
@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 沒(méi)有實(shí)現(xiàn) Functional 接口,所以不能直接賦值
// Functional fac = fa; // Incompatible ?
// 但可以通過(guò) Lambda 將函數(shù)賦值給接口 (類型需要匹配)
Functional f = fa::goodbye;
FunctionalNoAnn fna = fa::goodbye;
Functional fl = a -> "Goodbye, " + a;
FunctionalNoAnn fnal = a -> "Goodbye, " + a;
}
}
以上是自己創(chuàng)建 函數(shù)式接口的示例。
但在 java.util.function 包旨在創(chuàng)建一組完整的預(yù)定義接口,使得我們一般情況下不需再定義自己的接口。
在 java.util.function 的函數(shù)式接口的基本使用基本準(zhǔn)測(cè),如下
- 只處理對(duì)象而非基本類型,名稱則為 Function,Consumer,Predicate 等,參數(shù)通過(guò)泛型添加
- 如果接收的參數(shù)是基本類型,則由名稱的第一部分表示,如 LongConsumer, DoubleFunction,IntPredicate 等
- 如果返回值為基本類型,則用 To 表示,如 ToLongFunction 和 IntToLongFunction
- 如果返回值類型與參數(shù)類型一致,則是一個(gè)運(yùn)算符
- 如果接收兩個(gè)參數(shù)且返回值為布爾值,則是一個(gè)謂詞(Predicate)
- 如果接收的兩個(gè)參數(shù)類型不同,則名稱中有一個(gè) Bi
基本類型
下面枚舉了基于 Lambda 表達(dá)式的所有不同 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 {
// 根據(jù)不同參數(shù)獲得對(duì)象的函數(shù)表達(dá)式
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);
// 根據(jù)對(duì)象類型參數(shù),獲得基本數(shù)據(jù)類型返回值的函數(shù)表達(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);
// 基本類型的相互轉(zhuǎn)換
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);
}
}
以下是用表格整理基本類型相關(guān)的函數(shù)式接口:
| 函數(shù)式接口 | 特征 | 用途 | 方法名 |
|---|---|---|---|
| Function<T, R> | 接受一個(gè)參數(shù),返回一個(gè)結(jié)果 | 將輸入?yún)?shù)轉(zhuǎn)換成輸出結(jié)果,如數(shù)據(jù)轉(zhuǎn)換或映射操作 | R apply(T t) |
| IntFunction | 接受一個(gè) int 參數(shù),返回一個(gè)結(jié)果 | 將 int 值轉(zhuǎn)換成輸出結(jié)果 | R apply(int value) |
| LongFunction | 接受一個(gè) long 參數(shù),返回一個(gè)結(jié)果 | 將 long 值轉(zhuǎn)換成輸出結(jié)果 | R apply(long value) |
| DoubleFunction | 接受一個(gè) double 參數(shù),返回一個(gè)結(jié)果 | 將 double 值轉(zhuǎn)換成輸出結(jié)果 | R apply(double value) |
| ToIntFunction | 接受一個(gè)參數(shù),返回一個(gè) int 結(jié)果 | 將輸入?yún)?shù)轉(zhuǎn)換成 int 輸出結(jié)果 | int applyAsInt(T value) |
| ToLongFunction | 接受一個(gè)參數(shù),返回一個(gè) long 結(jié)果 | 將輸入?yún)?shù)轉(zhuǎn)換成 long 輸出結(jié)果 | long applyAsLong(T value) |
| ToDoubleFunction | 接受一個(gè)參數(shù),返回一個(gè) double 結(jié)果 | 將輸入?yún)?shù)轉(zhuǎn)換成 double 輸出結(jié)果 | double applyAsDouble(T value) |
| IntToLongFunction | 接受一個(gè) int 參數(shù),返回一個(gè) long 結(jié)果 | 將 int 值轉(zhuǎn)換成 long 輸出結(jié)果 | long applyAsLong(int value) |
| IntToDoubleFunction | 接受一個(gè) int 參數(shù),返回一個(gè) double 結(jié)果 | 將 int 值轉(zhuǎn)換成 double 輸出結(jié)果 | double applyAsDouble(int value) |
| LongToIntFunction | 接受一個(gè) long 參數(shù),返回一個(gè) int 結(jié)果 | 將 long 值轉(zhuǎn)換成 int 輸出結(jié)果 | int applyAsInt(long value) |
| LongToDoubleFunction | 接受一個(gè) long 參數(shù),返回一個(gè) double 結(jié)果 | 將 long 值轉(zhuǎn)換成 double 輸出結(jié)果 | double applyAsDouble(long value) |
| DoubleToIntFunction | 接受一個(gè) double 參數(shù),返回一個(gè) int 結(jié)果 | 將 double 值轉(zhuǎn)換成 int 輸出結(jié)果 | int applyAsInt(double value) |
| DoubleToLongFunction | 接受一個(gè) double 參數(shù),返回一個(gè) long 結(jié)果 | 將 double 值轉(zhuǎn)換成 long 輸出結(jié)果 | long applyAsLong(double value) |
非基本類型
在使用函數(shù)接口時(shí),名稱無(wú)關(guān)緊要——只要參數(shù)類型和返回類型相同。Java 會(huì)將你的方法映射到接口方法。示例:
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());
// 在使用函數(shù)接口時(shí),名稱無(wú)關(guān)緊要——只要參數(shù)類型和返回類型相同。Java 會(huì)將你的方法映射到接口方法。
bic = MethodConversion::someOtherName;
bic.accept(new In1(), new In2());
}
}
輸出結(jié)果:
accept()
someOtherName()
將方法引用應(yīng)用于基于類的函數(shù)式接口(即那些不包含基本類型的函數(shù)式接口)
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) {
// 無(wú)參數(shù),返回一個(gè)結(jié)果
Supplier<AA> s = ClassFunctionals::f1;
s.get();
// 比較兩個(gè)對(duì)象,用于排序和比較操作
Comparator<AA> c = ClassFunctionals::f2;
c.compare(new AA(), new AA());
// 執(zhí)行操作,通常是副作用操作,不需要返回結(jié)果
Consumer<AA> cons = ClassFunctionals::f3;
cons.accept(new AA());
// 執(zhí)行操作,通常是副作用操作,不需要返回結(jié)果,接受兩個(gè)參數(shù)
BiConsumer<AA, BB> bicons = ClassFunctionals::f4;
bicons.accept(new AA(), new BB());
// 將輸入?yún)?shù)轉(zhuǎn)換成輸出結(jié)果,如數(shù)據(jù)轉(zhuǎn)換或映射操作
Function<AA, CC> f = ClassFunctionals::f5;
CC cc = f.apply(new AA());
// 將兩個(gè)輸入?yún)?shù)轉(zhuǎn)換成輸出結(jié)果,如數(shù)據(jù)轉(zhuǎn)換或映射操作
BiFunction<AA, BB, CC> bif = ClassFunctionals::f6;
cc = bif.apply(new AA(), new BB());
// 接受一個(gè)參數(shù),返回 boolean 值: 測(cè)試參數(shù)是否滿足特定條件
Predicate<AA> p = ClassFunctionals::f7;
boolean result = p.test(new AA());
// 接受兩個(gè)參數(shù),返回 boolean 值,測(cè)試兩個(gè)參數(shù)是否滿足特定條件
BiPredicate<AA, BB> bip = ClassFunctionals::f8;
result = bip.test(new AA(), new BB());
// 接受一個(gè)參數(shù),返回一個(gè)相同類型的結(jié)果,對(duì)輸入執(zhí)行單一操作并返回相同類型的結(jié)果,是 Function 的特殊情況
UnaryOperator<AA> uo = ClassFunctionals::f9;
AA aa = uo.apply(new AA());
// 接受兩個(gè)相同類型的參數(shù),返回一個(gè)相同類型的結(jié)果,將兩個(gè)相同類型的值組合成一個(gè)新值,是 BiFunction 的特殊情況
BinaryOperator<AA> bo = ClassFunctionals::f10;
aa = bo.apply(new AA(), new AA());
}
}
以下是用表格整理的非基本類型的函數(shù)式接口:
| 函數(shù)式接口 | 特征 | 用途 | 方法名 |
|---|---|---|---|
| Supplier | 無(wú)參數(shù),返回一個(gè)結(jié)果 | 獲取值或?qū)嵗?,工廠模式,延遲計(jì)算 | T get() |
| Comparator | 接受兩個(gè)參數(shù),返回 int 值 | 比較兩個(gè)對(duì)象,用于排序和比較操作 | int compare(T o1, T o2) |
| Consumer | 接受一個(gè)參數(shù),無(wú)返回值 | 執(zhí)行操作,通常是副作用操作,不需要返回結(jié)果 | void accept(T t) |
| BiConsumer<T, U> | 接受兩個(gè)參數(shù),無(wú)返回值 | 執(zhí)行操作,通常是副作用操作,不需要返回結(jié)果,接受兩個(gè)參數(shù) | void accept(T t, U u) |
| Function<T, R> | 接受一個(gè)參數(shù),返回一個(gè)結(jié)果 | 將輸入?yún)?shù)轉(zhuǎn)換成輸出結(jié)果,如數(shù)據(jù)轉(zhuǎn)換或映射操作 | R apply(T t) |
| BiFunction<T, U, R> | 接受兩個(gè)參數(shù),返回一個(gè)結(jié)果 | 將兩個(gè)輸入?yún)?shù)轉(zhuǎn)換成輸出結(jié)果,如數(shù)據(jù)轉(zhuǎn)換或映射操作 | R apply(T t, U u) |
| Predicate | 接受一個(gè)參數(shù),返回 boolean 值 | 測(cè)試參數(shù)是否滿足特定條件 | boolean test(T t) |
| BiPredicate<T, U> | 接受兩個(gè)參數(shù),返回 boolean 值 | 測(cè)試兩個(gè)參數(shù)是否滿足特定條件 | boolean test(T t, U u) |
| UnaryOperator | 接受一個(gè)參數(shù),返回一個(gè)相同類型的結(jié)果 | 對(duì)輸入執(zhí)行單一操作并返回相同類型的結(jié)果,是 Function 的特殊情況 | T apply(T t) |
| BinaryOperator | 接受兩個(gè)相同類型的參數(shù),返回一個(gè)相同類型的結(jié)果 | 將兩個(gè)相同類型的值組合成一個(gè)新值,是 BiFunction 的特殊情況 | T apply(T t1, T t2) |
多參數(shù)函數(shù)式接口
java.util.functional 中的接口是有限的,如果需要 3 個(gè)參數(shù)函數(shù)的接口怎么辦?自己創(chuàng)建就可以了,如下:
// 創(chuàng)建處理 3 個(gè)參數(shù)的函數(shù)式接口
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
驗(yàn)證如下:
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 表達(dá)式
TriFunction<Integer, Long, Double, Integer> tf2 = (i, l, d) -> 12;
}
}
高階函數(shù)
高階函數(shù)(Higher-order Function)其實(shí)很好理解,并且在函數(shù)式編程中非常常見(jiàn),它有以下特點(diǎn):
- 接收一個(gè)或多個(gè)函數(shù)作為參數(shù)
- 返回一個(gè)函數(shù)作為結(jié)果
先來(lái)看看一個(gè)函數(shù)如何返回一個(gè)函數(shù):
import java.util.function.Function;
interface FuncSS extends Function<String, String> {} // [1] 使用繼承,輕松創(chuàng)建屬于自己的函數(shù)式接口
public class ProduceFunction {
// produce() 是一個(gè)高階函數(shù):既函數(shù)的消費(fèi)者,產(chǎn)生函數(shù)的函數(shù)
static FuncSS produce() {
return s -> s.toLowerCase(); // [2] 使用 Lambda 表達(dá)式,可以輕松地在方法中創(chuàng)建和返回一個(gè)函數(shù)
}
public static void main(String[] args) {
FuncSS funcSS = produce();
System.out.println(funcSS.apply("YELLING"));
}
}
然后再看看,如何接收一個(gè)函數(shù)作為函數(shù)的參數(shù):
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());
}
}
總之,高階函數(shù)使代碼更加簡(jiǎn)潔、靈活和可重用,常見(jiàn)于 Stream 流式編程中
閉包
在 Java 中,閉包通常與 lambda 表達(dá)式和匿名內(nèi)部類相關(guān)。簡(jiǎn)單來(lái)說(shuō),閉包允許在一個(gè)函數(shù)內(nèi)部訪問(wèn)和操作其外部作用域中的變量。在 Java 中的閉包實(shí)際上是一個(gè)特殊的對(duì)象,它封裝了一個(gè)函數(shù)及其相關(guān)的環(huán)境。這意味著閉包不僅僅是一個(gè)函數(shù),它還攜帶了一個(gè)執(zhí)行上下文,其中包括外部作用域中的變量。這使得閉包在訪問(wèn)這些變量時(shí)可以在不同的執(zhí)行上下文中保持它們的值。
讓我們通過(guò)一個(gè)例子來(lái)理解 Java 中的閉包:
public class ClosureExample {
public static void main(String[] args) {
int a = 10;
int b = 20;
// 這是一個(gè)閉包,因?yàn)樗东@了外部作用域中的變量 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(即在實(shí)際使用過(guò)程中保持不變)。這是為了防止在多線程環(huán)境中引起不可預(yù)測(cè)的行為和數(shù)據(jù)不一致。
函數(shù)組合
函數(shù)組合(Function Composition)意為 “多個(gè)函數(shù)組合成新函數(shù)”。它通常是函數(shù)式 編程的基本組成部分。
先看 Function 函數(shù)組合示例代碼:
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(),
// 重點(diǎn):使用函數(shù)組合將多個(gè)函數(shù)組合在一起
// compose 是先執(zhí)行參數(shù)中的函數(shù),再執(zhí)行調(diào)用者
// andThen 是先執(zhí)行調(diào)用者,再執(zhí)行參數(shù)中的函數(shù)
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(),它們的區(qū)別如下:
- compose 是先執(zhí)行參數(shù)中的函數(shù),再執(zhí)行調(diào)用者
- andThen 是先執(zhí)行調(diào)用者,再執(zhí)行參數(shù)中的函數(shù)
輸出結(jié)果:
AFTER ALL AMBULANCES
_fter _ll _mbul_nces
然后,再看一段 Predicate 的邏輯運(yùn)算演示代碼:
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); // 使用謂詞組合將多個(gè)謂詞組合在一起,negate 是取反,and 是與,or 是或
public static void main(String[] args) {
Stream.of("bar", "foobar", "foobaz", "fongopuckey")
.filter(p4)
.forEach(System.out::println);
}
}
p4 通過(guò)函數(shù)組合生成一個(gè)復(fù)雜的謂詞,最后應(yīng)用在 filter() 中:
- negate():取反值,內(nèi)容不包含 bar
- and(p2):長(zhǎng)度小于 5
- or(p3):或者包含 f3
輸出結(jié)果:
foobar
foobaz
在 java.util.function 中常用的支持函數(shù)組合的方法,大致如下:
| 函數(shù)式接口 | 方法名 | 描述 |
|---|---|---|
| Function<T, R> | andThen | 用于從左到右組合兩個(gè)函數(shù),即:h(x) = g(f(x)) |
| Function<T, R> | compose | 用于從右到左組合兩個(gè)函數(shù),即:h(x) = f(g(x)) |
| Consumer | andThen | 用于從左到右組合兩個(gè)消費(fèi)者,按順序執(zhí)行兩個(gè)消費(fèi)者操作 |
| Predicate | and | 用于組合兩個(gè)謂詞函數(shù),返回一個(gè)新的謂詞函數(shù),滿足兩個(gè)謂詞函數(shù)的條件 |
| Predicate | or | 用于組合兩個(gè)謂詞函數(shù),返回一個(gè)新的謂詞函數(shù),滿足其中一個(gè)謂詞函數(shù)的條件 |
| Predicate | negate | 用于對(duì)謂詞函數(shù)取反,返回一個(gè)新的謂詞函數(shù),滿足相反的條件 |
| UnaryOperator | andThen | 用于從左到右組合兩個(gè)一元操作符,即:h(x) = g(f(x)) |
| UnaryOperator | compose | 用于從右到左組合兩個(gè)一元操作符,即:h(x) = f(g(x)) |
| BinaryOperator | andThen | 用于從左到右組合兩個(gè)二元操作符,即:h(x, y) = g(f(x, y)) |
柯里化
柯里化(Currying)是函數(shù)式編程中的一種技術(shù),它將一個(gè)接受多個(gè)參數(shù)的函數(shù)轉(zhuǎn)換為一系列單參數(shù)函數(shù)。
讓我們通過(guò)一個(gè)簡(jiǎn)單的 Java 示例來(lái)理解柯里化:
public class CurryingAndPartials {
static String uncurried(String a, String b) {
return a + b;
}
public static void main(String[] args) {
// 柯里化的函數(shù),它是一個(gè)接受多參數(shù)的函數(shù)
Function<String, Function<String, String>> sum = a -> b -> a + b;
System.out.println(uncurried("Hi ", "Ho"));
// 通過(guò)鏈?zhǔn)秸{(diào)用逐個(gè)傳遞參數(shù)
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"));
}
}
輸出結(jié)果:
Hi Ho
Hi Ho
Hup Ho
Hup Hey
接下來(lái)我們添加層級(jí)來(lái)柯里化一個(gè)三參數(shù)函數(shù):
import java.util.function.Function;
public class Curry3Args {
public static void main(String[] args) {
// 柯里化函數(shù)
Function<String,
Function<String,
Function<String, String>>> sum = a -> b -> c -> a + b + c;
// 逐個(gè)傳遞參數(shù)
Function<String, Function<String, String>> hi = sum.apply("Hi ");
Function<String, String> ho = hi.apply("Ho ");
System.out.println(ho.apply("Hup"));
}
}
輸出結(jié)果:
Hi Ho Hup
在處理基本類型的時(shí)候,注意選擇合適的函數(shù)式接口:
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));
}
}
輸出結(jié)果:
9
總結(jié)
Lambda 表達(dá)式和方法引用并沒(méi)有將 Java 轉(zhuǎn)換成函數(shù)式語(yǔ)言,而是提供了對(duì)函數(shù)式編程的支持(Java 的歷史包袱太重了),這些特性滿足了很大一部分的、羨慕 Clojure 和 Scala 這類更函數(shù)化語(yǔ)言的 Java 程序員。阻止了他們投奔向那些語(yǔ)言(或者至少讓他們?cè)谕侗贾白龊脺?zhǔn)備)??傊?,Lambdas 和方法引用是 Java 8 中的巨大改進(jìn)
以上就是一文帶你了解Java中的函數(shù)式編程的詳細(xì)內(nèi)容,更多關(guān)于Java函數(shù)式編程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot返回值轉(zhuǎn)成JSONString的處理方式
這篇文章主要介紹了springboot返回值轉(zhuǎn)成JSONString的處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06
Java向Runnable線程傳遞參數(shù)方法實(shí)例解析
這篇文章主要介紹了Java向Runnable線程傳遞參數(shù)方法實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
Java之SpringCloud nocos注冊(cè)中心講解
這篇文章主要介紹了Java之SpringCloud nocos注冊(cè)中心講解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
Spring MVC---數(shù)據(jù)綁定和表單標(biāo)簽詳解
本篇文章主要介紹了Spring MVC---數(shù)據(jù)綁定和表單標(biāo)簽詳解,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01
詳解SpringBoot配置文件啟動(dòng)時(shí)動(dòng)態(tài)配置參數(shù)方法
這篇文章主要介紹了詳解SpringBoot配置文件啟動(dòng)時(shí)動(dòng)態(tài)配置參數(shù)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
SpringBoot項(xiàng)目中定時(shí)器的實(shí)現(xiàn)示例
在Spring?Boot項(xiàng)目中,你可以使用Spring框架提供的@Scheduled注解來(lái)編寫定時(shí)任務(wù),本文就來(lái)介紹一下SpringBoot項(xiàng)目中定時(shí)器的實(shí)現(xiàn),感興趣的可以了解一下2023-11-11
Java中Spock框架Mock對(duì)象的方法經(jīng)驗(yàn)總結(jié)
這篇文章主要分享了Spock框架Mock對(duì)象的方法經(jīng)驗(yàn)總結(jié),下文分享一些常用項(xiàng)目實(shí)戰(zhàn)說(shuō)明以及代碼,供大家項(xiàng)目中參考,也具有一的的參考價(jià)值,需要的小伙伴可以參考一下2022-02-02
springboot?vue測(cè)試列表遞歸查詢子節(jié)點(diǎn)下的接口功能實(shí)現(xiàn)
這篇文章主要為大家介紹了springboot?vue測(cè)試列表遞歸查詢子節(jié)點(diǎn)下的接口功能實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05

