函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许你以函数式编程。
这就是函数式编程(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。
OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为。
需要提醒大家的是,函数式语言背后有很多动机,这意味着描述它们可能会有些混淆。它通常取决于各种观点:为“并行编程”,“代码可靠性”和“代码创建和库复用”。 关于函数式编程能高效创建更健壮的代码这一观点仍存在部分争议。
通常,传递给方法的数据不同,结果不同。如果我们希望方法在调用时行为不同,该怎么做呢?结论是:只要能将代码传递给方法,我们就可以控制它的行为。
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;
public Strategize(String msg) {
strategy = new Soft(); // [1]
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]
@Override
public String approach(String msg) {
return msg.toUpperCase() + "!";
}
},
msg -> msg.substring(0, 5), // [3]
Unrelated::twice // [4]
};
Strategize s = new Strategize("Hello there");
s.communicate();
for (Strategy newStrategy : strategies) {
s.changeStrategy(newStrategy); // [5]
s.communicate(); // [6]
}
}
}
输出:
hello there?
HELLO THERE!
Hello
Hello there Hello there
[1] 在 Strategize 中,Soft 作为默认策略,在构造函数中赋值。
[2] 一种略显简短且更自发的方法是创建一个匿名内部类。即使这样,仍有相当数量的冗余代码。
[3] Java 8 的 Lambda 表达式。由箭头 -> 分隔开参数和函数体,箭头左边是参数,箭头右侧是从Lambda 返回的表达式,即函数体。这实现了与定义类、匿名内部类相同的效果,但代码少得多。
[4] Java 8 的方法引用,由 :: 区分。在 :: 的左边是类或对象的名称,在 :: 的右边是方法的名称,但没有参数列表。
[5] 在使用默认的 Soft strategy 之后,我们逐步遍历数组中的所有 Strategy,并使用 changeStrategy() 方法将每个 Strategy 放入 变量 s 中。
[6] 现在,每次调用 communicate() 都会产生不同的行为,具体取决于此刻正在使用的策略代码对象。我们传递的是行为,而非仅数据。
在 Java 8 之前,我们能够通过 [1] 和 [2] 的方式传递功能。然而,这种语法的读写非常笨拙,并且我们别无选择。方法引用和 Lambda 表达式的出现让我们可以在需要时传递功能,而不是仅在必要才这么做。
Lambda 表达式是使用最小可能语法编写的函数定义:
Lambda 表达式产生函数,而不是类。 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是 一个类,因此在幕后执行各种操作使 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 {
// PS:当只用一个参数,可以不需要括号 () 。
static Body body = h -> h + " No Parens!";
// PS:正常情况使用括号 () 包裹参数。
static Body body2 = (h) -> h + " More details";
// PS:如果没有参数,则必须使用括号 () 表示空参数列表
static Description desc = () -> "Short info";
// PS:对于多个参数,将参数列表放在括号 () 中
static Multi multi = (h, n) -> h + n;
// PS:如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。
// 在这种情况下,就需要使用 return
// Lambda 表达式方法体若是单行,使用 return 是非法的
static Description moreLines = () -> {
System.out.println("moreLines()");
return "from moreLines()";
};
public static void main(String[] args) {
System.out.println(body.detailed("Oh!"));
System.out.println(body2.detailed("Hi!"));
System.out.println(desc.brief());
System.out.println(multi.twoArg("Pi! ", 3.14159));
System.out.println(moreLines.brief());
}
}
递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。
interface IntCall {
int call(int arg);
}
public class RecursiveFactorial {
// PS: fact 是一个静态变量
static IntCall fact;
public static void main(String[] args) {
fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
for (int i = 0; i < 10; i++) {
System.out.println(fact.call(i));
}
}
}
输出:
1
1
2
6
24
120
720
5040
40320
362880
所有递归函数都有==“停止条件”==,否则将无限递归并产生异常。
方法引用组成:类名或对象名,后面跟 :: ,然后跟方法名称。
interface Callable {
void call(String s);
}
class Describe {
void show(String msg) {
System.out.println(msg);
}
}
public class MethodReferences {
static void hello(String name) {
System.out.println("hello " + name);
}
static class Description {
String about;
public Description(String desc) {
this.about = desc;
}
void help(String msg) {
System.out.println(about + " " + msg);
}
}
static class Helper {
static void assist(String msg) {
System.out.println(msg);
}
}
public static void main(String[] args) {
Describe d = new Describe();
// PS:这里是将 d 的 show() 赋值给了 Callable,它没有 show() 方法,而是 call() 方法。
// 能成功的原因是因为 show() 的签名(参数类型和返回类型)符合 Callable 的 call() 的签名
Callable c = d::show;
// PS:这里虽然执行的是 call() 但是J ava 将 call() 映射到 show()
c.call("call()");
// PS:下面的例子成功也是因为方法签名一样
c = MethodReferences::hello;
c.call("Bob");
c = new Description("valuable")::help;
c.call("information");
c = Helper::assist;
c.call("Help!");
}
}
输出:
call()
hello Bob
valuable information
Help!
Runnable 符合特殊的单方法接口格式:它的方法 run() 不带参数,也没有返回值。因此,可以使用 Lambda 表达式和方法引用作为 Runnable:
class Go {
static void go() {
System.out.println("Go::go()");
}
}
public class RunnableMethodReference {
public static void main(String[] args) {
// PS:方式1,匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Anonymous");
}
}).start();
// PS:方式2,lambda
new Thread(() -> {
System.out.println("lambda");
}).start();
// PS:方式3,方法引用
new Thread(Go::go).start();
}
}
输出:
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 ms = X::f; // [1-1]
// MakeString ms = new X()::f; // [1-2]
TransformX sp = X::f; // [2]
X x = new X();
System.out.println(sp.transform(x)); // [3]
System.out.println(x.f());
}
}
输出:
X::f()
X::f()
在 [1],我们尝试把 X 的 f() 方法引用赋值给 MakeString。结果:即使 make() 与 f() 具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。 这是因为实际上还有另一个隐藏的参数:我们的老朋友 this 。你不能在没有 X 对象的前提下调用 f() 。 因此, X :: f 表示未绑定的方法引用,因为它尚未“绑定”到对象。
—PS:解决方法1-将 f() 用 static 修饰,方法2-创建个对象在用,类似 代码注释[1-2]
要解决这个问题,我们需要一个 X 对象,所以我们的接口实际上需要一个额外的参数的接口,如上例中的 TransformX。 如果将 X :: f 赋值给 TransformX,这在 Java 中是允许的。这次我们需要调整下心里预期——使用未绑定的引用时,函数方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 理由是:你需要一个对象来调用方法。
—PS:这个地方解释的是代码注释[2],也就是如果想使用 X :: f 需在函数方法中加入 X 类型的参数
代码注释[3]的结果有点像脑筋急转弯。 我接受未绑定的引用并对其调用 transform() ,将其传递给 X ,并以某种方式导致对 x.f() 的调用。
// 未绑定的方法与多参数的结合运用
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;
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) {
// PS:Dog :: new 构造函数引用。
// 这 3 个构造函数只有一个相同名 称: :: new ,但在每种情况下都赋值给不同的接口。
// 编译器可以检测并知道从哪个构造函数引用。
MakeNoArgs mna = Dog::new;
Make1Arg m1a = Dog::new;
Make2Args m2a = Dog::new;
Dog dn = mna.make();
Dog d1 = m1a.make("Comet");
Dog d2 = m2a.make("Ralph", 4);
}
}
—PS:::new 构造函数引用
方法引用和 Lambda 表达式必须被赋值,同时编译器需要识别类型信息以确保类型正确。 Lambda 表达式特别引入了新的要求。 代码示例:
x -> x.toString()
我们清楚这里返回类型必须是 String,但 x 是什么类型呢?
Lambda 表达式包含类型推导(编译器会自动推导出类型信息,避免了程序员显式地声明)。编译器必须能够以某种方式推导出 x 的类型。
为了解决这个问题,Java 8 引入了 java.util.function 包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。
在编写接口时,可以使用 @FunctionalInterface 注解强制执行此“函数式方法”模式:
@FunctionalInterface
interface Functional {
String goodbye(String arg);
}
interface FunctionalNoAnn {
String goodbye(String arg);
}
/*
@FunctionalInterface
interface NotFunctional {
String goodbye(String arg);
String hello(String arg);
}
产生错误信息: NotFunctional is not a functional interface multiple non-overriding
abstract methods found in interface NotFunctional */
public class FunctionalAnnotation {
public String goodbye(String arg) {
return "Goodbye, " + arg;
}
public static void main(String[] args) {
FunctionalAnnotation fa = new FunctionalAnnotation();
// PS:方法引用的方式
Functional f = fa::goodbye;
FunctionalNoAnn fna = fa::goodbye;
// Functional fac=fa; // Incompatible
// PS:lambda 表达式的方式
Functional fl = a -> "Goodbye, " + a;
FunctionalNoAnn fnal = a -> "Goodbye, " + a;
}
}
@FunctionalInterface 注解是可选的; Java 在 main() 中把 Functional 和 FunctionalNoAnn 都当作函数式接口。 @FunctionalInterface 的值在 NotFunctional 的定义中可见:接口中如果有多个方法则会产生编译时错误消息。
函数式接口基本命名准则:
如果只处理对象而非基本类型,名称则为 Function , Consumer , Predicate 等。参数类型通过泛型添加。
如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumer , DoubleFunction , IntPredicate 等,但基本 Supplier 类型例外。
如果返回值为基本类型,则用 To 表示,如 ToLongFunction 和 IntToLongFunction 。
如果返回值类型与参数类型一致,则是一个运算符:单个参数使用 UnaryOperator ,两个参数 使用 BinaryOperator 。
如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate)。
如果接收的两个参数类型不同,则名称中有一个 Bi 。
下表描述了 java.util.function 中的目标类型(包括例外情况):
特征 | 函数方法名 | 示例 |
---|---|---|
无参数; 无返回值 |
Runnable (java.lang) run() |
Runnable |
无参数; 返回类型任意 |
Supplier get() getAs类型() |
Supplier BooleanSupplier IntSupplier LongSupplier DoubleSupplier |
无参数; 返回类型任意 |
Callable (java.util.concurrent) call() |
Callable |
1参数; 无返回值 |
Consumer accept() |
Consumer IntConsumer LongConsumer DoubleConsumer |
2 参数 Consumer | BiConsumer accept() |
BiConsumer |
2 参数 Consumer; 1 引用; 1 基本类型 |
Obj类型Consumer accept() |
ObjIntConsumer ObjLongConsumer ObjDoubleConsumer |
1 参数; 返回类型不同 |
Function apply() To类型 和 类型To类型 applyAs类型() |
Function IntFunction LongFunction DoubleFunction ToIntFunction ToLongFunction ToDoubleFunction IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction |
1 参数; 返回类型相同 |
UnaryOperator apply() |
UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator |
2 参数类型相同; 返回类型相同 |
BinaryOperator apply() |
BinaryOperator IntBinaryOperator LongBinaryOperator DoubleBinaryOperator |
2 参数类型相同; 返回整型 |
Comparator (java.util) compare() |
Comparator |
2 参数; 返回布尔型 |
Predicate test() |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
参数基本类型; 返回基本类型 |
类型To类型Function applyAs类型() |
IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction |
2 参数类型不同 | Bi操作 (不同方法名) |
BiFunction BiConsumer BiPredicate ToIntBiFunction ToLongBiFunction ToDoubleBiFunction |
import java.util.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 {
// PS:第一类:1个参数(类型 Foo),返回类型 Bar,所以 lambda 表达式的 f 类型就被推断为了 Foo,下同
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);
// PS:第二类:1个参数(类型 IBaz),返回类型 Int,所以 lambda 表达式的 ib 类型就被推断为了 IBaz,下同
static ToIntFunction<IBaz> f5 = ib -> ib.i;
static ToLongFunction<LBaz> f6 = lb -> lb.l;
static ToDoubleFunction<DBaz> f7 = db -> db.d;
// PS:第三类:1个参数(类型 Int),返回类型为 Long,大转小涉及强转,下同
static IntToLongFunction f8 = i -> i;
static IntToDoubleFunction f9 = i -> i;
static LongToIntFunction f10 = i -> (int) i;
static LongToDoubleFunction f11 = i -> i;
static DoubleToIntFunction f12 = i -> (int) i;
static DoubleToLongFunction f13 = i -> (long) i;
public static void main(String[] args) {
Bar apply = f1.apply(new Foo());
IBaz ib = f2.apply(11);
LBaz lb = f3.apply(11);
DBaz db = f4.apply(11);
int i = f5.applyAsInt(ib);
long l = f6.applyAsLong(lb);
double d = f7.applyAsDouble(db);
l = f8.applyAsLong(12);
d = f9.applyAsDouble(12);
i = f10.applyAsInt(12);
d = f11.applyAsDouble(12);
i = f12.applyAsInt(13.0);
l = f13.applyAsLong(13.0);
}
}
主方法中的每个测试都显示了 Function 接口中不同类型的 apply() 方法。 每个都产生一个与其关联的 Lambda 表达式的调用。
下面是方法引用的方式:
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());
bic = MethodConversion::someOtherName;
// bic.someOtherName(new In1(), new In2()); // Nope
bic.accept(new In1(), new In2());
}
}
输出:
accept()
someOtherName()
查看 BiConsumer 的文档,你会看到 accept() 方法。 实际上,如果我们将方法命名为 accept() ,它就可以作为方法引用。 但是我们也可用不同的名称,比如 someOtherName() 。只要 参数类型、返回类型与 BiConsumer 的 accept() 相同即可。
内部图.jpg
因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法名(在本例中为 accept() ),而不是你的方法名。
—PS:你的方法名也调用不到
java.util.functional 中的接口是有限的。比如有了 BiFunction ,但它不能变化。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看 Java 库源代码并自行创建。代码示例:
@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> tf = TriFunctionTest::f;
tf = (i, l, d) -> 12;
}
}
高阶函数(Higher-order Function)只是一个消费或产生函数的函数。
import java.util.function.Function;
// PS:使用继承,可以轻松地为专用接口创建别名
interface FuncSS extends Function<String, String> {
}
public class ProduceFunction {
// PS:produce() 就是高阶函数
static FuncSS produce() {
return s -> s.toLowerCase();
}
public static void main(String[] args) {
FuncSS f = produce();
System.out.println(f.apply("YELLING"));
}
}
输出:
yelling
要消费一个函数,消费函数需要在参数列表正确地描述函数类型。代码示例:
import java.util.function.Function;
class One {
}
class Two {
}
public class ConsumeFunction {
// PS:consume() 就是高阶函数
static Two consume(Function<One, Two> onetwo) {
return onetwo.apply(new One());
}
public static void main(String[] args) {
Two two = consume(one -> new Two());
}
}
当基于消费函数生成新函数时,事情就变得相当有趣了。代码示例如下:
import java.util.function.Function;
class I {
@Override
public String toString() {
return "I";
}
}
class O {
@Override
public String toString() {
return "O";
}
}
public class TransformFunction {
static Function<I, O> transform(Function<I, O> in) {
return in.andThen(o -> {
System.out.println(0);
return o;
});
}
public static void main(String[] args) {
Function<I, O> f = transform(i -> {
System.out.println(i);
return new O();
});
f.apply(new I());
}
}
输出:
I
0
在这里, transform() 生成一个与传入的函数具有相同签名的函数,但是你可以生成任何你想要的类型。
这里使用到了 Function 接口中名为 andThen() 的默认方法,该方法专门用于操作函数。 顾名思义,在调用 in 函数之后调用 toThen() (还有个 compose() 方法,它在 in 函数之前应用新函数)。 要附加一个 andThen() 函数,我们只需将该函数作为参数传递。 transform() 产生的是一个新函数,它将 in 的动作与 andThen() 参数的动作结合起来。
—PS: transform() 消耗一个函数,生成一个与传入的函数具有相同签名的函数。andThen() 在入参函数执行之后才会执行
闭包(Closure)一词总结了这些问题。 它非常重要,利用闭包可以轻松生成函数。
考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 能够解决这个问题的语言被称为支持闭包,或者叫作在词法上限定范围( 也使用术语变量捕获 )。Java 8 提供了有限但合理的闭包支持,我们将用一些简单的例子来研究它。
import java.util.function.IntSupplier;
public class Closure1 {
int i;
IntSupplier makeFun(int x) {
return () -> x + i++;
}
}
import java.util.function.IntSupplier;
public class SharedStorage {
public static void main(String[] args) {
Closure1 c1 = new Closure1();
IntSupplier f1 = c1.makeFun(0);
IntSupplier f2 = c1.makeFun(0);
IntSupplier f3 = c1.makeFun(0);
System.out.println(f1.getAsInt());
System.out.println(f2.getAsInt());
System.out.println(f3.getAsInt());
}
}
输出:
0
1
2
每次调用 getAsInt() 都会增加 i ,表明存储是共享的。
如果 i 是 makeFun() 的局部变量怎么办?
import java.util.function.IntSupplier;
public class Closure2 {
IntSupplier makeFun(int x) {
int i = 0;
return () -> x + i;
}
}
由 makeFun() 返回的 IntSupplier “关闭” i 和 x ,因此当你调用返回的函数时两者仍然有效。 但请注意,我没有像 Closure1.java 那样递增 i ,因为会产生编译时错误。代码示例:
import java.util.function.IntSupplier;
public class Closure3 {
IntSupplier makeFun(int x) {
int i = 0;
// x++ 和 i++ 都会报错:
return () -> x++ + i++;
}
}
从 Lambda 表达式引用的局部变量必须是 final 或者是等同final 效果的。
如果使用 final 修饰 x 和 i ,就不能再递增它们的值了。代码示例:
import java.util.function.IntSupplier;
public class Closure4 {
IntSupplier makeFun(final int x) {
final int i = 2;
return () -> x + i;
}
}
这就叫做等同 final 效果(Effectively Final)。这个术语是在 Java 8 才开始出现的,表示虽然没有明确地声明变量是 final 的,但是因变量值没被改变过而实际有了 final 同等的效果。 如果局部变量的初始值永远不会改变,那么它实际上就是 final 的。
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class Closure8 {
Supplier<List<Integer>> makeFun() {
final List<Integer> ai = new ArrayList<>();
ai.add(1);
return () -> ai;
}
public static void main(String[] args) {
Closure8 c8 = new Closure8();
List<Integer> l1 = c8.makeFun().get();
List<Integer> l2 = c8.makeFun().get();
System.out.println(l1);
System.out.println(l2);
l1.add(42);
l2.add(96);
System.out.println(l1);
System.out.println(l2);
}
}
输出:
[1]
[1]
[1, 42]
[1, 96]
可以看到,这次一切正常。我们改变了 List 的值却没产生编译时错误。通过观察本例的输出结果,我们发现这看起来非常安全。这是因为每次调用 makeFun() 时,其实都会创建并返回一个全新的ArrayList 。 也就是说,每个闭包都有自己独立的 ArrayList , 它们之间互不干扰。
—PS:上面的代码去掉 final 效果一样
重新赋值引用会触发错误消息。
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class Closure9 {
Supplier<List<Integer>> makeFun() {
List<Integer> ai = new ArrayList<>();
ai = new ArrayList<>();
return () -> ai;
}
}
让我们回顾一下 Closure1.java 。那么现在问题来了:为什么变量 i 被修改编译器却没有报错呢。 它既不是 final 的,也不是等同 final 效果的。因为 i 是外围类的成员,所以这样做肯定是安全的(除非你正在创建共享可变内存的多个函数)。是的,你可以辩称在这种情况下不会发生变量捕获(Variable Capture)。但可以肯定的是, Closure3.java 的错误消息是专门针对局部变量的。
因此,规则并非只是“在 Lambda 之外定义的任何变量必须是 final 的或等同 final 效果那么简单。相反,你必须考虑捕获的变量是否是等同 final 效果的。 如果它是对象中的字段,那么它拥有独立的生存周期,并且不需要任何特殊的捕获,以便稍后在调用 Lambda 时存在。
可以使用匿名内部类重写之前的例子:
import java.util.function.IntSupplier;
public class AnonymousClosure {
IntSupplier makeFun(int x){
int i = 0;
//i++; // PS:和上面的例子一样,等同 final 不能改变
//x++; // 同上
return new IntSupplier() {
@Override
public int getAsInt() {
return x+i;
}
};
}
}
实际上只要有内部类,就会有闭包(Java 8 只是简化了闭包操作)。在 Java 8 之前,变量 x 和 i 必须被明确声明为 final 。在 Java 8 中,内部类的规则放宽,包括等同 final 效果。
—PS:上面的所有例子说明,闭包内的变量是 final 修饰的
函数组合(Function Composition)意为“多个函数组合成新函数”。java.util.function 接口中包含支持函数组合的方法 。
组合方法 | 支持接口 |
---|---|
andThen(argument) 根据参数执行原始操作 |
Function BiFunction Consumer BiConsumer IntConsumer LongConsumer DoubleConsumer UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator BinaryOperator |
compose(argument) 根据参数执行原始操作 |
Function UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator |
and(argument) 短路逻辑与原始谓词和参数谓词 |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
or(argument) 短路逻辑或原始谓词和参数谓词 |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
negate() 该谓词的逻辑否谓词 |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
—PS:推荐大佬的一篇博客
【JAVA8】快速理解Consumer、Supplier、Predicate与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(),
f4 = f1.compose(f2).andThen(f3);
public static void main(String[] args) {
String apply = f4.apply("GO AFTER ALL AMBULANCES");
System.out.println(apply);
}
}
输出:
AFTER ALL AMBULANCES
_fter _ll _mbul_nces
当 f1 获得字符串时,它已经被 f2 剥离了前三个字符。这是因为 compose(f2) 表示 f2 的调用发生在 f1 之前。
—PS:f4 是 先执行 f2 再执行 f1 andThen f3
import java.util.function.Predicate;
import java.util.stream.Stream;
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);
public static void main(String[] args) {
Stream.of("bar", "foobar", "foobaz", "fongopuckey")
.filter(p4)
.forEach(System.out ::println);
}
}
输出:
foobar
foobaz
p4 获取到了所有谓词并组合成一个更复杂的谓词。解读:如果字符串中不包含 bar 且长度小于 5,或者它包含 foo ,则结果为 true 。
—PS:negate 反相器、倒换器
正因它产生如此清晰的语法,我在主方法中采用了一些小技巧,并借用了下一章的内容。首先,我创建了一个字符串对象的流,然后将每个对象传递给 filter() 操作。 filter() 使用 p4 的谓词来确定对象的去留。最后我们使用 forEach() 将 println 方法引用应用在每个留存的对象上。
从输出结果我们可以看到 p4 的工作流程:任何带有 foo 的东西都会留下,即使它的长度大于 5。 fongopuckey 因长度超出和不包含 bar 而被丢弃。
柯里化(Currying)的名称来自于其发明者之一 Haskell Curry。他可能是计算机领域唯一名字被命名重要概念的人(另外就是 Haskell 编程语言)。 柯里化意为:将一个多参数的函数,转换为一系列单参数函数。
import java.util.function.Function;
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
柯里化的目的是能够通过提供一个参数来创建一个新函数。
即使没有函数式支持,像 C 这样的基础语言,也可以按照一定的原则编写纯函数式程序。Java 8 让函数式编程更简单,不过我们要确保一切是 final 的,同时你的所有方法和函数没有副作用。因为 Java 在本质上并非是不可变语言,我们无法通过编译器查错。
这种情况下,我们可以借助第三方工具,但使用 Scala 或 Clojure 这样的语言可能更简单。因为它们从一开始就是为保持不变性而设计的。你可以采用这些语言来编写你的 Java 项目的一部分。如果必须要用纯函数式编写,则可以用 Scala(需要一些规则) 或 Clojure (需要的规则更少)。虽然 Java 支持并发编程,但如果这是你项目的核心部分,你应该考虑在项目部分功能中使用 Scala 或 Clojure 之类的语言。
Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。
但是,Lambdas 和方法引用远非完美,特别是没有泛型 Lambda,所以 Lambda 在 Java 中并非一等公民。
当你遇到学习困难时,请记住通过 IDE(NetBeans、IntelliJ Idea 和 Eclipse)获得帮助,因为 IDE 可以智能提示你何时使用 Lambda 表达式或方法引用,甚至有时还能为你优化代码。
自我学习总结:
Java 8 的 Lambda 表达式,(参数列表) -> {函数体},只有一个参数时 () 可以省略,函数体有多行时,需要 return
Java 8 的方法引用, 类或对象名::方法名称
递归函数是一个自我调用的函数,递归方法中必须是实例变量或静态变量,必须有终止条件,要不然会一直循环直至内存溢出
构造函数引用,类名::new
函数式接口,使用注解 @FunctionalInterface ,只包含一个抽象方法,称为函数式方法
java.util.function 提供了一些函数式接口,方便为 lambda 表达式或者方法引用赋值。其中重要的四个接口的类型,Consumer(消费型)、Supplier(供给型)、Predicate(判断型)与Function(转换型) ,对应的抽象方法 Consumer(accpet)、Supplier(get)、Predicate(test) 与 Function(apply)
多参数函数式接口可以模拟自行创建
高阶函数:消费或产生函数的函数
有内部类就会有闭包,闭包内部的变量是不可变的(final 定义或等同 final)
函数组合,多个函数组合成新函数,组合方法有:
组合方法 | 示例 | 说明 |
---|---|---|
andThen(argument) | f1.andThen(f2) | f2在f1之后执行 |
compose(argument) | f1.compose(f2) | f2在f1之前执行 |
and(argument) | f1.and(f2) | 包含f1和f2执行后的结果 |
or(argument) | f1.or(f2) | 包含f1或f2执行后的结果 |
negate() | f1.negate() | f1执行后取反 |
柯里化意为:将一个多参数的函数,转换为一系列单参数函数