vlambda博客
学习文章列表

JDK17 |java17学习 第 3 部分 高级 Java

Chapter 13: Functional Programming

本章带你进入函数式编程的世界。它解释了什么是函数式接口,概述了 JDK 附带的函数式接口,并定义和演示了 Lambda 表达式以及如何将它们与函数式接口一起使用,包括使用方法参考。

本章将涵盖以下主题:

  • 什么是函数式编程?
  • 标准功能接口
  • 功能管道
  • Lambda 表达式限制
  • 方法参考

在本章结束时,您将能够编写函数并将它们用于 Lambda 表达式,以便将它们作为方法参数传递。

Technical requirements

为了能够执行本章提供的代码示例,您将需要以下内容:

  • 装有 Microsoft Windows、Apple macOS 或 Linux 操作系统的计算机
  • Java SE 版本 17 或更高版本
  • IDE 或您喜欢的任何代码编辑器

第 1 章Java 17 入门。本章的代码示例文件可在 GitHub 上的 https://github .com/PacktPublishing/Learn-Java-17-Programming.gitexamples/src/main/java/com/packt/learnjava/ch13_functional 文件夹中。

What is functional programming?

在我们提供定义之前,让我们用函数式编程元素重新审视代码,我们在前面的章节中已经使用过 。所有这些示例都让您很好地了解了如何构造函数并将其作为参数传递。

