读书笔记-《ON JAVA 中文版》-摘要13[第十三章 函数式编程]

第十三章 函数式编程

函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许你以函数式编程。

这就是函数式编程(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。

OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为

需要提醒大家的是,函数式语言背后有很多动机,这意味着描述它们可能会有些混淆。它通常取决于各种观点:为“并行编程”,“代码可靠性”和“代码创建和库复用”。 关于函数式编程能高效创建更健壮的代码这一观点仍存在部分争议。

1. 新旧对比

通常,传递给方法的数据不同,结果不同。如果我们希望方法在调用时行为不同,该怎么做呢?结论是:只要能将代码传递给方法,我们就可以控制它的行为

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 表达式的出现让我们可以在需要时传递功能,而不是仅在必要才这么做。

2. Lambda表达式

2.1 Lambda表达式

Lambda 表达式是使用最小可能语法编写的函数定义:

  1. Lambda 表达式产生函数,而不是类。 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是 一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假 装它们“只是函数”。

  2. Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用。

任何 Lambda 表达式的基本语法是:

  1. 参数。

  2. 接着 -> ,可视为“产出”。

  3. -> 之后的内容都是方法体。

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());
    }
}

2.2 递归

递归函数是一个自我调用的函数。可以编写递归的 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

所有递归函数都有==“停止条件”==,否则将无限递归并产生异常。

3. 方法引用

3.1 方法引用

方法引用组成:类名或对象名,后面跟 :: ,然后跟方法名称。

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!

3.2 Runnable接口

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()

3.3 未绑定的方法引用

未绑定的方法引用是指没有关联对象的普通(非静态)方法。 使用未绑定的引用之前,我们必须先提供对象:

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');
    }
}

3.4 构造函数引用

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 构造函数引用

4. 函数式接口

4.1 函数式接口

方法引用和 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() 中把 FunctionalFunctionalNoAnn 都当作函数式接口。 @FunctionalInterface 的值在 NotFunctional 的定义中可见:接口中如果有多个方法则会产生编译时错误消息

函数式接口基本命名准则:

  1. 如果只处理对象而非基本类型,名称则为 Function , Consumer , Predicate 等。参数类型通过泛型添加。

  2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumer , DoubleFunction , IntPredicate 等,但基本 Supplier 类型例外。

  3. 如果返回值为基本类型,则用 To 表示,如 ToLongFunction 和 IntToLongFunction 。

  4. 如果返回值类型与参数类型一致,则是一个运算符:单个参数使用 UnaryOperator ,两个参数 使用 BinaryOperator 。

  5. 如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate)。

  6. 如果接收的两个参数类型不同,则名称中有一个 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 参数 ConsumerBiConsumer
accept()
BiConsumer<T,U>
2 参数 Consumer
1 引用;
1 基本类型
Obj类型Consumer
accept()
ObjIntConsumer
ObjLongConsumer
ObjDoubleConsumer
1 参数;
返回类型不同
Function
apply()
To类型 和 类型To类型
applyAs类型()
Function <T,R>
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<T,U>
IntPredicate
LongPredicate
DoublePredicate
参数基本类型;
返回基本类型
类型To类型Function
applyAs类型()
IntToLongFunction
IntToDoubleFunction
LongToIntFunction
LongToDoubleFunction
DoubleToIntFunction
DoubleToLongFunction
2 参数类型不同Bi操作
(不同方法名)
BiFunction<T,U,R>
BiConsumer<T,U>
BiPredicate<T,U>
ToIntBiFunction<T,U>
ToLongBiFunction<T,U>
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:你的方法名也调用不到

4.2 多参数函数式接口

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;
    }
}

5. 高阶函数

高阶函数(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() 在入参函数执行之后才会执行

6. 闭包

6.1 闭包

闭包(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++;
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PEry9ENj-1680252194017)(c3.png)]

从 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;
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjaLzi57-1680252194018)(list1.png)]

