Skip to content

Latest commit

 

History

History
767 lines (562 loc) · 31.2 KB

File metadata and controls

767 lines (562 loc) · 31.2 KB

十七、Lambda 表达式与函数式编程

本章解释函数式编程的概念。它概述了 JDK 附带的函数接口,解释了如何在 lambda 表达式中使用它们,以及如何以最简洁的风格编写 lambda 表达式。

在本章中,我们将介绍以下主题:

  • 函数式编程
  • 功能接口
  • Lambda 表达式
  • 方法引用
  • 练习–使用方法引用创建新对象

函数式编程

函数式编程允许我们将代码块(函数)视为对象,将其作为参数或方法的返回值传递。此功能在许多编程语言中都存在。它不需要我们管理对象状态。该函数是无状态的。它的结果只取决于输入数据,不管调用了多少次。这种风格使结果更加可预测,这是函数式编程最吸引人的方面。

如果没有函数式编程,在 Java 中将功能作为参数传递的唯一方法是编写一个实现接口的类,创建其对象,然后将其作为参数传递。但是,即使是使用匿名类的最简单的样式也需要编写太多的样板代码。使用函数接口和 lambda 表达式可以使代码更短、更清晰、更具表现力。

将它添加到 Java 中可以通过将并行性的责任从客户机代码转移到库中来提高并行编程能力。在此之前,为了处理 Java 集合的元素,客户机代码必须迭代集合并组织处理。在 Java8 中,添加了新的(默认)方法,这些方法接受函数(函数接口的实现)作为参数,然后根据内部处理算法将其并行或不并行地应用于集合的每个元素。因此,组织并行处理是图书馆的责任。

在本章中,我们将定义和解释这些 Java 特性、函数接口和 lambda 表达式,并在代码示例中演示它们的适用性。它们使函数成为语言的一流公民,其重要性与对象相同。

什么是功能接口?

事实上,您已经在我们的演示代码中看到了函数式编程的元素。一个例子是forEach(Consumer consumer)方法,可用于每个Iterable,其中Consumer是一个功能接口。另一个例子是removeIf(Predicate predicate)方法,可用于每个Collection对象。传入的Predicate对象是一个函数——一个函数接口的实现。类似地,List接口中的sort(Comparator comparator)replaceAll(UnaryOperator uo)方法以及Map中的几个compute()方法都是函数式编程的例子。

函数接口是只有一个抽象方法的接口,包括从父接口继承的抽象方法。

为了避免运行时错误,Java8 中引入了一个@FunctionalInterface注释,告知编译器意图,因此编译器可以检查注释接口中是否只有一个抽象方法。让我们回顾一下同一继承行的以下接口:

@FunctionalInterface
interface A {
  void method1();
  default void method2(){}
  static void method3(){}
}

@FunctionalInterface
interface B extends A {
  default void method4(){}
}

@FunctionalInterface
interface C extends B {
  void method1();
}

//@FunctionalInterface  //compilation error
interface D extends C {
  void method5();
}

接口A是一个功能接口,因为它只有一个抽象方法:method1()。接口B也是一个功能接口,因为它也只有一个抽象方法——从接口A继承的相同method1()。接口C是一个功能接口,因为它只有一个抽象方法method1(),它覆盖了父接口A的抽象method1()方法。接口D不能是功能接口,因为它有两个抽象方法—method1(),来自父接口Amethod5()

当使用@FunctionalInterface注释时,它告诉编译器检查是否只存在一个抽象方法,并警告读取代码的程序员,该接口只有一个抽象方法。否则,程序员可能会浪费时间来增强接口,但后来发现它无法完成。

出于同样的原因,Java 早期版本中存在的RunnableCallable接口在 Java8 中被注释为@FunctionalInterface。它明确了这一区别,并提醒其用户和可能尝试添加另一个抽象方法的用户:

@FunctionalInterface
interface Runnable { 
  void run(); 
} 
@FunctionalInterface
interface Callable<V> { 
  V call() throws Exception; 
}

正如您所看到的,创建功能接口很容易。但在这样做之前,请考虑使用在 Type T0-包中提供的 43 个功能接口中的一个。

随时可用的标准功能接口

java.util.function包中提供的大部分接口是以下四个接口的专业化:FunctionConsumerSupplierPredicate。让我们回顾一下它们,然后简要概述其余 39 个标准功能接口。

