From 4aa1d5edab3f3b1799082714f6dc938c5234a121 Mon Sep 17 00:00:00 2001 From: Yibin YANG Date: Sun, 19 Jul 2020 23:55:12 +0200 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B913=E7=AB=A0=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E5=BC=8F=E7=BC=96=E7=A8=8B=E7=AB=A0=E8=8A=82=E4=B8=AD=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=B8=8D=E5=87=86=E7=A1=AE=E7=9A=84=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/13-Functional-Programming.md | 75 +++++++++++++------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/docs/book/13-Functional-Programming.md b/docs/book/13-Functional-Programming.md index beaf61db..db3dba56 100644 --- a/docs/book/13-Functional-Programming.md +++ b/docs/book/13-Functional-Programming.md @@ -202,7 +202,7 @@ from moreLines() - **[4]** 对于多个参数,将参数列表放在括号 `()` 中。 -到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 **return** 关键字是非法的。 这是 Lambda 表达式缩写用于描述功能的语法的另一种方式。 +到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 **return** 关键字是非法的。 这是 Lambda 表达式简化相应语法的另一种方式。 **[5]** 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 **return**。 @@ -255,7 +255,7 @@ public class RecursiveFactorial { 这里,`fact` 是一个静态变量。 注意使用三元 **if-else**。 递归函数将一直调用自己,直到 `i == 0`。所有递归函数都有“停止条件”,否则将无限递归并产生异常。 -我们可以将 `Fibonacci` 序列改为使用递归 Lambda 表达式来实现,这次使用实例变量: +我们可以将 `Fibonacci` 序列用递归的 Lambda 表达式来实现,这次使用实例变量: ```java // functional/RecursiveFibonacci.java @@ -379,9 +379,9 @@ Help! **[9]** 这是 **[6]** 的另一个版本:对已实例化对象的方法的引用,有时称为*绑定方法引用*。 -**[10]** 最后,获取静态内部类的方法引用的操作与 **[8]** 中外部类方式一样。 +**[10]** 最后,获取静态内部类中静态方法的引用与 **[8]** 中通过外部类引用相似。 -上例只是简短的介绍,我们很快就能看到方法引用的全部变化。 +上例只是简短的介绍,我们很快就能看到方法引用的所有不同形式。 ### Runnable接口 @@ -431,7 +431,7 @@ Go::go() ### 未绑定的方法引用 -未绑定的方法引用是指没有关联对象的普通(非静态)方法。 使用未绑定的引用之前,我们必须先提供对象: +未绑定的方法引用是指没有关联对象的普通(非静态)方法。 使用未绑定的引用时,我们必须先提供对象: ```java // functional/UnboundMethodReference.java @@ -469,13 +469,13 @@ X::f() ``` -截止目前,我们已经知道了与接口方法同名的方法引用。 在 **[1]**,我们尝试把 `X` 的 `f()` 方法引用赋值给 **MakeString**。结果:即使 `make()` 与 `f()` 具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。 这是因为实际上还有另一个隐藏的参数:我们的老朋友 `this`。 你不能在没有 `X` 对象的前提下调用 `f()`。 因此,`X :: f` 表示未绑定的方法引用,因为它尚未“绑定”到对象。 +截止目前,我们看到了与对应接口签名相同的方法引用。 在 **[1]**,我们尝试把 `X` 的 `f()` 方法引用赋值给 **MakeString**。结果即使 `make()` 与 `f()` 具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。 这是因为实际上还有另一个隐藏的参数:我们的老朋友 `this`。 你不能在没有 `X` 对象的前提下调用 `f()`。 因此,`X :: f` 表示未绑定的方法引用,因为它尚未“绑定”到对象。 要解决这个问题,我们需要一个 `X` 对象,所以我们的接口实际上需要一个额外的参数,如上例中的 **TransformX**。 如果将 `X :: f` 赋值给 **TransformX**,在 Java 中是允许的。我们必须做第二个心理调整——使用未绑定的引用时,函数式方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 原因是:你需要一个对象来调用方法。 -**[2]** 的结果有点像脑筋急转弯。我拿到未绑定的方法引用,并且调用它的`transform()`方法,将一个X类的对象传递给它,然后就以某种方式导致了对 `x.f()` 的调用。Java知道它必须拿到第一个参数,该参数实际就是`this`,并在其上调用方法。 +**[2]** 的结果有点像脑筋急转弯。我拿到未绑定的方法引用,并且调用它的`transform()`方法,将一个X类的对象传递给它,最后使得 `x.f()` 以某种方式被调用。Java知道它必须拿到第一个参数,该参数实际就是`this`,然后调用方法作用在它之上。 -如果你的函数式接口中的方法有多个参数,就以第一个参数接受`this`的模式来处理。 +如果你的方法有更多个参数,就以第一个参数接受`this`的模式来处理。 ```java // functional/MultiUnbound.java @@ -514,7 +514,7 @@ public class MultiUnbound { } ``` -为了指明这一点,我将类命名为 **This**,将函数式方法的第一个参数命名为 **athis**,但你在生产代码中应该使用其他名字,以防止混淆。 +需要指出的是,我将类命名为 **This**,并将函数式方法的第一个参数命名为 **athis**,但你在生产级代码中应该使用其他名字,以防止混淆。 ### 构造函数引用 @@ -556,9 +556,9 @@ public class CtorReference { } ``` -**Dog** 有三个构造函数,函数接口内的 `make()` 方法反映了构造函数参数列表( `make()` 方法名称可以不同)。 +**Dog** 有三个构造函数,函数式接口内的 `make()` 方法反映了构造函数参数列表( `make()` 方法名称可以不同)。 -**注意**我们如何对 **[1]**,**[2]** 和 **[3]** 中的每一个使用 `Dog :: new`。 这 3 个构造函数只有一个相同名称:`:: new`,但在每种情况下都赋值给不同的接口。编译器可以检测并知道从哪个构造函数引用。 +**注意**我们如何对 **[1]**,**[2]** 和 **[3]** 中的每一个使用 `Dog :: new`。 这三个构造函数只有一个相同名称:`:: new`,但在每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数。 编译器知道调用函数式方法(本例中为 `make()`)就相当于调用构造函数。 @@ -566,7 +566,7 @@ public class CtorReference { ## 函数式接口 -方法引用和 Lambda 表达式必须被赋值,同时编译器需要识别类型信息以确保类型正确。 Lambda 表达式特别引入了新的要求。 代码示例: +方法引用和 Lambda 表达式都必须被赋值,同时赋值需要类型信息才能使编译器保证类型的正确性。尤其是Lambda 表达式,它引入了新的要求。 代码示例: ```java x -> x.toString() @@ -576,13 +576,13 @@ x -> x.toString() Lambda 表达式包含类型推导(编译器会自动推导出类型信息,避免了程序员显式地声明)。编译器必须能够以某种方式推导出 `x` 的类型。 -下面是第 2 个代码示例: +下面是第二个代码示例: ```java (x, y) -> x + y ``` -现在 `x` 和 `y` 可以是任何支持 `+` 运算符连接的数据类型,可以是两个不同的数值类型或者是 1 个 **String** 加任意一种可自动转换为 **String** 的数据类型(这包括了大多数类型)。 但是,当 Lambda 表达式被赋值时,编译器必须确定 `x` 和 `y` 的确切类型以生成正确的代码。 +现在 `x` 和 `y` 可以是任何支持 `+` 运算符连接的数据类型,可以是两个不同的数值类型或者是 一个 **String** 加任意一种可自动转换为 **String** 的数据类型(这包括了大多数类型)。 但是,当 Lambda 表达式被赋值时,编译器必须确定 `x` 和 `y` 的确切类型以生成正确的代码。 该问题也适用于方法引用。 假设你要传递 `System.out :: println` 到你正在编写的方法 ,你怎么知道传递给方法的参数的类型? @@ -632,17 +632,17 @@ public class FunctionalAnnotation { `@FunctionalInterface` 注解是可选的; Java 在 `main()` 中把 **Functional** 和 **FunctionalNoAnn** 都当作函数式接口。 在 `NotFunctional` 的定义中可看到`@FunctionalInterface` 的作用:接口中如果有多个方法则会产生编译期错误。 -仔细观察在定义 `f` 和 `fna` 时发生了什么。 `Functional` 和 `FunctionalNoAnn` 定义接口,然而被赋值的只是方法 `goodbye()`。首先,这只是一个方法而不是类;其次,它甚至都不是实现了该接口的类中的方法。这是添加到Java 8中的一点小魔法:如果将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会自动包装方法引用或 Lambda 表达式到实现目标接口的类的实例中。 +仔细观察在定义 `f` 和 `fna` 时发生了什么。 `Functional` 和 `FunctionalNoAnn` 定义接口,然而被赋值的只是方法 `goodbye()`。首先,这只是一个方法而不是类;其次,它甚至都不是实现了该接口的类中的方法。这是添加到Java 8中的一点小魔法:如果将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会在后台把方法引用或 Lambda 表达式包装进实现目标接口的类的实例中。 -尽管 `FunctionalAnnotation` 确实适合 `Functional` 模型,但 Java不允许我们像`fac`定义中的那样,将 `FunctionalAnnotation` 直接赋值给 `Functional`,因为 `FunctionalAnnotation` 没有明确说明实现 `Functional` 接口。唯一的惊喜是,Java 8 允许我们将函数赋值给接口,这样的语法更加简单漂亮。 +尽管 `FunctionalAnnotation` 确实适合 `Functional` 模型,但 Java不允许我们像`fac`定义中的那样,将 `FunctionalAnnotation` 直接赋值给 `Functional`,因为 `FunctionalAnnotation` 并没有显式地去实现 `Functional` 接口。唯一的惊喜是,Java 8 允许我们将函数赋值给接口,这样的语法更加简单漂亮。 -`java.util.function` 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。这主要是因为基本类型会产生一小部分接口。 如果你了解命名模式,顾名思义就能知道特定接口的作用。 +`java.util.function` 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。主要因为基本类型的存在,导致预定义的接口数量有少许增加。 如果你了解命名模式,顾名思义就能知道特定接口的作用。 以下是基本命名准则: 1. 如果只处理对象而非基本类型,名称则为 `Function`,`Consumer`,`Predicate` 等。参数类型通过泛型添加。 -2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 `LongConsumer`,`DoubleFunction`,`IntPredicate` 等,但基本 `Supplier` 类型例外。 +2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 `LongConsumer`,`DoubleFunction`,`IntPredicate` 等,但返回基本类型的 `Supplier` 接口例外。 3. 如果返回值为基本类型,则用 `To` 表示,如 `ToLongFunction ` 和 `IntToLongFunction`。 @@ -671,13 +671,13 @@ public class FunctionalAnnotation { |2 参数类型不同|**Bi操作**
(不同方法名)|**`BiFunction`
`BiConsumer`
`BiPredicate`
`ToIntBiFunction`
`ToLongBiFunction`
`ToDoubleBiFunction`**| -此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出更多行的函数式接口。 +此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出你所需要的函数式接口。 可以看出,在创建 `java.util.function` 时,设计者们做出了一些选择。 -例如,为什么没有 `IntComparator`,`LongComparator` 和 `DoubleComparator` 呢?有 `BooleanSupplier` 却没有其他表示 **Boolean** 的接口;有通用的 `BiConsumer` 却没有用于 **int**,**long** 和 **double** 的 `BiConsumers` 变体(我对他们放弃的原因表示同情)。这些选择是疏忽还是有人认为其他组合的使用情况出现得很少(他们是如何得出这个结论的)? +例如,为什么没有 `IntComparator`,`LongComparator` 和 `DoubleComparator` 呢?有 `BooleanSupplier` 却没有其他表示 **Boolean** 的接口;有通用的 `BiConsumer` 却没有用于 **int**,**long** 和 **double** 的 `BiConsumers` 变体(我理解他们为什么放弃这些接口)。这到底是疏忽还是有人认为其他组合使用得很少呢(他们是如何得出这个结论的)? -你还可以看到基本类型给 Java 添加了多少复杂性。为了缓和效率问题,该语言的第一版中就包含了基本类型。现在,在语言的生命周期中,我们仍然受到语言设计选择不佳的影响。 +你还可以看到基本类型给 Java 添加了多少复杂性。基于效率方面的考虑(问题之后有所缓解),该语言的第一版中就包含了基本类型。现在,在语言的生命周期中,我们仍然会受到语言设计选择不佳的影响。 下面枚举了基于 Lambda 表达式的所有不同 **Function** 变体的示例: @@ -842,11 +842,11 @@ public class ClassFunctionals { 请**注意**,每个方法名称都是随意的(如 `f1()`,`f2()`等)。正如你刚才看到的,一旦将方法引用赋值给函数接口,我们就可以调用与该接口关联的函数方法。 在此示例中为 `get()`、`compare()`、`accept()`、`apply()` 和 `test()`。 - + ### 多参数函数式接口 -`java.util.functional` 中的接口是有限的。比如有了 `BiFunction`,但它不能变化。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看 Java 库源代码并自行创建。代码示例: +`java.util.functional` 中的接口是有限的。比如有 `BiFunction`,但也仅此而已。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看 Java 库源代码并自行创建。代码示例: ```java // functional/TriFunction.java @@ -872,11 +872,11 @@ public class TriFunctionTest { } ``` -这里我们测试了方法引用和 Lambda 表达式。 +这里我们同时测试了方法引用和 Lambda 表达式。 ### 缺少基本类型的函数 -让我们重温一下 `BiConsumer`,看看我们如何创建缺少 **int**,**long** 和 **double** 的各种排列: +让我们重温一下 `BiConsumer`,看看我们如何创建缺少的针对 **int**,**long** 和 **double** 的各种排列: ```java // functional/BiConsumerPermutations.java @@ -908,7 +908,7 @@ public class BiConsumerPermutations { 这里使用 `System.out.format()` 来显示。它类似于 `System.out.println()` 但提供了更多的显示选项。 这里,`%f` 表示我将 `n` 作为浮点值给出,`%d` 表示 `n` 是一个整数值。 这其中可以包含空格,输入 `%n` 会换行 — 当然使用传统的 `\n` 也能换行,但 `%n` 是自动跨平台的,这是使用 `format()` 的另一个原因。 -上例简单使用了包装类型,装箱和拆箱用于在基本类型之间来回转换。 我们也可以使用包装类型,如 `Function`,而不是预定义的基本类型。代码示例: +上例简单使用了包装类型,装箱和拆箱负责它与基本类型之间的来回转换。 又比如,我们可以将包装类型和`Function`一起使用,而不去用各种针对基本类型的预定义接口。代码示例: ```java // functional/FunctionWithWrapped.java @@ -936,7 +936,7 @@ public interface IntToDoubleFunction { 似乎是考虑到使用频率,某些函数类型并没有预定义。 -当然,如果因缺少基本类型而造成的性能问题,你也可以轻松编写自己的接口( 参考 Java 源代码)——尽管这里出现性能瓶颈的可能性不大。 +当然,如果因为缺少针对基本类型的函数式接口造成了性能问题,你可以轻松编写自己的接口( 参考 Java 源代码)——尽管这里出现性能瓶颈的可能性不大。 ## 高阶函数 @@ -1050,7 +1050,7 @@ O **闭包**(Closure)一词总结了这些问题。 它非常重要,利用闭包可以轻松生成函数。 -考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者叫作在词法上限定范围( 也使用术语*变量捕获* )。Java 8 提供了有限但合理的闭包支持,我们将用一些简单的例子来研究它。 +考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决,那问题将变得非常棘手。 能够解决这个问题的语言被称为**支持闭包**,或者叫作在词法上限定范围( 也使用术语*变量捕获* )。Java 8 提供了有限但合理的闭包支持,我们将用一些简单的例子来研究它。 首先,下列方法返回一个函数,该函数访问对象字段和方法参数: @@ -1067,7 +1067,7 @@ public class Closure1 { } ``` -但是,仔细考虑一下,`i` 的这种用法并非是个大难题,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,被现存函数以这种方式绑定的对象,垃圾收集器肯定会保留[^5]。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们共享 `i` 的存储空间: +但是,仔细考虑一下,`i` 的这种用法并非是个大难题,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,垃圾收集器几乎肯定会保留以这种方式被绑定到现存函数的对象[^5]。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们共享 `i` 的存储空间: ```java // functional/SharedStorage.java @@ -1250,7 +1250,7 @@ public class Closure8 { 请**注意**我已经声明 `ai` 是 `final` 的了。尽管在这个例子中你可以去掉 `final` 并得到相同的结果(试试吧!)。 应用于对象引用的 `final` 关键字仅表示不会重新赋值引用。 它并不代表你不能修改对象本身。 -下面我们来看看 `Closure7.java` 和 `Closure8.java` 之间的区别。我们看到:在 `Closure7.java` 中变量 `i` 有过重新赋值。 也许这就是**等同 final 效果**错误消息的触发点。 +下面我们来看看 `Closure7.java` 和 `Closure8.java` 之间的区别。我们看到:在 `Closure7.java` 中变量 `i` 有过重新赋值。 也许这就是触发**等同 final 效果**错误消息的原因。 ```java // functional/Closure9.java @@ -1380,12 +1380,13 @@ foobaz 正因它产生如此清晰的语法,我在主方法中采用了一些小技巧,并借用了下一章的内容。首先,我创建了一个字符串对象的流,然后将每个对象传递给 `filter()` 操作。 `filter()` 使用 `p4` 的谓词来确定对象的去留。最后我们使用 `forEach()` 将 `println` 方法引用应用在每个留存的对象上。 -从输出结果我们可以看到 `p4` 的工作流程:任何带有 `foo` 的东西都会留下,即使它的长度大于 5。 `fongopuckey` 因长度超出和不包含 `bar` 而被丢弃。 +从输出结果我们可以看到 `p4` 的工作流程:任何带有 `"foo"` 的字符串都得以保留,即使它的长度大于 5。 `"fongopuckey"` 因长度超出且不包含 `bar` 而被丢弃。 + ## 柯里化和部分求值 -[柯里化](https://en.wikipedia.org/wiki/Currying)(Currying)的名称来自于其发明者之一 *Haskell Curry*。他可能是计算机领域唯一名字被命名重要概念的人(另外就是 Haskell 编程语言)。 柯里化意为:将一个多参数的函数,转换为一系列单参数函数。 +[柯里化](https://en.wikipedia.org/wiki/Currying)(Currying)的名称来自于其发明者之一 *Haskell Curry*。他可能是计算机领域唯一姓氏和名字都命名过重要概念的人(另外就是 Haskell 编程语言)。 柯里化意为:将一个多参数的函数,转换为一系列单参数函数。 ```java // functional/CurryingAndPartials.java @@ -1459,9 +1460,9 @@ public class Curry3Args { Hi Ho Hup ``` -对于每个级别的箭头级联(Arrow-cascading),你在类型声明中包裹了另一个 **Function**。 +对于每个级别的箭头级联(Arrow-cascading),你都要在类型声明中包裹另一层 **Function**。 -处理基本类型和装箱时,请使用适当的 **Function** 接口: +处理基本类型和装箱时,请使用适当的函数式接口: ```java // functional/CurriedIntAdd.java @@ -1490,9 +1491,9 @@ public class CurriedIntAdd { ## 纯函数式编程 -即使没有函数式支持,像 C 这样的基础语言,也可以按照一定的原则编写纯函数式程序。Java 8 让函数式编程更简单,不过我们要确保一切是 `final` 的,同时你的所有方法和函数没有副作用。因为 Java 在本质上并非是不可变语言,我们无法通过编译器查错。 +即使没有函数式支持,像 C 这样的基础语言,也可以按照一定的原则编写纯函数式程序。Java 8 让函数式编程更简单,不过我们要确保一切是 `final` 的,同时你的所有方法和函数没有副作用。因为 Java 在本质上并非是不可变语言,所以编译器对我们犯的错误将无能为力。 -这种情况下,我们可以借助第三方工具[^9],但使用 Scala 或 Clojure 这样的语言可能更简单。因为它们从一开始就是为保持不变性而设计的。你可以采用这些语言来编写你的 Java 项目的一部分。如果必须要用纯函数式编写,则可以用 Scala(需要一些规则) 或 Clojure (需要的规则更少)。虽然 Java 支持[并发编程](./24-Concurrent-Programming.md),但如果这是你项目的核心部分,你应该考虑在项目部分功能中使用 `Scala` 或 `Clojure` 之类的语言。 +这种情况下,我们可以借助第三方工具[^9],但使用 Scala 或 Clojure 这样的语言可能更简单。因为它们从一开始就是为保持不变性而设计的。你可以采用这些语言来编写你的 Java 项目的一部分。如果必须要用纯函数式编写,则可以用 Scala(需要遵循一些规则) 或 Clojure (遵循的规则更少)。虽然 Java 支持[并发编程](./24-Concurrent-Programming.md),但如果这是你项目的核心部分,你应该考虑在项目部分功能中使用 `Scala` 或 `Clojure` 之类的语言。 ## 本章小结 @@ -1502,7 +1503,7 @@ Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而 这些特性满足了很多羡慕Clojure、Scala 这类更函数化语言的程序员,并且阻止了Java程序员转向那些更函数化的语言(就算不能阻止,起码提供了更好的选择)。 -但是,Lambdas 和方法引用远非完美,我们永远要为 Java 设计者早期的草率决定付出代价。特别是没有泛型 Lambda,所以 Lambda 在 Java 中并非一等公民。虽然我不否认 Java 8 的巨大改进,但这意味着和许多 Java 特性一样,它的使用还是会让人感觉沮丧和鸡肋。 +但是,Lambdas 和方法引用远非完美,我们永远要为 Java 设计者早期的草率决定付出代价。特别是没有泛型 Lambda,所以 Lambda 在 Java 中并非一等公民。虽然我不否认 Java 8 的巨大改进,但这意味着和许多 Java 特性一样,它终究还是会让人感觉沮丧和鸡肋。 当你遇到学习困难时,请记住通过 IDE(NetBeans、IntelliJ Idea 和 Eclipse)获得帮助,因为 IDE 可以智能提示你何时使用 Lambda 表达式或方法引用,甚至有时还能为你优化代码。