欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

On java8第十三章

程序员文章站 2024-02-29 11:34:28
...

函数式编程

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

​ 通过修改内存中的代码,使程序可以执行不同的操作,用这种方式来节省代码空间。这种技术被称为自修改代码,只要程序小到几个人就能够维护所有棘手和难懂的汇编代码,你就能让程序运行起来。

​ 使用代码以某种方式操纵其他代码的想法也很有趣,只要能保证它更安全。从代码创建,维护和可靠性的角度来看,这个想法非常吸引人。我们不用从头开始编写大量代码,而是从易于理解、充分测试及可靠的现有小块开始,最后将它们组合在一起以创建新代码。

函数式编程(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。至少在某些情况下,这套理论似乎很有用。在这一过程中,函数式语言已经产生了优雅的语法,这些语法对于非函数式语言也适用。

​ 纯粹的函数式语言在安全性方面更进一步。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。

​ 函数式语言背后有很多动机,这意味着描述它们可能会有些混淆。它通常取决于各种观点:为“并行编程”,“代码可靠性”和“代码创建和库复用”。


新旧对比

​ 通常,传递给方法的数据不同,结果不同。如果我们希望方法在调用时行为不同,只要能将代码传递给方法,我们就可以控制它的行为。

//创建一个接口
interface Strategy{
    //返回值类型String的接口方法
    String approach(String msg);
}

//实现Strategy接口
class Soft implements Strategy{
    //实现接口方法
    @Override
    public String approach(String msg) {
        return msg.toLowerCase() + "?";
    }
}
//创建一个Unrelated类
class Unrelated {
    static String twice(String msg) {
        return msg + " " + msg;
    }
}

public class Strategize {
    //创建Strategy引用
    Strategy strategy;
    //创建String引用
    String msg;
    //构造器
    Strategize(String msg){
        strategy=new Soft();// [1]  在构造器中赋值
        this.msg=msg;
    }
	//方法communicate,打印输出Strategize类调用approach()方法后的输出内容
    void communicate() {
        System.out.println(strategy.approach(msg));
    }
	//通过方法赋值strategy
    void changeStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public static void main(String[] args) {
        //创建数组
        Strategy[] strategies = {
                new Strategy() { // [2]  匿名内部类
                    //元素1
                    public String approach(String msg) {
                        return msg.toUpperCase() + "!";
                    }
                },
            	//元素2,lambad表达式
                msg -> msg.substring(0, 5), // [3]  Lambda 表达式
            	//元素3
                Unrelated::twice // [4]  方法引用
        };
        //创建Strategize对象
        Strategize s = new Strategize("Hello there");
        //调用communicate方法打印输出
        s.communicate();
        //通过forin循环,执行循环赋值,和打印输出操作
        for(Strategy newStrategy : strategies) {
            s.changeStrategy(newStrategy); // [5]
            s.communicate(); // [6]
        }
    }
}

Strategy 接口提供了单一的 approach() 方法来承载函数式功能。通过创建不同的 Strategy 对象,我们可以创建不同的行为。

我们一般通过创建一个实现Strategy接口的类来实现这种行为,正如在Soft里所做的。

  • [1]Strategize 中,你可以看到 Soft 作为默认策略,在构造函数中赋值。
  • [2] 一种较为简洁且更加自然的方法是创建一个匿名内部类。即便如此,仍有相当数量的冗余代码。你总需要仔细观察后才会发现:“哦,我明白了,原来这里使用了匿名内部类。”
  • [3] Java 8 的 Lambda 表达式,其参数和函数体被箭头 -> 分隔开。箭头右侧是从 Lambda 返回的表达式。它与单独定义类和采用匿名内部类是等价的,但代码少得多。
  • [4] Java 8 的方法引用,它以 :: 为特征。 :: 的左边是类或对象的名称, :: 的右边是方法的名称,但是没有参数列表。
  • [5] 在使用默认的 Soft 策略之后,我们逐步遍历数组中的所有 Strategy,并通过调用 changeStrategy() 方法将每个 Strategy 传入变量 s 中。
  • [6] 现在,每次调用 communicate() 都会产生不同的行为,具体取决于此刻正在使用的策略代码对象。我们传递的是行为,而并不仅仅是数据。

Lambda表达式

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

  1. Lambda 表达式产生函数,而不是类。 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”。
  2. 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!";
    //正常情况使用括号 () 包裹参数。 为了保持一致性,也可以使用括号 () 包裹单个参数,虽然这种情况并不常见。
    static Body bod2=(h)->h+"More details";
    //如果没有参数,则必须使用括号 () 表示空参数列表。
    static Description desc=()->"Short info";
    //对于多个参数,将参数列表放在括号 () 中。
    static Multi mult=(h,n)->h+n;
    //在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 return。
    static Description moreLines=()->{
        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());
    }
}

