Skip to content

Files

Latest commit

eb0ea53 · Sep 21, 2021

History

History
787 lines (574 loc) · 35.1 KB

File metadata and controls

787 lines (574 loc) · 35.1 KB

十三、函数式程序设计

本章将读者带入函数式编程的世界。它解释了什么是函数式接口,概述了 JDK 附带的函数式接口,定义并演示了 Lambda 表达式以及如何将它们用于函数式接口,包括使用方法引用

本章将讨论以下主题:

  • 什么是函数式编程?
  • 标准函数式接口
  • 函数管道
  • Lambda 表达式限制
  • 方法引用

什么是函数式编程?

在前面的章节中,我们实际使用了函数式编程。在第 6 章、“数据结构、泛型和流行工具”中,我们讨论了Iterable接口及其default void forEach (Consumer<T> function)方法,并提供了以下示例:

Iterable<String> list = List.of("s1", "s2", "s3");
System.out.println(list);                       //prints: [s1, s2, s3]
list.forEach(e -> System.out.print(e + " "));   //prints: s1 s2 s3

您可以看到一个Consumer e -> System.out.print(e + " ")函数如何被传递到forEach()方法中,并应用到列表中流入该方法的每个元素。我们将很快讨论Consumer函数。

我们还提到了Collection接口接受函数作为参数的两种方法:

  • default boolean remove(Predicate<E> filter)方法,它试图从集合中删除所有满足给定谓词的元素;Predicate函数接受集合中的一个元素并返回一个boolean
  • default T[] toArray(IntFunction<T[]> generator)方法,返回集合中所有元素的数组,使用提供的IntFunction生成器函数分配返回的数组

在同一章中,我们还提到了List接口的以下方法:

  • default void replaceAll(UnaryOperator<E> operator):将列表中的每个元素替换为将提供的UnaryOperator应用于该元素的结果;UnaryOperator是我们将在本章中回顾的函数之一。

我们描述了Map接口,它的方法default V merge(K key, V value, BiFunction<V,V,V> remappingFunction)以及如何使用它来连接String值:map.merge(key, value, String::concat)BiFunction<V,V,V>接受两个相同类型的参数,并返回相同类型的值。String::concat构造称为方法引用,将在“方法引用”部分中解释。

我们提供了传递Comparator函数的以下示例:

list.sort(Comparator.naturalOrder());
Comparator<String> cmp = (s1, s2) -> s1 == null ? -1 : s1.compareTo(s2);
list.sort(cmp);

取两个String参数,然后将第一个参数与null进行比较。如果第一个参数是null,则返回-1,否则使用compareTo()方法比较第一个参数和第二个参数。

在第 11 章“网络编程”中,我们看了下面的代码:

HttpClient httpClient = HttpClient.newBuilder().build();
HttpRequest req = HttpRequest.newBuilder()
          .uri(URI.create("http://localhost:3333/something")).build();
try {
    HttpResponse<String> resp = 
                        httpClient.send(req, BodyHandlers.ofString());
    System.out.println("Response: " + 
                             resp.statusCode() + " : " + resp.body());
} catch (Exception ex) {
    ex.printStackTrace();
}

BodyHandler对象(函数)由BodyHandlers.ofString()工厂方法生成,并作为参数传入send()方法。在方法内部,代码调用其apply()方法:

BodySubscriber<T> apply​(ResponseInfo responseInfo)

最后,在第 12 章“Java GUI 编程”中,我们在下面的代码片段中使用了一个EventHandler函数作为参数:

btn.setOnAction(e -> { 
                       System.out.println("Bye! See you later!");
                       Platform.exit();
                     }
               );
primaryStage.onCloseRequestProperty()
       .setValue(e -> System.out.println("Bye! See you later!"));

第一个函数是EventHanlder<ActionEvent>。它打印一条消息并强制应用退出。第二个是EventHandler<WindowEvent>函数。它只是打印信息。

所有这些例子都很好地说明了如何构造器并将其作为参数传递。这种能力构成了函数式编程。它存在于许多编程语言中。它不需要管理对象状态。函数是无状态的。它的结果只取决于输入数据,不管调用了多少次。这样的编码使得结果更加可预测,这是函数式编程最吸引人的方面。

