From d7c1f08c5f015464eef37f15fdc2d87f1ebe62fe Mon Sep 17 00:00:00 2001 From: legendyql Date: Fri, 15 May 2020 00:15:02 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E7=AC=AC13=E7=AB=A0=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改第13章翻译中拗口、错误的地方 --- docs/book/13-Functional-Programming.md | 42 ++++++++++++++------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/book/13-Functional-Programming.md b/docs/book/13-Functional-Programming.md index 3ddd4493..6c6e3ba8 100644 --- a/docs/book/13-Functional-Programming.md +++ b/docs/book/13-Functional-Programming.md @@ -7,9 +7,9 @@ > 函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许你以函数式编程。 -在计算机时代早期,内存是稀缺和昂贵的。几乎每个人都用汇编语言编程。人们对编译器有所了解,但仅仅想到编译生成的代码肯定会比手工编码多很多字节。 +在计算机时代早期,内存是稀缺和昂贵的。几乎每个人都用汇编语言编程。人们虽然知道编译器,但编译器生成的代码很低效,比手工编码的汇编程序多很多字节,仅仅想到这一点,人们还是选择汇编语言。 -通常,只是为了使程序适合有限的内存,程序员通过修改内存中的代码来节省代码空间,以便在程序执行时执行不同的操作。这种技术被称为**自修改代码** (self-modifying code)。只要程序足够小,少数人可以维护所有棘手和神秘的汇编代码,你就可以让它运行起来。 +通常,为了使程序能在有限的内存上运行,在程序运行时,程序员通过修改内存中的代码,使程序可以执行不同的操作,用这种方式来节省代码空间。这种技术被称为**自修改代码** (self-modifying code)。只要程序小到几个人就能够维护所有棘手和难懂的汇编代码,你就能让程序运行起来。 随着内存和处理器变得更便宜、更快。C 语言出现并被大多数汇编程序员认为更“高级”。人们发现使用 C 可以显著提高生产力。同时,使用 C 创建自修改代码仍然不难。 @@ -17,7 +17,7 @@ 然而,使用代码以某种方式操纵其他代码的想法也很有趣,只要能保证它更安全。从代码创建,维护和可靠性的角度来看,这个想法非常吸引人。我们不用从头开始编写大量代码,而是从易于理解、充分测试及可靠的现有小块开始,最后将它们组合在一起以创建新代码。难道这不会让我们更有效率,同时创造更健壮的代码吗? -这就是**函数式编程**(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。至少在某些情况下,这套理论似乎很有用。在这一过程中,一些非函数式语言已经习惯了使用函数式编程产生的优雅的语法。 +这就是**函数式编程**(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。至少在某些情况下,这套理论似乎很有用。在这一过程中,函数式语言已经产生了优雅的语法,这些语法对于非函数式语言也适用。 你也可以这样想: @@ -471,9 +471,11 @@ X::f() 截止目前,我们已经知道了与接口方法同名的方法引用。 在 **[1]**,我们尝试把 `X` 的 `f()` 方法引用赋值给 **MakeString**。结果:即使 `make()` 与 `f()` 具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。 这是因为实际上还有另一个隐藏的参数:我们的老朋友 `this`。 你不能在没有 `X` 对象的前提下调用 `f()`。 因此,`X :: f` 表示未绑定的方法引用,因为它尚未“绑定”到对象。 -要解决这个问题,我们需要一个 `X` 对象,所以我们的接口实际上需要一个额外的参数的接口,如上例中的 **TransformX**。 如果将 `X :: f` 赋值给 **TransformX**,这在 Java 中是允许的。这次我们需要调整下心里预期——使用未绑定的引用时,函数方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 理由是:你需要一个对象来调用方法。 +要解决这个问题,我们需要一个 `X` 对象,所以我们的接口实际上需要一个额外的参数,如上例中的 **TransformX**。 如果将 `X :: f` 赋值给 **TransformX**,在 Java 中是允许的。我们必须做第二个心理调整——使用未绑定的引用时,函数式方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 原因是:你需要一个对象来调用方法。 -**[2]** 的结果有点像脑筋急转弯。 我接受未绑定的引用并对其调用 `transform()`,将其传递给 `X`,并以某种方式导致对 `x.f()` 的调用。 Java 知道它必须采用第一个参数,这实际上就是 `this`,并在其上调用方法。 +**[2]** 的结果有点像脑筋急转弯。我拿到未绑定的方法引用,并且调用它的`transform()`方法,将一个X类的对象传递给它,然后就以某种方式导致了对 `x.f()` 的调用。Java知道它必须拿到第一个参数,该参数实际就是`this`,并在其上调用方法。 + +如果你的函数式接口中的方法有多个参数,就以第一个参数接受`this`的模式来处理。 ```java // functional/MultiUnbound.java @@ -512,7 +514,7 @@ public class MultiUnbound { } ``` -为了说明这一点,我将类命名为 **This** ,函数方法的第一个参数则是 **athis**,但是你应该选择其他名称以防止生产代码混淆。 +为了指明这一点,我将类命名为`This`,将函数式方法的第一个参数命名为`athis`,但你在生产代码中应该使用其他名字,以防止混淆。 ### 构造函数引用 @@ -558,7 +560,7 @@ public class CtorReference { **注意**我们如何对 **[1]**,**[2]** 和 **[3]** 中的每一个使用 `Dog :: new`。 这 3 个构造函数只有一个相同名称:`:: new`,但在每种情况下都赋值给不同的接口。编译器可以检测并知道从哪个构造函数引用。 -编译器能识别并调用你的构造函数( 在本例中为 `make()`)。 +编译器知道调用函数式方法(本例中为 `make()`)就相当于调用构造函数。 ## 函数式接口 @@ -628,11 +630,11 @@ public class FunctionalAnnotation { } ``` -`@FunctionalInterface` 注解是可选的; Java 在 `main()` 中把 **Functional** 和 **FunctionalNoAnn** 都当作函数式接口。 `@FunctionalInterface` 的值在 `NotFunctional` 的定义中可见:接口中如果有多个方法则会产生编译时错误消息。 +`@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 不允许我们将 `FunctionalAnnotation` 像 `fac` 定义一样直接赋值给 `Functional`,因为它没有明确地实现 `Functional` 接口。 令人惊奇的是 ,Java 8 允许我们以简便的语法为接口赋值函数。 +尽管 `FunctionalAnnotation` 确实适合 `Functional` 模型,但 Java不允许我们像`fac`定义中的那样,将 `FunctionalAnnotation` 直接赋值给 `Functional`,因为 `FunctionalAnnotation` 没有明确说明实现 `Functional` 接口。唯一的惊喜是,Java 8 允许我们将函数赋值给接口,这样的语法更加简单漂亮。 `java.util.function` 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。这主要是因为基本类型会产生一小部分接口。 如果你了解命名模式,顾名思义就能知道特定接口的作用。 @@ -790,7 +792,7 @@ someOtherName() 因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法名(在本例中为 `accept()`),而不是你的方法名。 -现在我们来看看所有基于类的函数式,应用于方法引用(即那些不涉及基本类型的函数)。下例我们创建了一个最简单的函数式签名。代码示例: +现在我们来看看,将方法引用应用于基于类的函数式接口(即那些不包含基本类型的函数式接口)。下面的例子中,我创建了适合函数式方法签名的最简单的方法: ```java // functional/ClassFunctionals.java @@ -930,7 +932,7 @@ public interface IntToDoubleFunction { } ``` -之所以我们可以简单地编写 `Function ` 并返回合适的结果,很明显是为了性能。使用基本类型可以防止传递参数和返回结果过程中的自动装箱和自动拆箱。 +因为我们可以简单地写 `Function ` 并产生正常的结果,所以用基本类型的唯一原因是可以避免传递参数和返回结果过程中的自动装箱和自动拆箱。用基本类型的目的是为了性能。 似乎是考虑到使用频率,某些函数类型并没有预定义。 @@ -1050,7 +1052,7 @@ O 考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者叫作在词法上限定范围( 也使用术语*变量捕获* )。Java 8 提供了有限但合理的闭包支持,我们将用一些简单的例子来研究它。 -首先,下例函数中,方法返回访问对象字段和方法参数。代码示例: +首先,下列方法返回一个函数,该函数访问对象字段和方法参数: ```java // functional/Closure1.java @@ -1065,7 +1067,7 @@ public class Closure1 { } ``` -但是,仔细考虑一下,`i` 的这种用法并非是个大难题,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上[^5]。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们共享 `i` 的存储空间: +但是,仔细考虑一下,`i` 的这种用法并非是个大难题,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,被现存函数以这种方式绑定的对象,垃圾收集器肯定会保留[^5]。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们共享 `i` 的存储空间: ```java // functional/SharedStorage.java @@ -1109,7 +1111,7 @@ public class Closure2 { } ``` -由 `makeFun()` 返回的 `IntSupplier` “关闭” `i` 和 `x`,因此当你调用返回的函数时两者仍然有效。 但请**注意**,我没有像 `Closure1.java` 那样递增 `i`,因为会产生编译时错误。代码示例: +由 `makeFun()` 返回的 `IntSupplier` “关住了” `i` 和 `x`,因此即使`makeFun()`已执行完毕,当你调用返回的函数时`i` 和 `x`仍然有效,而不是像正常情况下那样在 `makeFun()` 执行后 `i` 和`x`就消失了。 但请注意,我没有像 `Closure1.java` 那样递增 `i`,因为会产生编译时错误。代码示例: ```java // functional/Closure3.java @@ -1126,7 +1128,7 @@ public class Closure3 { } ``` -`x` 和 `i` 的操作都犯了同样的错误:从 Lambda 表达式引用的局部变量必须是 `final` 或者是等同 `final` 效果的。 +`x` 和 `i` 的操作都犯了同样的错误:被 Lambda 表达式引用的局部变量必须是 `final` 或者是等同 `final` 效果的。 如果使用 `final` 修饰 `x`和 `i`,就不能再递增它们的值了。代码示例: @@ -1189,7 +1191,7 @@ public class Closure6 { 上例中 `iFinal` 和 `xFinal` 的值在赋值后并没有改变过,因此在这里使用 `final` 是多余的。 -如果这里是引用的话,需要把 **int** 型更改为 **Integer** 型。代码示例: +如果函数式方法中使用的外部局部变量是引用,而不是基本类型的话,会是什么情况呢?我们可以把`int`类型改为`Integer`类型研究一下: ```java // functional/Closure7.java @@ -1206,7 +1208,7 @@ public class Closure7 { } ``` -编译器非常智能,它能识别变量 `i` 的值被更改过了。 对于包装类型的处理可能比较特殊,因此我们尝试下 **List**: +编译器非常聪明地识别到变量 `i` 的值被更改过。 因为包装类型可能被特殊处理过了,所以我们尝试下 **List**: ```java // functional/Closure8.java @@ -1244,7 +1246,7 @@ public class Closure8 { [1, 96] ``` -可以看到,这次一切正常。我们改变了 **List** 的值却没产生编译时错误。通过观察本例的输出结果,我们发现这看起来非常安全。这是因为每次调用 `makeFun()` 时,其实都会创建并返回一个全新的 `ArrayList`。 也就是说,每个闭包都有自己独立的 `ArrayList`, 它们之间互不干扰。 +可以看到,这次一切正常。我们改变了 **List** 的内容却没产生编译时错误。通过观察本例的输出结果,我们发现这看起来非常安全。这是因为每次调用 `makeFun()` 时,其实都会创建并返回一个全新而非共享的 `ArrayList`。也就是说,每个闭包都有自己独立的 `ArrayList`,它们之间互不干扰。 请**注意**我已经声明 `ai` 是 `final` 的了。尽管在这个例子中你可以去掉 `final` 并得到相同的结果(试试吧!)。 应用于对象引用的 `final` 关键字仅表示不会重新赋值引用。 它并不代表你不能修改对象本身。 @@ -1498,7 +1500,7 @@ public class CurriedIntAdd { Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。这对 Java 来说是一个巨大的改进。因为这允许你编写更简洁明了,易于理解的代码。在下一章中,你会看到它们在流式编程中的应用。相信你会像我一样,喜欢上流式编程。 -这些特性满足大部分 Java 程序员的需求。他们开始羡慕嫉妒 Clojure、Scala 这类新语言的功能,并试图阻止 Java 程序员流失到其他阵营 (就算不能阻止,起码提供了更好的选择)。 +这些特性满足了很多羡慕Clojure、Scala 这类更函数化语言的程序员,并且阻止了Java程序员转向那些更函数化的语言(就算不能阻止,起码提供了更好的选择)。 但是,Lambdas 和方法引用远非完美,我们永远要为 Java 设计者早期的草率决定付出代价。特别是没有泛型 Lambda,所以 Lambda 在 Java 中并非一等公民。虽然我不否认 Java 8 的巨大改进,但这意味着和许多 Java 特性一样,它的使用还是会让人感觉沮丧和鸡肋。 From 0cc4fe8b7d0e48055133836e9ef95c0356414b65 Mon Sep 17 00:00:00 2001 From: legendyql Date: Sun, 17 May 2020 12:19:32 +0800 Subject: [PATCH 2/3] Update 13-Functional-Programming.md --- docs/book/13-Functional-Programming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/13-Functional-Programming.md b/docs/book/13-Functional-Programming.md index 67868505..9463b254 100644 --- a/docs/book/13-Functional-Programming.md +++ b/docs/book/13-Functional-Programming.md @@ -514,7 +514,7 @@ public class MultiUnbound { } ``` -为了指明这一点,我将类命名为`This`,将函数式方法的第一个参数命名为`athis`,但你在生产代码中应该使用其他名字,以防止混淆。 +为了指明这一点,我将类命名为**This**,将函数式方法的第一个参数命名为**athis**,但你在生产代码中应该使用其他名字,以防止混淆。 ### 构造函数引用 From 41f02019ff5c99e6e12a9f3fd6b7f5a536d86ebb Mon Sep 17 00:00:00 2001 From: legendyql Date: Sun, 17 May 2020 12:23:40 +0800 Subject: [PATCH 3/3] Update 13-Functional-Programming.md --- docs/book/13-Functional-Programming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/13-Functional-Programming.md b/docs/book/13-Functional-Programming.md index 9463b254..ed33d550 100644 --- a/docs/book/13-Functional-Programming.md +++ b/docs/book/13-Functional-Programming.md @@ -514,7 +514,7 @@ public class MultiUnbound { } ``` -为了指明这一点,我将类命名为**This**,将函数式方法的第一个参数命名为**athis**,但你在生产代码中应该使用其他名字,以防止混淆。 +为了指明这一点,我将类命名为 **This**,将函数式方法的第一个参数命名为 **athis**,但你在生产代码中应该使用其他名字,以防止混淆。 ### 构造函数引用