功能

此接口和其他功能<indexentry content="standard functional interfaces:function">接口的符号包括输入数据(T和返回数据(R的类型列表。因此,Function<T, R>表示该接口的唯一抽象方法接受类型为T的参数并生成类型为R的结果。您可以通过阅读联机文档找到该抽象方法的名称。对于Function<T, R>接口,其方法为R apply(T)

了解所有这些之后,我们可以使用匿名类创建此接口的实现:

Function<Integer, Double> multiplyByTen = new Function<Integer, Double>(){
  public Double apply(Integer i){
    return i * 10.0;
  }
};

由程序员决定哪种实际类型为T(输入参数)哪种类型为R(返回值)。在我们的示例中,我们已确定输入参数将为Integer类型,结果将为Double类型。正如您现在可能已经意识到的,类型只能是引用类型,基本类型的装箱和拆箱是自动执行的。

我们现在可以按任何需要使用新的Function<Integer, Double> multiplyByTen功能。我们可以直接使用它,如下所示:

System.out.println(multiplyByTen.apply(1)); //prints: 10.0

或者,我们可以创建一个接受此函数作为参数的方法:

void useFunc(Function<Integer, Double> processingFunc, int input){
  System.out.println(processingFunc.apply(input));
}

然后,我们可以将函数传递到此方法中,并让该方法使用它:

useFunc(multiplyByTen, 10);     //prints: 100.00

我们还可以创建一个方法,在需要时生成函数:

Function<Integer, Double> createMultiplyBy(double num){
  Function<Integer, Double> func = new Function<Integer, Double>(){
    public Double apply(Integer i){
      return i * num;
    }
  };
  return func;
}

使用上述方法,我们可以编写以下代码:

Function<Integer, Double> multiplyByFive = createMultiplyBy(5);
System.out.println(multiplyByFive.apply(1)); //prints: 5.0
useFunc(multiplyByFive, 10);                 //prints: 50.0

在下一节中,我们将介绍如何使用 lambda 和函数表达式来表达 lambda。

消费者

通过查看Consumer<T>接口定义,您已经可以猜测该接口有一个抽象方法,该方法接受T类型<indexentry content=“standard functional interfaces:Consumer”>的参数,并且不返回任何内容。从Consumer<T>接口的文档中,我们了解到它的抽象方法是void accept(T),这意味着,例如,我们可以如下实现它:

Consumer<Double> printResult = new Consumer<Double>() {
  public void accept(Double d) {
    System.out.println("Result=" + d);
  }
};
printResult.accept(10.0);         //prints: Result=10.0

或者我们可以创建一个生成函数的方法:

Consumer<Double> createPrintingFunc(String prefix, String postfix){
  Consumer<Double> func = new Consumer<Double>() {
    public void accept(Double d) {
      System.out.println(prefix + d + postfix);
    }
  };
  return func;
}

现在我们可以按如下方式使用它:

Consumer<Double> printResult = createPrintingFunc("Result=", " Great!");
printResult.accept(10.0);    //prints: Result=10.0 Great!

我们还可以创建一个新方法,该方法不仅接受处理函数作为参数,还接受打印函数:

void processAndConsume(int input, 
                       Function<Integer, Double> processingFunc, 
                                          Consumer<Double> consumer){
  consumer.accept(processingFunc.apply(input));
}

然后我们可以编写以下代码:

Function<Integer, Double> multiplyByFive = createMultiplyBy(5);
Consumer<Double> printResult = createPrintingFunc("Result=", " Great!");
processAndConsume(10, multiplyByFive, printResult); //Result=50.0 Great! 

如前所述,在下一节中,我们将介绍 lambda 表达式,并将展示如何使用它们以更少的代码表示函数接口实现。

供应商

这里有一个技巧性的问题:猜测Supplier<T>接口的抽象方法的输入和输出类型。答案是:不接受参数,返回T类型。正如您现在所理解的,区别在于接口本身的名称。它应该给你一个提示:消费者只是消费,什么也不返回,而供应商只是供应,没有任何输入。Supplier<T>接口的抽象方式为T get()

与前面的函数类似,我们可以编写供应商生成方法:

Supplier<Integer> createSuppplier(int num){
  Supplier<Integer> func = new Supplier<Integer>() {
    public Integer get() { return num; }
  };
  return func;
}

我们现在可以编写一个只接受函数的方法:

void supplyProcessAndConsume(Supplier<Integer> input, 
                             Function<Integer, Double> process, 
                                      Consumer<Double> consume){
  consume.accept(processFunc.apply(input.get()));
}

注意input函数的输出类型如何与process函数的输入相同,后者返回的类型与consume函数使用的类型相同。它使以下代码成为可能:

Supplier<Integer> supply7 = createSuppplier(7);
Function<Integer, Double> multiplyByFive = createMultiplyBy(5);
Consumer<Double> printResult = createPrintingFunc("Result=", " Great!");
supplyProcessAndConsume(supply7, multiplyByFive, printResult); 
                                            //prints: Result=35.0 Great!

在这一点上,我们希望您开始欣赏函数式编程带来的价值。它允许我们传递功能块,这些功能块可以插入算法的中间,而无需创建对象。静态方法也不需要创建对象,但它们由所有应用程序线程共享,因为它们在 JVM 中是唯一的。同时,每个函数都是一个对象,可以是 JVM 中唯一的(如果分配给静态变量),也可以是为每个处理线程创建的(通常是这样)。它的编码开销非常小,在 lambda 表达式中使用时,它的管道甚至更少,这是我们下一节的主题。

到目前为止,我们已经演示了如何将函数插入现有的控制流表达式中。现在我们将描述最后一个缺失的部分——一个表示决策构造的函数,它也可以作为对象传递。

谓词

这是一个表示布尔值函数的接口,该函数有一个方法:boolean test(T)。下面是创建Predicate<Integer>函数的方法示例:

Predicate<Integer> createTestSmallerThan(int num){
  Predicate<Integer> func = new Predicate<Integer>() {
    public boolean test(Integer d) {
      return d < num;
    }
  };
  return func;
}

我们可以使用它为处理方法添加一些逻辑:

void supplyDecideProcessAndConsume(Supplier<Integer> input, 
                                  Predicate<Integer> test, 
                                   Function<Integer, Double> process, 
                                            Consumer<Double> consume){
  int in = input.get();
  if(test.test(in)){
    consume.accept(process.apply(in));
  } else {
    System.out.println("Input " + in + 
                     " does not pass the test and not processed.");
  }
}

下面的代码演示了它的用法:

Supplier<Integer> input = createSuppplier(7);
Predicate<Integer> test = createTestSmallerThan(5);
Function<Integer, Double> multiplyByFive = createMultiplyBy(5);
Consumer<Double> printResult = createPrintingFunc("Result=", " Great!");
supplyDecideProcessAndConsume(input, test, multiplyByFive, printResult);
             //prints: Input 7 does not pass the test and not processed.

让我们将输入设置为 3,例如:

Supplier<Integer> input = createSuppplier(3)

上述代码将产生以下输出:

Result=15.0 Great!

其他标准功能接口

java.util.function包中的其他 39 个功能接口是我们刚刚回顾的四个接口的变体。创建这些变体是为了实现以下一个或任意组合:

  • 通过显式使用整数、双精度或长原语避免自动装箱和取消装箱,从而提高性能
  • 允许两个输入参数
  • 较短的符号

以下只是几个例子:

  • 使用R apply(int)方法的IntFunction<R>提供了更短的符号(输入参数类型没有泛型),并通过要求int原语作为参数避免了自动装箱
  • BiFunction<T,U,R>R apply(T,U)方法允许两个输入参数
  • 使用T apply(T,T)方法的BinaryOperator<T>允许两个T类型的输入参数,并返回一个相同T类型的值
  • 使用int applAsInt(int,int)方法的IntBinaryOperator接受int类型的两个参数,并返回int类型的值

如果您打算使用功能接口,我们鼓励您学习java.util.functional包接口的 API。

链接标准函数

java.util.function包中的大多数函数接口都有默认方法,允许我们构建函数链(也称为管道或管道),将一个函数的结果作为输入参数传递给另一个函数,从而构成一个新的复杂函数。例如:

Function<Double, Long> f1 = d -> Double.valueOf(d / 2.).longValue();
Function<Long, String> f2 = l -> "Result: " + (l + 1);
Function<Double, String> f3 = f1.andThen(f2);
System.out.println(f3.apply(4.));            //prints: 3

从前面的代码中可以看到,我们通过使用andThen()方法组合f1f2函数,创建了一个新的f3函数。这就是我们将在本节中探讨的方法背后的想法。首先,我们将函数表示为匿名类,在下一节中,我们将介绍前面示例中使用的 lambda 表达式。

链二功能

我们可以使用Function接口的andThen(Function after)默认方法。我们已经创建了Function<Integer, Double> createMultiplyBy()方法:

Function<Integer, Double> createMultiplyBy(double num){
  Function<Integer, Double> func = new Function<Integer, Double>(){
    public Double apply(Integer i){
      return i * num;
    }
  };
  return func; 

我们还可以编写另一种方法,创建具有Double输入类型的减法函数,以便将其链接到乘法函数:

private static Function<Double, Long> createSubtractInt(int num){
  Function<Double, Long> func = new Function<Double, Long>(){
    public Long apply(Double dbl){
      return Math.round(dbl - num);
    }
  };
  return func;
}

现在我们可以编写以下代码:

Function<Integer, Double> multiplyByFive = createMultiplyBy(5);
System.out.println(multiplyByFive.apply(2));  //prints: 10.0

Function<Double, Long> subtract7 = createSubtractInt(7);
System.out.println(subtract7.apply(11.0));   //prints: 4

long r = multiplyByFive.andThen(subtract7).apply(2);
System.out.println(r);                          //prints: 3

如您所见,multiplyByFive.andThen(subtract7)链有效地充当Function<Integer, Long> multiplyByFiveAndSubtractSeven

Function接口有另一个默认方法Function<V,R> compose(Function<V,T> before),它也允许我们链接两个函数。必须首先执行的函数可以作为before参数传递到第二个函数的compose()方法中:

boolean r = subtract7.compose(multiplyByFive).apply(2);
System.out.println(r);                          //prints: 3         

链二消费者

Consumer接口也有andThen(Consumer after)方法。我们已经编写了创建打印函数的方法:

Consumer<Double> createPrintingFunc(String prefix, String postfix){
  Consumer<Double> func = new Consumer<Double>() {
    public void accept(Double d) {
      System.out.println(prefix + d + postfix);
    }
  };
  return func;
}

现在我们可以创建并链接两个打印功能,如下所示:

Consumer<Double> print21By = createPrintingFunc("21 by ", "");
Consumer<Double> equalsBy21 = createPrintingFunc("equals ", " by 21");
print21By.andThen(equalsBy21).accept(2d);  
//prints: 21 by 2.0 
//        equals 2.0 by 21

正如您在Consumer链中所看到的,这两个函数在该链定义的序列中使用相同的值。

链二谓词

Supplier接口没有默认方法,Predicate接口有一个静态方法isEqual(Object targetRef)和三个默认方法:and(Predicate other)negate()or(Predicate other)。例如,为了演示and(Predicate other)or(Predicate other)方法的用法,让我们编写创建两个Predicate<Double>函数的方法。一个函数检查值是否小于输入值:

Predicate<Double> testSmallerThan(double limit){
  Predicate<Double> func = new Predicate<Double>() {
    public boolean test(Double num) {
      System.out.println("Test if " + num + " is smaller than " + limit);
      return num < limit;
    }
  };
  return func;
}

另一个函数检查值是否大于输入值:

Predicate<Double> testBiggerThan(double limit){
  Predicate<Double> func = new Predicate<Double>() {
    public boolean test(Double num) {
      System.out.println("Test if " + num + " is bigger than " + limit);
      return num > limit;
    }
  };
  return func;
}

现在我们可以创建两个Predicate<Double>函数并链接它们:

Predicate<Double> isSmallerThan20 = testSmallerThan(20d);
System.out.println(isSmallerThan20.test(10d));
     //prints: Test if 10.0 is smaller than 20.0
     //        true

Predicate<Double> isBiggerThan18 = testBiggerThan(18d);
System.out.println(isBiggerThan18.test(10d));
    //prints: Test if 10.0 is bigger than 18.0
    //        false

boolean b = isSmallerThan20.and(isBiggerThan18).test(10.);
System.out.println(b);
    //prints: Test if 10.0 is smaller than 20.0
    //        Test if 10.0 is bigger than 18.0
    //        false

b = isSmallerThan20.or(isBiggerThan18).test(10.);
System.out.println(b);
    //prints: Test if 10.0 is smaller than 20.0
    //        true

如您所见,and()方法需要执行每个函数,而or()方法没有在链中的第一个函数返回true后立即执行第二个函数。

identity()和其他默认方法

java.util.function包的功能接口还有其他有用的默认方法。最突出的是identity()方法,它返回一个始终返回其输入参数的函数:

Function<Integer, Integer> id = Function.identity();
System.out.println(id.apply(4));          //prints: 4

当某些过程需要提供某个函数,但您不希望所提供的函数更改任何内容时,identity()方法非常有用。在这种情况下,将创建具有必要输出类型的标识函数。例如,在我们前面的一个代码片段中,我们可能会决定multiplyByFive函数不应更改multiplyByFive.andThen(subtract7)链中的任何内容:

Function<Double, Double> multiplyByFive = Function.identity();
System.out.println(multiplyByFive.apply(2.));  //prints: 2.0

Function<Double, Long> subtract7 = createSubtractInt(7);
System.out.println(subtract7.apply(11.0));    //prints: 4

long r = multiplyByFive.andThen(subtract7).apply(2.);
System.out.println(r);                       //prints: -5

如您所见,multiplyByFive函数没有对输入参数2做任何处理,因此结果(减去7后)为-5

其他默认方法主要与转换、装箱和拆箱相关,但也提取两个参数的最小值和最大值。如果您感兴趣,您可以查看java.util.function包接口的 API,并对其可能性有所了解。

Lambda 表达式

上一节中的示例(使用匿名类实现函数接口)看起来很笨重,感觉过于冗长。首先,不需要重复接口名,因为我们已经将其声明为对象引用的类型。其次,对于只有一个抽象方法的函数接口,不需要指定必须实现的方法名称。编译器和 Java 运行时可以解决这个问题。我们所需要的只是提供新的功能。Lambda 表达式就是为了这个目的而引入的。

什么是 lambda 表达式?

lambda 一词来自 lambda 演算——一种通用计算模型,可用于模拟任何图灵机。它是由数学家阿隆佐·丘奇在 20 世纪 30 年代提出的。lambda 表达式是一个函数,在 Java 中作为匿名方法实现,它还允许我们省略修饰符、返回类型和参数类型。这是一种非常紧凑的表示法。

lambda 表达式的语法包括参数列表、箭头标记->和主体。参数列表可以是空的(),没有括号(如果只有一个参数),也可以是用逗号分隔的参数列表,用括号括起来。主体可以是单个表达式或语句块。

让我们看几个例子:

  • () -> 42;始终返回42
  • x -> x + 1;x变量增加1
  • (x, y) -> x * y;x乘以y返回结果
  • (char x) -> x == '$';比较x变量和$符号的值,并返回一个布尔值
  • x -> { System.out.println("x=" + x); };打印带有x=前缀的x

重新实现功能

我们可以使用 lambda 表达式重写上一节中创建的函数,如下所示:

Function<Integer, Double> createMultiplyBy(double num){
  Function<Integer, Double> func = i -> i * num;
  return func;
}
Consumer<Double> createPrintingFunc(String prefix, String postfix){
  Consumer<Double> func = d -> System.out.println(prefix + d + postfix);
  return func;
}
Supplier<Integer> createSuppplier(int num){
  Supplier<Integer> func = () -> num;
  return func;
}
Predicate<Integer> createTestSmallerThan(int num){
  Predicate<Integer> func = d -> d < num;
  return func;
}

我们不重复实现接口的名称,因为它在方法签名中被指定为返回类型。我们也没有指定抽象方法的名称,因为它是必须实现的接口的唯一方法。由于 lambda 表达式和函数接口的结合,编写如此紧凑高效的代码成为可能。

查看前面的示例,您可能会意识到不再需要创建函数的方法。让我们更改调用supplyDecideProcessAndConsume()方法的代码:

void supplyDecideProcessAndConsume(Supplier<Integer> input, 
                                   Predicate<Integer> test, 
                                   Function<Integer, Double> process, 
                                            Consumer<Double> consume){
  int in = input.get();
  if(test.test(in)){
    consume.accept(process.apply(in));
  } else {
    System.out.println("Input " + in + 
                 " does not pass the test and not processed.");
  }
}

让我们重温以下几行:

Supplier<Integer> input = createSuppplier(7);
Predicate<Integer> test = createTestSmallerThan(5);
Function<Integer, Double> multiplyByFive = createMultiplyBy(5);
Consumer<Double> printResult = createPrintingFunc("Result=", " Great!");
supplyDecideProcessAndConsume(input, test, multiplyByFive, printResult);

我们可以将前面的代码更改为以下代码,而无需更改功能:

Supplier<Integer> input = () -> 7;
Predicate<Integer> test = d -> d < 5.;
Function<Integer, Double> multiplyByFive = i -> i * 5.;;
Consumer<Double> printResult = 
                     d -> System.out.println("Result=" + d + " Great!");
supplyDecideProcessAndConsume(input, test, multiplyByFive, printResult); 

我们甚至可以内联前面的函数,并在一行中编写前面的代码,如下所示:

supplyDecideProcessAndConsume(() -> 7, d -> d < 5, i -> i * 5., 
                    d -> System.out.println("Result=" + d + " Great!")); 

请注意,打印函数的定义变得更加透明。这就是 lambda 表达式与函数接口相结合的强大和美妙之处。在第 18 章流和管道中,您将看到 lambda 表达式实际上是处理流数据的唯一方法。

Lambda 限制

我们想指出并澄清 lambda 表达式的两个方面,即:

  • 如果 lambda 表达式使用在其外部创建的局部变量,则该局部变量必须是 final 或有效 final(不在同一上下文中重新赋值)
  • lambda 表达式中的this关键字指的是封闭上下文,而不是 lambda 表达式本身

有效最终局部变量

与在匿名类中一样,在 lambda 表达式外部创建并在内部使用的变量实际上是最终变量,无法修改。您可以编写以下内容:

int x = 7;
//x = 3;       //compilation error
int y = 5;
double z = 5.;
supplyDecideProcessAndConsume(() -> x, d -> d < y, i -> i * z,
            d -> { //x = 3;      //compilation error
                   System.out.println("Result=" + d + " Great!"); } );

但是,正如您所看到的,我们无法更改 lambda 表达式中使用的局部变量的值。这种限制的原因是函数可以在不同的上下文(例如,不同的线程)中传递和执行,而尝试同步这些上下文将破坏无状态函数和表达式的独立分布式计算的原始思想。这就是为什么 lambda 表达式中使用的所有局部变量实际上都是 final,这意味着它们可以显式声明为 final,也可以通过在 lambda 表达式中使用而成为 final。

对于此限制,有一种可能的解决方法。如果局部变量是引用类型(但不是String或原始包装类型),则即使在 lambda 表达式中使用该局部变量,也可以更改其状态:

class A {
  private int x;
  public int getX(){ return this.x; }
  public void setX(int x){ this.x = x; }
}
void localVariable2(){
  A a = new A();
  a.setX(7);
  a.setX(3);
  int y = 5;
  double z = 5.;
  supplyDecideProcessAndConsume(() -> a.getX(), d -> d < y, i -> i * z,
               d -> { a.setX(5);
    System.out.println("Result=" + d + " Great!"); } );
}

但是这种解决方法应该只在真正需要的时候使用,并且必须小心,因为有意外副作用的危险。

这个关键词的解释

匿名类和 lambda 表达式之间的一个主要区别是对this关键字的解释。在匿名类中,它引用匿名类的实例。在 lambda 表达式中,this指围绕该表达式的类的实例,也称为封闭实例封闭上下文封闭范围

让我们编写一个ThisDemo类来说明差异:

class ThisDemo {
  private String field = "ThisDemo.field";
  public void useAnonymousClass() {
    Consumer<String> consumer = new Consumer<>() {
      private String field = "AnonymousClassConsumer.field";
      public void accept(String s) {
        System.out.println(this.field);
      }
    };
    consumer.accept(this.field);
  }
  public void useLambdaExpression() {
    Consumer<String> consumer = consumer = s -> {
      System.out.println(this.field);
    };
    consumer.accept(this.field);
  }

}

如您所见,匿名类中的this表示匿名类实例,而 lambda 表达式中的this表示封闭类实例。Lambda 表达式没有字段,也不能有字段。如果我们执行上述方法,输出将确认我们的假设:

ThisDemo d = new ThisDemo();
d.useAnonymousClass();   //prints: AnonymousClassConsumer.field
d.useLambdaExpression(); //prints: ThisDemo.field

lambda 表达式不是类实例,不能被this引用。根据 Java 规范,这种方法允许实现更大的灵活性,方法是将【这】与周围上下文中的处理方式相同。

方法引用

让我们看一下对supplyDecidePprocessAndConsume()方法调用的最后一个实现:

supplyDecideProcessAndConsume(() -> 7, d -> d < 5, i -> i * 5., 
                    d -> System.out.println("Result=" + d + " Great!")); 

我们使用的函数非常简单。在实际代码中,它们中的每一个都可能需要多行实现。在这种情况下,将代码块内联将使代码几乎不可读。在这种情况下,参考具有必要实现的方法会有所帮助。假设我们有以下Helper类:

public class Helper {
  public double calculateResult(int i){
    // Maybe many lines of code here
    return i* 5;
  }
  public static void printResult(double d){
    // Maybe many lines of code here
    System.out.println("Result=" + d + " Great!");
  }
}

Lambdas类中的 lambda 表达式可以引用Helper类和Lambdas类的方法,如下所示:

public class Lambdas {
  public void methodReference() {
    Supplier<Integer> input = () -> generateInput();
    Predicate<Integer> test = d -> checkValue(d);
    Function<Integer, Double> multiplyByFive = 
                                  i -> new Helper().calculateResult(i);
    Consumer<Double> printResult = d -> Helper.printResult(d);
    supplyDecideProcessAndConsume(input, test, 
                                           multiplyByFive, printResult);
  }
  private int generateInput(){
    // Maybe many lines of code here
    return 7;
  }
  private static boolean checkValue(double d){
    // Maybe many lines of code here
    return d < 5;
  }
}

前面的代码已经读得更好了,函数可以再次内联:

supplyDecideProcessAndConsume(() -> generateInput(), d -> checkValue(d), 
            i -> new Helper().calculateResult(i), Helper.printResult(d));

但在这种情况下,符号可以变得更加紧凑。当一行 lambda 表达式包含对现有方法的引用时,可以通过使用方法引用而不列出参数来进一步简化表示法。

方法引用的语法为Location::methodName,其中Location表示methodName方法的位置(在哪个对象或类中),两个冒号(::用作位置和方法名称之间的分隔符。如果在指定位置有多个同名的方法(由于方法重载),则引用方法由 lambda 表达式实现的函数接口的抽象方法的签名标识。

使用方法引用,Lambdas类中methodReference()方法下的前面代码可以重写如下:

Supplier<Integer> input = this::generateInput;
Predicate<Integer> test = Lambdas::checkValue;
Function<Integer, Double> multiplyByFive = new Helper()::calculateResult;;
Consumer<Double> printResult = Helper::printResult;
supplyDecideProcessAndConsume(input, test, multiplyByFive, printResult);

内联这些函数更有意义:

supplyDecideProcessAndConsume(this::generateInput, Lambdas::checkValue, 
                    new Helper()::calculateResult, Helper::printResult);

您可能已经注意到,为了演示各种可能性,我们特意使用了不同的位置、两种实例方法和两种静态方法。

如果感觉太难记住,那么好消息是一个现代 IDE(IntelliJ IDEA 就是一个例子)可以为您做到这一点,并将您正在编写的代码转换为最紧凑的形式。

练习–使用方法引用创建新对象

使用方法引用来表示创建新对象。让我们假设我们有class A{}。将以下Supplier函数声明替换为另一个使用方法引用的函数声明:

Supplier<A> supplier = () -> new A();

答复

答案是:

Supplier<A> supplier = A::new;

总结

本章介绍了函数式编程的概念。它概述了 JDK 附带的功能接口,并演示了如何使用它们。它还讨论并演示了 lambda 表达式以及它们如何有效地提高代码可读性。

下一章将使读者熟悉数据流处理的强大概念。它解释了什么是流,如何创建流和处理流的元素,以及如何构建处理管道。它还显示了并行组织流处理是多么容易。