从这种设计中受益最大的领域是并行数据处理。函数式编程允许将并行性的责任从客户端代码转移到库中。在此之前,为了处理 Java 集合的元素,客户端代码必须遍历集合并组织处理。在 Java8 中,添加了新的(默认)方法,这些方法接受一个函数作为参数,然后根据内部处理算法将其并行或不并行地应用于集合的每个元素。因此,组织并行处理是库的责任。

什么是函数式接口?

当我们定义一个函数时,实际上,我们提供了一个接口的实现,这个接口只有一个抽象方法。这就是 Java 编译器如何知道将提供的功能放在哪里的原因。编译器查看接口(ConsumerPredicateComparatorIntFunctionUnaryOperatorBiFunctionBodyHandlerEvenHandler在前面的示例中),只看到一个抽象方法,并使用传入的功能作为方法实现。唯一的要求是传入的参数必须与方法签名匹配。否则,将生成编译时错误。

这就是为什么只有一个抽象方法的接口被称为函数式接口。请注意,只有一个抽象方法的要求包括从父接口继承的方法。例如,考虑以下接口:

@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 
interface D extends C {
    void method5();
}

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

为了避免运行时错误,Java8 中引入了@FunctionalInterface注解。它将意图告诉编译器,以便编译器可以检查并查看在带注解的接口中是否确实只有一个抽象方法。此注解还警告读代码的程序员,此接口故意只有一个抽象方法。否则,程序员可能会浪费时间将另一个抽象方法添加到接口中,结果在运行时发现它无法完成。

出于同样的原因,RunnableCallable接口从 Java 早期版本开始就存在,在 Java8 中被注解为@FunctionalInterface。这种区别是明确的,并提醒用户,这些接口可用于创建函数:

@FunctionalInterface
interface Runnable {
    void run(); 
}

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

与任何其他接口一样,函数式接口可以使用匿名类实现:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello!");
    }
};

以这种方式创建的对象以后可以按如下方式使用:

runnable.run();   //prints: Hello!

如果我们仔细看前面的代码,就会发现有不必要的开销。首先,不需要重复接口名称,因为我们已经将其声明为对象引用的类型。其次,对于只有一个抽象方法的函数式接口,不需要指定必须实现的方法名。编译器和 Java 运行时可以解决这个问题。我们只需要提供新的功能。为此特别引入了 Lambda 表达式。

什么是 Lambda 表达式?

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

