JDK17 |java17学习 第 3 部分 高级 Java
Technical requirements
为了能够执行本章提供的代码示例,您将需要以下内容:
- 装有 Microsoft Windows、Apple macOS 或 Linux 操作系统的计算机
- Java SE 版本 17 或更高版本
- IDE 或您喜欢的任何代码编辑器
第 1 章,Java 17 入门。本章的代码示例文件可在 GitHub 上的 https://github .com/PacktPublishing/Learn-Java-17-Programming.git 在 examples/src/main/java/com/packt/learnjava/ch13_functional
文件夹中。
What is functional programming?
在我们提供定义之前,让我们用函数式编程元素重新审视代码,我们在前面的章节中已经使用过 。所有这些示例都让您很好地了解了如何构造函数并将其作为参数传递。
在第 6 章中,< em class="italic">数据结构、泛型和流行实用程序,我们讨论了 Iterable
接口及其 default void forEach (Consumer
方法,并提供以下示例:
你可以看到一个 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
方法,以及它如何用于连接 String
值:map.merge(key, value, String::concat)
。
BiFunction<V,V,V>
接受两个相同类型的参数,并返回 相同类型的值。 String::concat
构造 被称为 方法引用在方法参考部分解释。
我们提供了以下传递 Comparator
函数的示例:
它接受两个 String
参数,然后将第一个参数与 null
进行比较。如果第一个参数为null
,则函数返回-1
;否则,它使用 compareTo()
方法比较第一个参数和第二个参数。
在第 11 章中,< em class="italic">网络编程,我们查看了以下代码:
BodyHandler
对象(一个函数)由 BodyHandlers.ofString()
工厂方法生成并传入 send()
方法作为 参数。在方法内部,代码调用它的 apply()
方法:
最后,在 第 12 章 , Java GUI Programming,我们在下面的代码片段中使用了一个 EventHandler
函数作为参数:
第一个函数是EventHandler
。这会打印一条消息并强制应用程序退出。第二个是 EventHandler<WindowEvent>
函数。这只是打印消息。
这种将函数作为参数传递的能力构成了函数式编程。它存在于许多编程语言中,并且不需要管理对象状态。该函数是无状态的。它的结果只取决于输入数据,不管它被调用多少次。这样的编码使结果更可预测,这是函数式编程最吸引人的方面。
从这种设计中受益最多的领域是并行数据处理。函数式编程允许将并行性的责任从客户端代码转移到库。在此之前,为了处理 Java 集合的元素,客户端代码必须遍历集合并组织处理。在 Java 8 中,添加了新的(默认)方法 ,它们接受一个函数作为参数,然后将其并行或不并行应用于集合的每个元素,具体取决于内部处理算法。因此,组织并行处理是图书馆的责任。
What is a functional interface?
当我们定义一个函数时,我们提供了一个只有一个抽象方法的接口的实现。这就是 Java 编译器如何知道将提供的功能放在 的位置。编译器查看接口(Consumer
, Predicate
, Comparator
, < code class="literal">IntFunction, UnaryOperator
, BiFunction
, BodyHandler
和前面示例中的 EventHandler
),在那里只看到一个抽象方法,并使用传入的功能作为方法实现。唯一的要求是传入的参数必须与方法签名匹配。否则,会产生编译时错误。
这就是为什么任何只有一个抽象方法的接口都被称为功能接口。请注意,只有一个抽象方法的要求包括从父接口继承的方法。例如,考虑以下接口:
A
接口是一个函数式接口,因为它只有一个抽象方法,method1()
。 B
接口也是一个函数式接口,因为它只有一个抽象方法——与从 A
接口继承的相同。 C
接口是一个函数式接口,因为它只有一个抽象方法,method1()
,它覆盖了父接口的抽象方法A
。 D
接口不能是函数式接口,因为它有两个抽象方法——来自父接口的method1()
,A
和 method5()
。
为了帮助避免运行时错误,Java 8 中引入了 @FunctionalInterface
注释。它告诉编译器意图,以便编译器可以检查并查看是否真的只有一个抽象方法在带注释的界面中。该注释还警告阅读代码的程序员,该接口故意只有一个抽象方法。否则,程序员可能会浪费时间向接口添加另一个抽象方法,只是在运行时发现它无法完成。
出于同样的原因,从早期版本开始就存在于 Java 中的 Runnable
和 Callable
接口在 Java 8 中进行了注解作为 @FunctionalInterface
。这种区别是明确的,并提醒用户这些接口可用于创建函数:
以这种方式创建的对象以后可以按如下方式使用:
如果我们仔细查看前面的代码,我们会注意到存在不必要的开销。首先,不需要重复接口名称,因为我们已经将它声明为对象引用的类型。其次,在只有一个抽象方法的功能接口的情况下,不需要指定必须实现的方法名称。编译器和 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
接口重新实现前面的示例,如下所示:
如您所见,创建 函数式接口很容易,尤其是使用 Lambda 表达式的 。但在此之前,请考虑使用 java.util.function
包中提供的 43 个功能接口之一。这不仅可以让您编写更少的代码,还可以帮助其他熟悉标准接口的程序员更好地理解您的代码。
The local variable syntax for Lambda parameters
在 Java 11 发布之前,有两种方法可以声明参数类型——显式和隐式。这是一个明确的版本:
以下是隐式参数类型定义:
在前面的代码中,编译器从接口定义中推断出参数的类型。
在 Java 11 中,引入了另一种参数类型声明的方法,使用 var
类型持有者,它类似于 var
局部变量Java 10 中引入的类型持有者(参见 第 1 章,Java 17 入门)。
以下参数声明在语法上与 Java 11 之前的隐式声明完全相同:
新的局部变量样式语法允许我们在不显式定义参数类型的情况下添加注释。让我们将以下依赖项添加到 pom.xml
文件中:
注释将程序员的意图传达给编译器,因此如果声明的意图被违反,它可以在编译或执行期间警告程序员。例如,我们尝试运行以下代码:
它在运行时因 NullPointerException
而失败。然后,我们添加了如下注解:
运行上述代码的结果如下所示:
Lambda 表达式甚至没有被执行。
如果我们需要在参数是具有非常长名称的类的对象时使用 注释,则局部变量语法在 Lambda 参数的情况下的优势变得明显。在 Java 11 之前,代码可能如下所示:
我们必须显式声明变量的类型,因为我们想添加注释,而以下隐式版本甚至无法编译:
在 Java 11 中,新的 语法允许我们使用 var
类型持有者使用隐式参数类型推断:
这就是为 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
函数,按顺序执行当前操作,后面跟着
我们还可以有一个创建函数的工厂方法,例如:
现在,我们可以按如下方式使用它:
为了演示 andThen()
方法,让我们创建 Person
类:
您可能 注意到 record
是 唯一具有设置的属性.我们将使用它在消费者函数中设置个人记录:
setRecord
函数获取 Person
对象属性的值和来自外部源的一些数据,并将结果值设置为 < code class="literal">记录属性值。显然,它可以通过其他几种方式完成,但我们这样做是出于演示目的。我们还创建一个打印 record
属性的函数:
这两个函数的组合可以按如下方式创建和执行:
通过这种方式,可以创建一个完整的处理操作的管道,这些操作转换通过的对象的属性管道。
Predicate<T>
这个函数式接口,Predicate
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)
构造一个判断两个参数是否相等的谓词
这个接口的基本使用非常简单:
我们可以也可以将与之前创建的printWithPrefixAndPostfix(String pref, String postf)< /代码>功能:
其他方法(也称为操作)可用于创建操作链(也称为管道)和可以在下面的例子中看到:
谓词
对象可以链接成更复杂的逻辑语句,包括所有必要的外部数据,如前所述。
Supplier<T>
这个函数式接口,Supplier
T get( )
,其中
返回一个值。基本用法可以看如下:
它可以与前几节中讨论的函数链接:
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>
接口的基本用法示例:
我们还可以将它与我们在前面几节中讨论过的所有函数链接起来:
andThen()
方法允许从更简单的函数构造复杂函数。请注意 中的 divideByTwo.andThen()
行以下代码:
它描述了应用于输入值的操作顺序。注意 divideByTwo()
函数 (Long
) 的返回类型如何匹配 incrementAndCreateString()
函数。
compose()
方法完成相同的结果,但顺序相反:
现在,复杂函数的组成顺序与执行顺序不匹配。在尚未创建 divideByTwo()
函数并且您想内联创建它的情况下,这可能非常方便。然后,以下构造将无法编译:
以下行将编译得很好:
它在构建功能管道时提供了更大的灵活性,因此您可以在创建下一个操作时以流畅的样式构建它而不会破坏连续线。
identity()
方法在您需要传入与所需函数签名匹配但不执行任何操作的函数时很有用。但是,它只能替换返回与输入类型相同类型的函数,如下例所示:
为了证明它的可用性,假设我们有以下处理管道:
然后,我们决定在某些情况下,multiplyByTwo()
函数应该什么都不做。我们可以添加一个有条件的 close 来打开/关闭它。但是,如果我们想保持函数完整,或者如果这个函数是从第三方代码传递给我们的,我们可以执行以下操作:
如您所见,multiplyByTwo()
函数现在什么都不做,最终结果不同。
Other standard functional interfaces
java.util.function
包中的其他 39 个函数接口是我们刚刚回顾的 四个接口的变体。创建这些变体是为了实现以下一项或任意组合:
- 通过显式使用
int
、double
或long 避免自动装箱和拆箱,从而提高性能
原语 - 允许两个输入参数和/或更短的符号
IntFunction<R>
和R apply(int)
方法提供更短的表示法(没有输入参数类型的泛型)和通过要求原语int
作为参数来避免自动装箱。BiFunction
和R apply(T,U)
方法允许两个输入参数;BinaryOperator<T>
和T apply(T,T)
方法允许两个T
并返回相同类型的值T
。IntBinaryOperator
和int 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 表达式中的
this
关键字指的是封闭上下文,而不是 Lambda 表达式本身。
与 anonymous
类一样,在外部创建并在 Lambda 表达式内部使用的变量变为 实际上是最终的并且无法修改。以下是尝试更改已初始化变量的值导致的错误示例:
这种限制的原因是一个函数可以在不同的上下文(例如不同的线程)中传递和执行,并且尝试同步这些上下文会破坏无状态函数的原始想法和表达式的评估,具体取决于仅在输入参数上,而不在上下文变量上。这就是为什么 Lambda 表达式中使用的所有局部变量都必须是有效的最终变量,这意味着它们可以明确地声明 final,也可以成为< /em> final 由于不改变值。
不过,有一种可能的解决方法来解决此限制。如果局部变量是引用类型(但不是 String
或原始包装类型),则可以更改其状态,即使此局部变量在 Lambda 中使用表达:
应谨慎使用此解决方法,因为如果此 Lambda 在不同的上下文中执行,则存在意外副作用的危险。
this
关键字在 anonymous
类中是指 anonymous
的实例班级。与 相比,在 Lambda 表达式中,this
关键字指的是 类的实例,它围绕着 表达式,也称为 封闭实例 、封闭上下文或封闭范围。
让我们创建一个 ThisDemo
类来说明差异:
如果我们执行上述方法,输出将如以下代码注释所示:
可以看到,anonymous
类里面的this
关键字指的是anonymous
类 实例,而 Lambda 表达式中的
this
指的是封闭类实例。一个 Lambda 表达式只是没有(也不能)有一个字段。 Lambda 表达式不是类实例,不能被 this
引用。根据 Java 的规范,这种方法 通过将 this
视为与周围的上下文相同,从而为实现提供了更大的灵活性。
Method references
到目前为止,我们所有的函数都是短的单行函数。这是另一个例子:
如果函数由两行或多行组成,我们可以按如下方式实现它们:
当函数实现的大小超过几行代码时,这样的代码布局可能不会易于阅读。它可能会掩盖整个代码结构。为了避免这个问题,可以将函数实现移动到一个方法中,然后在 Lambda 表达式中引用这个方法。例如,让我们在使用 Lambda 表达式的类中添加一个静态方法和一个实例方法:
另外,为了演示的各种可能性,让我们创建另一个类,具有一个静态方法和一个实例方法:
现在,我们可以将最后一个示例改写如下:
如您所见,即使每个函数都由多行代码组成,这种结构仍使代码易于阅读。然而,当单行 Lambda 表达式包含对现有方法的引用时,可以通过使用方法引用而不列出参数来进一步简化表示法。
方法引用的语法是Location::methodName
,其中Location
表示methodName
方法所属,两个冒号 (::
) 作为位置 之间的分隔符和方法名称。使用方法引用表示法,前面的示例可以重写如下:
您可能已经注意到,我们故意使用了不同的位置、两个实例方法和 两个静态方法来展示各种可能性。如果感觉太多难以记住,好消息是现代 IDE(IntelliJ IDEA 就是一个例子)可以为您完成这项工作,并将您编写的代码转换为最紧凑的形式。你只需要接受 IDE 的建议。
Quiz
- 什么是功能接口?选择所有符合条件的:
- 函数集合
- 只有一个方法的接口
- 任何只有一个抽象方法的接口
- 任何用 Java 编写的库
- 什么是 Lambda 表达式?选择所有符合条件的:
- 一个函数,作为匿名方法实现,没有修饰符、返回类型和参数类型
- 函数式接口实现
- 任何 Lambda 演算风格的实现
- 包含参数列表、箭头标记 (->) 和由单个语句或语句块组成的主体的表示法
Consumer<T>
接口的实现有多少个输入参数?Consumer<T>
接口的实现中返回值的类型是什么?Predicate<T>
接口的实现有多少个输入参数?Predicate<T>
接口的实现中返回值的类型是什么?Supplier<T>
接口的实现有多少个输入参数?Supplier<T>
接口的实现中返回值的类型是什么?Function<T,R>
接口的实现有多少个输入参数?Function<T,R>
接口的实现中返回值的类型是什么?- 在 Lambda 表达式中,
this
关键字指的是什么? - 什么是方法引用语法?