让我们回顾一下 Closure1.java 。那么现在问题来了:为什么变量 i 被修改编译器却没有报错呢。 它既不是 final 的,也不是等同 final 效果的。因为 i 是外围类的成员,所以这样做肯定是安全的(除非你正在创建共享可变内存的多个函数)。是的,你可以辩称在这种情况下不会发生变量捕获(Variable Capture)。但可以肯定的是, Closure3.java 的错误消息是专门针对局部变量的。

因此,规则并非只是“在 Lambda 之外定义的任何变量必须是 final 的或等同 final 效果那么简单。相反,你必须考虑捕获的变量是否是等同 final 效果的。 如果它是对象中的字段,那么它拥有独立的生存周期,并且不需要任何特殊的捕获,以便稍后在调用 Lambda 时存在。

6.2 作为闭包的内部类

可以使用匿名内部类重写之前的例子:

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;
            }
        };
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C3Yw6Lfg-1680252194018)(in1.png)]

实际上只要有内部类,就会有闭包(Java 8 只是简化了闭包操作)。在 Java 8 之前,变量 x 和 i 必须被明确声明为 final 。在 Java 8 中,内部类的规则放宽,包括等同 final 效果。

—PS:上面的所有例子说明,闭包内的变量是 final 修饰的

7. 函数组合

函数组合(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 而被丢弃。

8. 柯里化和部分求值

柯里化(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

柯里化的目的是能够通过提供一个参数来创建一个新函数。

9. 纯函数式编程

即使没有函数式支持,像 C 这样的基础语言,也可以按照一定的原则编写纯函数式程序。Java 8 让函数式编程更简单,不过我们要确保一切是 final 的,同时你的所有方法和函数没有副作用。因为 Java 在本质上并非是不可变语言,我们无法通过编译器查错。

这种情况下,我们可以借助第三方工具,但使用 Scala 或 Clojure 这样的语言可能更简单。因为它们从一开始就是为保持不变性而设计的。你可以采用这些语言来编写你的 Java 项目的一部分。如果必须要用纯函数式编写,则可以用 Scala(需要一些规则) 或 Clojure (需要的规则更少)。虽然 Java 支持并发编程,但如果这是你项目的核心部分,你应该考虑在项目部分功能中使用 Scala 或 Clojure 之类的语言。

10. 本章小结

Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。

但是,Lambdas 和方法引用远非完美,特别是没有泛型 Lambda,所以 Lambda 在 Java 中并非一等公民。

当你遇到学习困难时,请记住通过 IDE(NetBeans、IntelliJ Idea 和 Eclipse)获得帮助,因为 IDE 可以智能提示你何时使用 Lambda 表达式或方法引用,甚至有时还能为你优化代码。

自我学习总结:

  1. Java 8 的 Lambda 表达式,(参数列表) -> {函数体},只有一个参数时 () 可以省略,函数体有多行时,需要 return

  2. Java 8 的方法引用, 类或对象名::方法名称

  3. 递归函数是一个自我调用的函数,递归方法中必须是实例变量或静态变量,必须有终止条件,要不然会一直循环直至内存溢出

  4. 构造函数引用,类名::new

  5. 函数式接口,使用注解 @FunctionalInterface ,只包含一个抽象方法,称为函数式方法

  6. java.util.function 提供了一些函数式接口,方便为 lambda 表达式或者方法引用赋值。其中重要的四个接口的类型,Consumer(消费型)、Supplier(供给型)、Predicate(判断型)与Function(转换型) ,对应的抽象方法 Consumer(accpet)、Supplier(get)、Predicate(test) 与 Function(apply)

  7. 多参数函数式接口可以模拟自行创建

  8. 高阶函数:消费或产生函数的函数

  9. 有内部类就会有闭包,闭包内部的变量是不可变的(final 定义或等同 final)

  10. 函数组合,多个函数组合成新函数,组合方法有:

    组合方法示例说明
    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执行后取反
  11. 柯里化意为:将一个多参数的函数,转换为一系列单参数函数

  12. 不建议使用 Java 实现纯函数式编程,可以使用 Scala 或 Clojure

    (图网,侵删)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值