第 6 章中,< em class="italic">数据结构、泛型和流行实用程序,我们讨论了 Iterable 接口及其 default void forEach (Consumer 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 方法尝试从集合中移除所有满足给定谓词的元素; Predicate 函数接受集合的一个元素并返回一个布尔值。
  • default T[] toArray(IntFunction 方法使用提供的 IntFunction 生成器函数来分配返回的数组。

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

  • default void replaceAll(UnaryOperator :这会将列表中的每个元素替换为应用提供的 UnaryOperator 的结果到那个元素; UnaryOperator 是我们将在本章中回顾的函数之一。

我们描述了Map接口,它的default V merge(K key, V value, BiFunction 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 章中,< em class="italic">网络编程,我们查看了以下代码:

  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 Programming,我们在下面的代码片段中使用了一个 EventHandler 函数作为参数:

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

第一个函数是EventHandler 。这会打印一条消息并强制应用程序退出。第二个是 EventHandler<WindowEvent> 函数。这只是打印消息。

这种将函数作为参数传递的能力构成了函数式编程。它存在于许多编程语言中,并且不需要管理对象状态。该函数是无状态的。它的结果只取决于输入数据,不管它被调用多少次。这样的编码使结果更可预测,这是函数式编程最吸引人的方面。

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

What is a functional interface?

当我们定义一个函数时,我们提供了一个只有一个抽象方法的接口的实现。这就是 Java 编译器如何知道将提供的功能放在 的位置。编译器查看接口(Consumer, Predicate, Comparator, < code class="literal">IntFunction, UnaryOperator, BiFunction, BodyHandler 和前面示例中的 EventHandler),在那里只看到一个抽象方法,并使用传入的功能作为方法实现。唯一的要求是传入的参数必须与方法签名匹配。否则,会产生编译时错误。

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

@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 接口继承的相同。 C 接口是一个函数式接口,因为它只有一个抽象方法,method1() ,它覆盖了父接口的抽象方法AD 接口不能是函数式接口,因为它有两个抽象方法——来自父接口的method1()Amethod5()

为了帮助避免运行时错误,Java 8 中引入了 @FunctionalInterface 注释。它告诉编译器意图,以便编译器可以检查并查看是否真的只有一个抽象方法在带注释的界面中。该注释还警告阅读代码的程序员,该接口故意只有一个抽象方法。否则,程序员可能会浪费时间向接口添加另一个抽象方法,只是在运行时发现它无法完成。

出于同样的原因,从早期版本开始就存在于 Java 中的 RunnableCallable 接口在 Java 8 中进行了注解作为 @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 表达式。

What is a Lambda expression?

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

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

  • () -> 42; 总是返回 42
  • x -> x*42 + 42;x 值乘以 42,然后加上 42 到结果并返回它。
  • (x, y) -> x * y; 将传入的参数相乘并返回结果。
  • s -> "abc".equals(s); 比较 s 变量和字面量 "abc" 的值;它返回一个布尔结果值。
  • s -> System.out.println("x=" + s); 打印带有前缀 "x="< 的 s 值/代码>。
  • (i, s) -> {我++; System.out.println(s + "=" + i); }; 递增输入整数并打印带有前缀 s + "=" 的新值,带有 s是第二个参数的值。

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

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

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

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

The local variable syntax for Lambda parameters

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

  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

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

在 Java 11 中,引入了另一种参数类型声明的方法,使用 var 类型持有者,它类似于 var 局部变量Java 10 中引入的类型持有者(参见 第 1 章Java 17 入门)。

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

  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>22.0.0</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 参数的情况下的优势变得明显。在 Java 11 之前,代码可能如下所示:

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

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

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

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

Standard functional interfaces

java.util.function 包中提供的大部分接口都是以下四个接口的特化消费者<T>谓词<T>供应商 函数<T,R>。让我们回顾一下它们,然后看一下其他 39 个标准功能接口的简短概述。

Consumer<T>

通过查看Consumer<T>接口定义,<indexentry content="standard functional interfaces:Consumer">,你已经可以猜到这个接口有一个抽象方法,它接受T类型的参数 并且不返回任何内容。好吧,当只列出一种类型时,它可以定义返回值的类型,如 Supplier 接口的情况。但是接口名称是一个线索:consumer 名称表示该接口的方法只取值,不返回任何内容,而 supplier< /code> 返回值。这个线索并不准确,但有助于记忆。

关于任何功能接口的最佳信息来源是 java.util.function 包 API 文档(https://docs.oracle.com/en/java/javase/12/docs /api/java.base/java/util/function/package-summary.html)。如果我们阅读它,我们会发现Consumer<T>接口有一个抽象方法和一个默认方法:

  • void accept(T t):将操作应用于给定的参数
  • 默认消费者<T> andThen(Consumer :返回一个组合的 Consumer函数,按顺序执行当前操作,后面跟着 操作后

也就是说,对于例子,我们可以实现然后执行如下:

  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 对象属性的值和来自外部源的一些数据,并将结果值设置为 < code class="literal">记录属性值。显然,它可以通过其他几种方式完成,但我们这样做是出于演示目的。我们还创建一个打印 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>

这个函数式接口,Predicate ,有一个抽象方法,五个 defaults,以及允许谓词链接的静态方法:

  • boolean test(T t):评估提供的参数,看它是否符合条件
  • 默认谓词<T> negate():返回当前谓词的否定
  • 静态 <T>谓词<T> not(Predicate target) :返回对所提供谓词的否定
  • 默认谓词<T> or(Predicate<T> other):从这个谓词和提供的谓词构造一个逻辑 OR
  • 默认谓词<T> and(Predicate<T> other):从这个谓词和提供的谓词构造一个逻辑 AND
  • 静态 <T>谓词<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>

这个函数式接口,Supplier ,只有一个抽象方法, 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> 函数通常用作进入处理管道的数据的入口点。

Function<T, R>

这个和其他返回值的功能接口的表示法包括将返回 类型列为泛型列表中的最后一个 (本例中为R)和前面输入数据的类型(本例中为T类型的输入参数案子)。因此,Function<T, R> 表示法意味着此接口的唯一抽象方法接受 T 类型的参数,并且产生 R 类型的结果。我们看一下在线文档(https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/util/function/Function。 html)。

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

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

还有一个 identity() 方法:

  • 静态 <T>函数<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.andThen() 行以下代码:

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() 函数应该什么都不做。我们可以添加一个有条件的 close 来打开/关闭它。但是,如果我们想保持函数完整,或者如果这个函数是从第三方代码传递给我们的,我们可以执行以下操作:

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.identity();
System.out.println(multiplyByTwo.apply(2.)); //prints: 2.0;
r = multiplyByTwo.andThen(subtract7).apply(2.);
System.out.println(r);                      //prints: -5

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

Other standard functional interfaces

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

  • 通过显式使用 intdoublelong 避免自动装箱和拆箱,从而提高性能 原语
  • 允许两个输入参数和/或更短的符号

以下是几个例子:

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

如果你打算使用函数式接口,我们建议你学习 java.util.functional 包(https://docs.oracle.com/en /java/javase/12/docs/api/java.base/java/util/function/package-summary.html)。

Lambda expression limitations

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

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

anonymous 类一样,在外部创建并在 Lambda 表达式内部使用的变量变为 实际上是最终的并且无法修改。以下是尝试更改已初始化变量的值导致的错误示例:

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

这种限制的原因是一个函数可以在不同的上下文(例如不同的线程)中传递和执行,并且尝试同步这些上下文会破坏无状态函数的原始想法和表达式的评估,具体取决于仅在输入参数上,而不在上下文变量上。这就是为什么 Lambda 表达式中使用的所有局部变量都必须是有效的最终变量,这意味着它们可以明确地声明 final,也可以成为< /em> 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 关键字在 anonymous 类中是指 anonymous 的实例班级。与 相比,在 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

可以看到,anonymous类里面的this关键字指的是anonymous 实例,而 Lambda 表达式中的 this 指的是封闭类实例。一个 Lambda 表达式只是没有(也不能)有一个字段。 Lambda 表达式不是类实例,不能被 this 引用。根据 Java 的规范,这种方法 通过将 this 视为与周围的上下文相同,从而为实现提供了更大的灵活性

Method references

到目前为止,我们所有的函数都是短的单行函数。这是另一个例子:

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 的建议。

Summary

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

现在,您可以编写函数并将它们用于 Lambda 表达式,以便将它们作为方法参数传递。

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

Quiz

  1. 什么是功能接口?选择所有符合条件的:
    1. 函数集合
    2. 只有一个方法的接口
    3. 任何只有一个抽象方法的接口
    4. 任何用 Java 编写的库
  2. 什么是 Lambda 表达式?选择所有符合条件的:
    1. 一个函数,作为匿名方法实现,没有修饰符、返回类型和参数类型
    2. 函数式接口实现
    3. 任何 Lambda 演算风格的实现
    4. 包含参数列表、箭头标记 (->) 和由单个语句或语句块组成的主体的表示法
  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. 什么是方法引用语法?