Lambda 表达式的语法包括参数列表、箭头标记(->和正文。参数列表可以是空的,例如(),不带括号(如果只有一个参数),或者用逗号分隔的参数列表,用括号括起来。主体可以是单个表达式,也可以是大括号内的语句块({}。我们来看几个例子:

  • () -> 42;总是返回42
  • x -> x*42 + 42;x值乘以42,再将42相加返回。
  • (x, y) -> x * y;将传入的参数相乘,返回结果。
  • s -> "abc".equals(s);比较变量s和文字"abc"的值,返回boolean结果值。
  • s -> System.out.println("x=" + s);打印前缀为"x="s值。
  • (i, s) -> { i++; System.out.println(s + "=" + i); };增加输入整数并打印前缀为s + "="``s的新值,作为第二个参数的值。

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

例如,Lambda 表达式允许我们使用Runnable接口重新实现前面的示例,如下所示:

Runnable runnable = () -> System.out.println("Hello!");

如您所见,创建函数式接口很容易,尤其是使用 Lambda 表达式。但在此之前,请考虑使用包java.util.function中提供的 43 个函数式接口之一。这不仅可以让您编写更少的代码,还可以帮助其他熟悉标准接口的程序员更好地理解您的代码。

Lambda 参数的局部变量语法

在 Java11 发布之前,有两种方法可以显式和隐式声明参数类型。下面是一个明确的版本:

BiFunction<Double, Integer, Double> f = (Double x, Integer y) -> x / y;
System.out.println(f.apply(3., 2)); //prints: 1.5

以下是隐式参数类型定义:

BiFunction<Double, Integer, Double> f = (x, y) -> x / y;
System.out.println(f.apply(3., 2));       //prints: 1.5

在前面的代码中,编译器从接口定义推断参数的类型。

在 Java11 中,使用var类型占位符引入了另一种参数类型声明方法,类似于 Java10 中引入的局部变量类型占位符var(参见第 1 章、“Java12 入门”)。

以下参数声明在语法上与 Java11 之前的隐式声明完全相同:

BiFunction<Double, Integer, Double> f = (var x, var y) -> x / y;
System.out.println(f.apply(3., 2));               //prints: 1.5

新的局部变量样式语法允许我们添加注解,而无需显式定义参数类型。让我们向pom.xml文件添加以下依赖项:

<dependency>
    <groupId>org.jetbrains</groupId>
    <artifactId>annotations</artifactId>
    <version>16.0.2</version>
</dependency>

它允许我们将传入的变量定义为非空:

import javax.validation.constraints.NotNull;
import java.util.function.BiFunction;
import java.util.function.Consumer;

BiFunction<Double, Integer, Double> f =
(@NotNull var x, @NotNull var y) -> x / y;
System.out.println(f.apply(3., 2));    //prints: 1.5

注解将程序员的意图传达给编译器,因此如果违反了声明的意图,它可以在编译或执行过程中警告程序员。例如,我们尝试运行以下代码:

BiFunction<Double, Integer, Double> f = (x, y) -> x / y;
System.out.println(f.apply(null, 2));

它在运行时与NullPointerException一起失败。然后我们添加了如下注解:

BiFunction<Double, Integer, Double> f =
        (@NotNull var x, @NotNull var y) -> x / y;
System.out.println(f.apply(null, 2));

运行上述代码的结果如下所示:

Exception in thread "main" java.lang.IllegalArgumentException: 
Argument for @NotNull parameter 'x' of 
com/packt/learnjava/ch13_functional/LambdaExpressions
.lambda$localVariableSyntax$1 must not be null
at com.packt.learnjava.ch13_functional.LambdaExpressions
.$$$reportNull$$$0(LambdaExpressions.java)
at com.packt.learnjava.ch13_functional.LambdaExpressions
.lambda$localVariableSyntax$1(LambdaExpressions.java)
at com.packt.learnjava.ch13_functional.LambdaExpressions
.localVariableSyntax(LambdaExpressions.java:59)
at com.packt.learnjava.ch13_functional.LambdaExpressions
.main(LambdaExpressions.java:12)

Lambda 表达式甚至没有执行。

当参数是具有很长名称的类的对象时,如果我们需要使用注解,那么在 Lambda 参数的情况下局部变量语法的优势就变得很明显了。在 Java11 之前,代码可能如下所示:

BiFunction<SomeReallyLongClassName,
AnotherReallyLongClassName, Double> f =
      (@NotNull SomeReallyLongClassName x,
       @NotNull AnotherReallyLongClassName y) -> x.doSomething(y);

我们必须显式声明变量的类型,因为我们要添加注解,而下面的隐式版本甚至无法编译:

BiFunction<SomeReallyLongClassName,
AnotherReallyLongClassName, Double> f =
           (@NotNull x, @NotNull y) -> x.doSomething(y);

在 Java11 中,新的语法允许我们使用类型持有者var来使用隐式参数类型推断:

BiFunction<SomeReallyLongClassName,
AnotherReallyLongClassName, Double> f =
           (@NotNull var x, @NotNull var y) -> x.doSomething(y);

这就是为 Lambda 参数的声明引入局部变量语法的优势和动机。否则,请考虑不要使用var。如果变量的类型很短,使用它的实际类型可以使代码更容易理解。

标准函数式接口

java.util.function包中提供的大部分接口是以下四种接口的特化:Consumer<T>Predicate<T>Supplier<T>Function<T,R>。让我们回顾一下它们,然后简单地概述一下其他 39 个标准函数式接口。

消费者

通过查看Consumer<T>接口定义,您可能已经猜到这个接口有一个抽象方法,它接受一个T类型的参数,并且不返回任何东西。当只列出一个类型时,它可以定义返回值的类型,就像在Supplier<T>接口中一样。但接口名称作为线索,消费者名称表示该接口的方法只取值,不返回任何值,供应者返回值。这条线索并不精确,但有助于唤起记忆。

关于任何函数式接口的最佳信息源是java.util.function包 API 文档。如果我们读了它,就会知道Consumer<T>接口有一个抽象和一个默认方法:

  • void accept(T t):将操作应用于给定参数
  • default Consumer<T> andThen(Consumer<T> after):返回一个组合的Consumer函数,该函数依次执行当前操作和after操作

这意味着,例如,我们可以实现并执行它,如下所示:

Consumer<String> printResult = s -> System.out.println("Result: " + s);
printResult.accept("10.0");   //prints: Result: 10.0

我们也可以使用工厂方法来创建函数,例如:

Consumer<String> printWithPrefixAndPostfix(String pref, String postf){
    return s -> System.out.println(pref + s + postf);

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

printWithPrefixAndPostfix("Result: ", " Great!").accept("10.0");            
                                           //prints: Result: 10.0 Great!

为了演示andThen()方法,让我们创建类Person

public class Person {
    private int age;
    private String firstName, lastName, record;
    public Person(int age, String firstName, String lastName) {
        this.age = age;
        this.lastName = lastName;
        this.firstName = firstName;
    }
    public int getAge() { return age; }
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public String getRecord() { return record; }
    public void setRecord(String fullId) { this.record = record; }
}

您可能已经注意到,record是唯一具有设置的属性。我们将使用它在消费函数中设置个人记录:

String externalData = "external data";
Consumer<Person> setRecord =
      p -> p.setFullId(p.getFirstName() + " " +
             p.getLastName() + ", " + p.getAge() + ", " + externalData);

setRecord函数获取Person对象属性的值和来自外部源的一些数据,并将结果值设置为record属性值。显然,它可以用其他几种方法来实现,但我们这样做只是为了演示。我们还要创建一个函数来打印record属性:

Consumer<Person> printRecord = p -> System.out.println(p.getRecord());

这两个函数的组合可以按如下方式创建和执行:

Consumer<Person> setRecordThenPrint = setRecord.andThen(printPersonId);
setRecordThenPrint.accept(new Person(42, "Nick", "Samoylov"));   
                         //prints: Nick Samoylov, age 42, external data

这样,就可以创建一个完整的操作处理管道,用于转换通过管道的对象的属性

谓词

这个函数式接口Predicate<T>有一个抽象方法、五个默认值和一个允许谓词链接的静态方法:

  • boolean test(T t):评估提供的参数是否符合标准
  • default Predicate<T> negate():返回当前谓词的否定
  • static <T> Predicate<T> not(Predicate<T> target):返回所提供谓词的否定
  • default Predicate<T> or(Predicate<T> other):从这个谓词和提供的谓词构造一个逻辑OR
  • default Predicate<T> and(Predicate<T> other):从这个谓词和提供的谓词构造一个逻辑AND
  • static <T> Predicate<T> isEqual(Object targetRef):构造谓词,根据Objects.equals(Object, Object)判断两个参数是否相等

此接口的基本用法非常简单:

Predicate<Integer> isLessThan10 = i -> i < 10;
System.out.println(isLessThan10.test(7));      //prints: true
System.out.println(isLessThan10.test(12));     //prints: false

我们也可以将其与之前创建的printWithPrefixAndPostfix(String pref, String postf)函数结合起来:

int val = 7;
Consumer<String> printIsSmallerThan10 = printWithPrefixAndPostfix("Is " 
                               + val + " smaller than 10? ", " Great!");
printIsSmallerThan10.accept(String.valueOf(isLessThan10.test(val)));         
                          //prints: Is 7 smaller than 10? true Great!

其他方法(也称为操作)也可以用于创建操作链(也称为管道),如下例所示:

Predicate<Integer> isEqualOrGreaterThan10 = isLessThan10.negate();
System.out.println(isEqualOrGreaterThan10.test(7));   //prints: false
System.out.println(isEqualOrGreaterThan10.test(12));  //prints: true

isEqualOrGreaterThan10 = Predicate.not(isLessThan10);
System.out.println(isEqualOrGreaterThan10.test(7));   //prints: false
System.out.println(isEqualOrGreaterThan10.test(12));  //prints: true

Predicate<Integer> isGreaterThan10 = i -> i > 10;
Predicate<Integer> is_lessThan10_OR_greaterThan10 = 
                                       isLessThan10.or(isGreaterThan10);
System.out.println(is_lessThan10_OR_greaterThan10.test(20));  // true
System.out.println(is_lessThan10_OR_greaterThan10.test(10));  // false

Predicate<Integer> isGreaterThan5 = i -> i > 5;
Predicate<Integer> is_lessThan10_AND_greaterThan5 = 
                                       isLessThan10.and(isGreaterThan5);
System.out.println(is_lessThan10_AND_greaterThan5.test(3));  // false
System.out.println(is_lessThan10_AND_greaterThan5.test(7));  // true

Person nick = new Person(42, "Nick", "Samoylov");
Predicate<Person> isItNick = Predicate.isEqual(nick);
Person john = new Person(42, "John", "Smith");
Person person = new Person(42, "Nick", "Samoylov");
System.out.println(isItNick.test(john));              //prints: false
System.out.println(isItNick.test(person));            //prints: true

谓词对象可以链接到更复杂的逻辑语句中,并包含所有必要的外部数据,如前面所示。

生产者

这个函数式接口Supplier<T>只有一个抽象方法T get(),返回一个值。基本用法如下:

Supplier<Integer> supply42 = () -> 42;
System.out.println(supply42.get());  //prints: 42

它可以与前面几节中讨论的函数链接:

int input = 7;
int limit = 10;
Supplier<Integer> supply7 = () -> input;
Predicate<Integer> isLessThan10 = i -> i < limit;
Consumer<String> printResult = printWithPrefixAndPostfix("Is " + input + 
                             " smaller than " + limit + "? ", " Great!");
printResult.accept(String.valueOf(isLessThan10.test(supply7.get())));
                           //prints: Is 7 smaller than 10? true Great!

Supplier<T>函数通常用作数据进入处理管道的入口点。

函数

这个和其他返回值的函数式接口的表示法,包括作为泛型列表中最后一个的返回类型的列表(在本例中为R)和它前面的输入数据的类型(在本例中为T类型的输入参数)。因此,符号Function<T, R>表示此接口的唯一抽象方法接受T类型的参数并生成R类型的结果。让我们看看在线文档

Function<T, R>接口有一个抽象方法R apply(T),还有两个操作链接方法:

  • default <V> Function<T,V> andThen(Function<R, V> after):返回一个组合函数,首先将当前函数应用于其输入,然后将after函数应用于结果。
  • default <V> Function<V,R> compose(Function<V, T> before):返回一个组合函数,首先将before函数应用于其输入,然后将当前函数应用于结果。

还有一种identity()方法:

  • static <T> Function<T,T> identity():返回始终返回其输入参数的函数

让我们回顾一下所有这些方法以及如何使用它们。以下是Function<T,R>接口的基本用法示例:

Function<Integer, Double> multiplyByTen = i -> i * 10.0;
System.out.println(multiplyByTen.apply(1));    //prints: 10.0

我们还可以将其与前面几节中讨论的所有功能链接起来:

Supplier<Integer> supply7 = () -> 7;
Function<Integer, Double> multiplyByFive = i -> i * 5.0;
Consumer<String> printResult = 
                       printWithPrefixAndPostfix("Result: ", " Great!");
printResult.accept(multiplyByFive.
        apply(supply7.get()).toString()); //prints: Result: 35.0 Great!

andThen()方法允许从简单函数构造复杂函数。注意下面代码中的divideByTwo.amdThen()行:

Function<Double, Long> divideByTwo = 
                               d -> Double.valueOf(d / 2.).longValue();
Function<Long, String> incrementAndCreateString = 
                                            l -> String.valueOf(l + 1);
Function<Double, String> divideByTwoIncrementAndCreateString = 
                         divideByTwo.andThen(incrementAndCreateString);
printResult.accept(divideByTwoIncrementAndCreateString.apply(4.));
                                             //prints: Result: 3 Great!

它描述了应用于输入值的操作顺序。注意divideByTwo()函数(Long的返回类型如何匹配incrementAndCreateString()函数的输入类型。

compose()方法实现相同的结果,但顺序相反:

Function<Double, String> divideByTwoIncrementAndCreateString =  
                        incrementAndCreateString.compose(divideByTwo);
printResult.accept(divideByTwoIncrementAndCreateString.apply(4.));  
                                            //prints: Result: 3 Great!

现在,复合函数的组合顺序与执行顺序不匹配。如果函数divideByTwo()还没有创建,并且您想在线创建它,那么它可能非常方便。则以下构造将不编译:

Function<Double, String> divideByTwoIncrementAndCreateString =
        (d -> Double.valueOf(d / 2.).longValue())
                                    .andThen(incrementAndCreateString); 

下面一行可以很好地编译:

Function<Double, String> divideByTwoIncrementAndCreateString =
        incrementAndCreateString
                     .compose(d -> Double.valueOf(d / 2.).longValue());

它允许在构建函数管道时具有更大的灵活性,因此在创建下一个操作时,可以以流畅的方式构建它,而不会打断连续的行。

当您需要传入与所需函数签名匹配但不执行任何操作的函数时,identity()方法非常有用。但它只能替换返回与输入类型相同类型的函数。例如:

Function<Double, Double> multiplyByTwo = d -> d * 2.0; 
System.out.println(multiplyByTwo.apply(2.));  //prints: 4.0

multiplyByTwo = Function.identity();
System.out.println(multiplyByTwo.apply(2.));  //prints: 2.0

为了演示其可用性,假设我们有以下处理管道:

Function<Double, Double> multiplyByTwo = d -> d * 2.0;
System.out.println(multiplyByTwo.apply(2.));  //prints: 4.0

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

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

然后,我们决定在某些情况下,multiplyByTwo()函数不应该做任何事情。我们可以给它添加一个条件关闭来打开/关闭它。但是,如果我们想保持函数的完整性,或者如果这个函数是从第三方代码传递给我们的,我们可以只执行以下操作:

Function<Double, Double> multiplyByTwo = d -> d * 2.0;
System.out.println(multiplyByTwo.apply(2.));  //prints: 4.0

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

multiplyByTwo = Function.identity();

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

如您所见,multiplyByTwo()函数现在什么都不做,最终的结果是不同的。

其他标准函数式接口

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

  • 通过显式使用intdoublelong原始类型来避免自动装箱和拆箱,从而获得更好的性能
  • 允许两个输入参数和/或更短的符号

以下只是几个例子:

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

如果您要使用函数式接口,我们鼓励您学习java.util.functional的接口。

Lambda 表达式限制

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

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

与在匿名类中一样,在 Lambda 表达式外部创建并在其中使用的变量实际上是final的,不能修改。以下是试图更改已初始化变量的值而导致的错误示例:

int x = 7;
//x = 3; //compilation error
Function<Integer, Integer> multiply = i -> i * x;

这种限制的原因是一个函数可以在不同的上下文(例如,不同的线程)中传递和执行,而同步这些上下文的尝试将破坏无状态函数的最初想法和表达式的计算,这仅取决于输入参数,而不是上下文变量。这就是为什么 Lambda 表达式中使用的所有局部变量都必须是有效的final,这意味着它们可以显式声明为final,也可以通过不改变值而变为final

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

List<Integer> list = new ArrayList();
list.add(7);
int x = list.get(0);
System.out.println(x);  // prints: 7
list.set(0, 3);
x = list.get(0);
System.out.println(x);  // prints: 3
Function<Integer, Integer> multiply = i -> i * list.get(0);

由于在不同的上下文中执行 Lambda 可能会产生意外的副作用,因此应小心使用此解决方法。

匿名类中的this关键字是指匿名类的实例。相比之下,在 Lambda 表达式中,this关键字是指围绕该表达式的类的实例,也称为封闭实例封闭上下文封闭范围

让我们创建一个ThisDemo类来说明区别:

class ThisDemo {
    private String field = "ThisDemo.field";

    public void useAnonymousClass() {
        Consumer<String> consumer = new Consumer<>() {
            private String field = "Consumer.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);
    }
}

如果执行上述方法,输出将如以下代码注解所示:

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

如您所见,匿名类中的关键字this表示匿名类实例,而 Lambda 表达式中的this表示封闭类实例。Lambda 表达式没有字段,也不能有字段。Lambda 表达式不是类实例,this不能引用。根据 Java 的规范,这种方法通过将this与周围的上下文相同看待,为实现提供了更大的灵活性。

方法引用

到目前为止,我们所有的功能都是简短的一行。下面是另一个例子:

Supplier<Integer> input = () -> 3;
Predicate<Integer> checkValue = d -> d < 5;
Function<Integer, Double> calculate = i -> i * 5.0;
Consumer<Double> printResult = d -> System.out.println("Result: " + d);

if(checkValue.test(input.get())){
    printResult.accept(calculate.apply(input.get()));
} else {
    System.out.println("Input " + input.get() + " is too small.");
} 

如果函数由两行或多行组成,我们可以按如下方式实现它们:

Supplier<Integer> input = () -> {
     // as many line of code here as necessary
     return 3;
};
Predicate<Integer> checkValue = d -> {
    // as many line of code here as necessary
    return d < 5;
};
Function<Integer, Double> calculate = i -> {
    // as many lines of code here as necessary
    return i * 5.0;
};
Consumer<Double> printResult = d -> {
    // as many lines of code here as necessary
    System.out.println("Result: " + d);
};
if(checkValue.test(input.get())){
    printResult.accept(calculate.apply(input.get()));
} else {
    System.out.println("Input " + input.get() + " is too small.");
}

当函数实现的大小超过几行代码时,这样的代码布局可能不容易阅读。它可能会模糊整个代码结构。为了避免此问题,可以将函数实现移到方法中,然后在 Lambda 表达式中引用此方法。例如,让我们向使用 Lambda 表达式的类添加一个静态方法和一个实例方法:

private int generateInput(){
    // Maybe many lines of code here
    return 3;
}
private static boolean checkValue(double d){
    // Maybe many lines of code here
    return d < 5;
}

另外,为了演示各种可能性,让我们用一个静态方法和一个实例方法创建另一个类:

class Helper {
    public double calculate(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);
    }
}

现在我们可以将最后一个示例重写如下:

Supplier<Integer> input = () -> generateInput();
Predicate<Integer> checkValue = d -> checkValue(d);
Function<Integer, Double> calculate = i -> new Helper().calculate(i);
Consumer<Double> printResult = d -> Helper.printResult(d);

if(checkValue.test(input.get())){
    printResult.accept(calculate.apply(input.get()));
} else {
    System.out.println("Input " + input.get() + " is too small.");
}

如您所见,即使每个函数都由许多行代码组成,这样的结构也使代码易于阅读。然而,当一行 Lambda 表达式包含对现有方法的引用时,可以通过使用方法引用而不列出参数来进一步简化表示法。

方法引用的语法是Location::methodName,其中Location表示methodName方法属于哪个对象或类,两个冒号(::作为位置和方法名之间的分隔符。使用方法引用表示法,前面的示例可以重写如下:

Supplier<Integer> input = this::generateInput;
Predicate<Integer> checkValue = MethodReferenceDemo::checkValue;
Function<Integer, Double> calculate = new Helper()::calculate;
Consumer<Double> printResult = Helper::printResult;

if(checkValue.test(input.get())){
    printResult.accept(calculate.apply(input.get()));
} else {
    System.out.println("Input " + input.get() + " is too small.");
}

您可能已经注意到,为了演示各种可能性,我们特意使用了不同的位置、两个实例方法和两个静态方法。如果感觉太难记住,那么好消息是一个现代 IDE(IntelliJ IDEA 就是一个例子)可以帮您完成,并将您正在编写的代码转换为最紧凑的形式。你必须接受 IDE 的建议。

总结

本章通过解释和演示函数式接口和 Lambda 表达式的概念,向读者介绍函数式编程。JDK 附带的标准函数式接口概述帮助读者避免编写自定义代码,而方法引用表示法允许读者编写易于理解和维护的结构良好的代码。

在下一章中,我们将讨论数据流处理。我们将定义什么是数据流,并研究如何处理它们的数据以及如何在管道中链接流操作。具体来说,我们将讨论流的初始化和操作(方法),如何以流畅的方式连接它们,以及如何创建并行流。

测验

  1. 什么是函数式接口?选择所有适用的选项:

  2. 什么是 Lambda 表达式?选择所有适用的选项:

  3. Consumer<T>接口的实现有多少个输入参数?

  4. Consumer<T>接口实现时返回值的类型是什么?

  5. Predicate<T>接口的实现有多少个输入参数?

  6. Predicate<T>接口实现时返回值的类型是什么?

  7. Supplier<T>接口的实现有多少个输入参数?

  8. Supplier<T>接口实现时返回值的类型是什么?

  9. Function<T,R>接口的实现有多少个输入参数?

  10. Function<T,R>接口实现时返回值的类型是什么?

  11. 在 Lambda 表达式中,关键字this指的是什么?

  12. 什么是方法引用语法?