每个接口都有一个单独的方法,每个方法都有不同数量的参数,以便演示 Lambda 表达式语法。

Lambda 表达式的基本语法是:

  1. 参数。
  2. 接着 ->,可视为“产出”。
  3. -> 之后的内容都是方法体。

Lambda 表达式通常比匿名内部类产生更易读的代码,因此我们将在本书中尽可能使用它们。


递归

​ 递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。

//创建一个接受 int 型参数并生成 int 的接口
public interface IntCall {
    int call(int arg);
}

使用Lambda表达式实现递归操作:

public class RecursiveFibonacci {
    IntCall fib;

    RecursiveFibonacci(){
        //此处使用了lambda表达式赋值
        fib=n->n==0?0:
               n==1?1: 
        	  //递归
        	  fib.call(n-1)+fib.call(n-2);
    }
    int fibonacci(int n){return fib.call(n);}

    public static void main(String[] args) {
        RecursiveFibonacci rf=new RecursiveFibonacci();
        for (int i = 0; i <=10; i++) {
            System.out.println(rf.fibonacci(i));
        }
    }
}

方法引用

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

interface Callable { // [1]
  void call(String s);
}

class Describe {
  void show(String msg) { // [2]
    System.out.println(msg);
  }
}

public class MethodReferences {
  static void hello(String name) { // [3]
    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]
      System.out.println(msg);
    }
  }
  public static void main(String[] args) {
    Describe d = new Describe();
    Callable c = d::show; // [6]
    c.call("call()"); // [7]

    c = MethodReferences::hello; // [8]
    c.call("Bob");

    c = new Description("valuable")::help; // [9]
    c.call("information");

    c = Helper::assist; // [10]
    c.call("Help!");
  }
}
  1. 我们从单一方法接口开始(同样,你很快就会了解到这一点的重要性)。
  2. show() 的签名(参数类型和返回类型)符合 Callablecall() 的签名。
  3. hello()也符合call()的签名。
  4. help()也符合,它是静态内部类中的非静态方法。
  5. assist()是静态内部类中的静态方法。
  6. 我们将 **Describe** 对象的方法引用赋值给 **Callable** ,它没有show()方法,而是call()方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 **Callable** 的call()方法的签名。
  7. 我们现在可以通过调用call()来调用show(),因为 Java 将call()映射到show()`。
  8. 这是一个静态方法引用。
  9. 这是 [6] 的另一个版本:对已实例化对象的方法的引用,有时称为绑定方法引用
  10. 最后,获取静态内部类中静态方法的引用与 [8] 中通过外部类引用相似。

Runnable接口

Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 run() 不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable

class Go{
    static void go(){
        System.out.println("Go::go");
    }
}

public class RunnableMethodReference {
    public static void main(String[] args) {
        //传入匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymous");
            }
        }).start();
        //使用lambda表达式
        new Thread(()-> System.out.println("lambda")){
        }.start();
        //使用方法引用
        new Thread(Go::go).start();
    }
}

Thread 对象将 Runnable 作为其构造函数参数,并具有会调用 run() 的方法 start()注意,只有匿名内部类才需要具有名为 run() 的方法。


未绑定的方法引用

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


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) {
        TransformX sp=X::f;//个人理解:只是将X对象传递到TransformX函数式接口方法参数列表中
        X x = new X();
        System.out.println(sp.transform(x)); //[2] 
        System.out.println(x.f()); 
    }

​ 注意上面代码中,如果被赋值的方法引用和需要赋值的接口方法的签名(参数和返回值)相同,那么对于未绑定的方法引用,必须提供对象实例;**如果需要赋值的接口方法中的参数中有该类的对象,则认为需要一个这样的对象来调用方法,因此参数可以不完全匹配。**不完全匹配的意思是被赋值方法引用除了该对象外,其余参数匹配。

//如果你的方法有更多个参数,就以第一个参数接受this的模式来处理
class This{
    void two(int i, double d) {
        System.out.println("TWO");
    }
    void two2(int i, double d) {
        System.out.println("TWO2");
    }
    void three(int i, double d, String s) {
        System.out.println("THREE");
    }
    void four(int i, double d, String s, char c) {
        System.out.println("FOUR");
    }
}

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;//个人理解:将This类绑定two方法,传入twoArgs中
        ThreeArgs threeArgs=This::three;
        FourArgs fourArgs=This::four;
        This athis = new This();
        twoArgs.call2(athis,11,3.14);//通过调用接口方法call2时,会使用two方法覆盖call2方法
        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) {
    MakeNoArgs mna = Dog::new; // [1]
    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);
  }
}

Dog 有三个构造函数,函数式接口内的 make() 方法反映了构造函数参数列表( make() 方法名称可以不同)。

​ 对 [1]**,[2]** 和 [3] 中的每一个使用 Dog :: new。 这三个构造函数只有一个相同名称::: new,但在每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数。


函数式接口

​ Java 8 引入了 java.util.function 包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。

//此注解强制接口只有一个抽象方法
@FunctionalInterface
interface Functional{
    String goodbye(String arg);
}

interface FunctionalNoAnn {
    String goodbye(String arg);
    //如果添加如下抽象方法,则该接口不是函数式接口
    //String goodbye(String arg);
}
public class FunctionalAnnotation {
    public String goodbye(String arg){
        return "Goodbye, " + arg;
    }

    public static void main(String[] args) {
        FunctionalAnnotation fa=new FunctionalAnnotation();
        //使用方法引用方式
        Functional f=fa::goodbye;
        FunctionalNoAnn fna=fa::goodbye;
	   // Functional fac = fa; //会报错,因为FunctionalAnnotation 并没有显式地去实现 Functional 接口
        //使用lambda表达式
        Functional f1=a->"Goodbye"+a;
        FunctionalNoAnn fnal=a->"Goodbye"+a;
    }
}

main() 中把 FunctionalFunctionalNoAnn 都当作函数式接口。

FunctionalFunctionalNoAnn 定义接口,然而被赋值的只是方法 goodbye()。首先,这只是一个方法而不是类;它甚至都不是实现了该接口的类中的方法,将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会在后台把方法引用或 Lambda 表达式包装进实现目标接口的类的实例中。

接口的基本命名准则:

  1. 如果只处理对象而非基本类型,名称则为 FunctionConsumerPredicate 等。参数类型通过泛型添加。
  2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumerDoubleFunctionIntPredicate 等,但返回基本类型的 Supplier 接口例外。
  3. 如果返回值为基本类型,则用 To 表示,如 ToLongFunction <T>IntToLongFunction
  4. 如果返回值类型与参数类型一致,则是一个运算符:单个参数使用 UnaryOperator,两个参数使用 BinaryOperator
  5. 如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate)。
  6. 如果接收的两个参数类型不同,则名称中有一个 Bi

多参数函数式接口

java.util.functional 中的接口是有限的。可以自行创建

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

同时测试了方法引用和 Lambda 表达式。


缺少基本类型的函数

使用了包装类型,装箱和拆箱负责它与基本类型之间的来回转换。


高阶函数

​ 高阶函数是指接受另外一个函数作为参数,或返回一个函数的函数。什么样的参数是函数类型的参数?要看该参数是否是一个函数式接口,函数式接口只会有一个方法,会使用 @FunctionalInterface 这个注解来修饰。

//产生一个函数
//继承一个接口
interface FuncSS extends Function<String, String> {} // [1]

public class ProduceFunction {
    //produce就是高阶函数
  static FuncSS produce() {
      //使用lambda在方法中创建和返回一个函数
    return s -> s.toLowerCase(); // [2]
  }
  public static void main(String[] args) {
    FuncSS f = produce();
    System.out.println(f.apply("YELLING"));
  }
}

[1] 使用继承,可以轻松地为专用接口创建别名。

[2] 使用 Lambda 表达式,可以轻松地在方法中创建和返回一个函数。

//消费一个函数,消费函数需要在参数列表正确地描述函数类型。
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());
    }
}

当基于消费函数生成新函数时,事情就变得相当有趣了。

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

    public static void main(String[] args) {

        Function<I,O> f2=transform(i -> {
            System.out.println(i);
            return new O();
        });

//        Function<I,O> f2=transform(new Function<I, O>() {
//            @Override
//            public O apply(I i) {
//                System.out.println(i);
//                return new O();
//            }
//        });
        O o=f2.apply(new I());
    }
}

transform() 生成一个与传入的函数具有相同签名的函数,但是你可以生成任何你想要的类型。

​ 这里使用到了 Function 接口中名为 andThen() 的默认方法,该方法专门用于操作函数。 顾名思义,在调用 in 函数之后调用 andThen()(还有个 compose() 方法,它在 in 函数之前应用新函数)。 要附加一个 andThen() 函数,我们只需将该函数作为参数传递。 transform() 产生的是一个新函数,它将 in 的动作与 andThen() 参数的动作结合起来。


闭包

闭包包含*(未绑定到特定对象)变量;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。

就是可以绑定变量作用域,那么很显然,JAVA是完全支持闭包的,

public class Closure1 {
    int i=0;
    IntSupplier makeFun(int x){
        //此处调用全局变量i
        return ()->x + i++;
    }
}

结论:

  1. JAVA支持变量作用域,完全支持闭包;
  2. 并不是只有函数式编程语言才支持闭包,见代码片段1,完全是面向对象的写法;
  3. 相比与Javascript等函数式编程语言,由于final的限定,JAVA并不支持对变量作用域值的修改;
  4. 由于JAVA严格的面向对象设计语言,对于函数的调用必须加上对象名限定(如类名、实例名及接口名称);
  5. 闭包没有那么神奇,也没有那么高不可攀;
//使用List,尝试在作用域实现添加元素
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 c7 = new Closure8();
    List<Integer>
      l1 = c7.makeFun().get(),
      l2 = c7.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,它们之间互不干扰。


作为闭包的内部类

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

public class AnonymousClosure {
  IntSupplier makeFun(int x) {
    int i = 0;
    // 同样规则的应用:
    // i++; // 非等同 final 效果
    // x++; // 同上
    return new IntSupplier() {
      public int getAsInt() { return x + i; }
    };
  }
}

函数组合

函数组合(Function Composition)意为“多个函数组合成新函数”。它通常是函数式编程的基本组成部分。

组合方法 支持接口
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
//实例
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) {
        System.out.println(f4.apply("GO AFTER ALL AMBULANCES"));
    }
}

f1 获得字符串时,它已经被f2 剥离了前三个字符。这是因为 compose(f2) 表示 f2 的调用发生在 f1 之前。

//更复杂的组合
public class PredicateComposition {
    static Predicate<String>
    p1=s->s.contains("bar"),
    p2=s->s.length()<5,
    p3=s->s.contains("foo"),
    //解释:如果字符串中不包含 bar 且长度小于 5,或者它包含 foo ,则结果为 true。
    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);
    }
}

​ 首先,我创建了一个字符串对象的流,然后将每个对象传递给 filter() 操作。 filter() 使用 p4 的谓词来确定对象的去留。最后我们使用 forEach()println 方法引用应用在每个留存的对象上。

​ 从输出结果我们可以看到 p4 的工作流程:任何带有 "foo" 的字符串都得以保留,即使它的长度大于 5。 "fongopuckey" 因长度超出且不包含 foo 而被丢弃。


柯里化和部分求值

柯里化意为:将一个多参数的函数,转换为一系列单参数函数。

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"));

        //柯里化的目的是能够通过提供一个参数来创建一个新函数,所以现在有了一个“带参函数”和剩下的 “*函数”(free argumnet)
        // 实际上,你从一个双参数函数开始,最后得到一个单参数函数。
        Function<String,String> sumHi=sum.apply("Hup");
        System.out.println(sumHi.apply("Ho"));
        System.out.println(sumHi.apply("Hey"));
    }
}

通过添加级别来柯里化一个三参数函数

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

对于每个级别的箭头级联(Arrow-cascading),你都要在类型声明中包裹另一层 Function

注意:处理基本类型和装箱时,请使用适当的函数式接口


纯函数式编程

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

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