Skip to content

第13章翻译修改 #458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 19, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 22 additions & 20 deletions docs/book/13-Functional-Programming.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@

> 函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许你以函数式编程。

在计算机时代早期,内存是稀缺和昂贵的。几乎每个人都用汇编语言编程。人们对编译器有所了解,但仅仅想到编译生成的代码肯定会比手工编码多很多字节
在计算机时代早期,内存是稀缺和昂贵的。几乎每个人都用汇编语言编程。人们虽然知道编译器,但编译器生成的代码很低效,比手工编码的汇编程序多很多字节,仅仅想到这一点,人们还是选择汇编语言

通常,只是为了使程序适合有限的内存,程序员通过修改内存中的代码来节省代码空间,以便在程序执行时执行不同的操作。这种技术被称为**自修改代码** (self-modifying code)。只要程序足够小,少数人可以维护所有棘手和神秘的汇编代码,你就可以让它运行起来
通常,为了使程序能在有限的内存上运行,在程序运行时,程序员通过修改内存中的代码,使程序可以执行不同的操作,用这种方式来节省代码空间。这种技术被称为**自修改代码** (self-modifying code)。只要程序小到几个人就能够维护所有棘手和难懂的汇编代码,你就能让程序运行起来

随着内存和处理器变得更便宜、更快。C 语言出现并被大多数汇编程序员认为更“高级”。人们发现使用 C 可以显著提高生产力。同时,使用 C 创建自修改代码仍然不难。

随着硬件越来越便宜,程序的规模和复杂性都在增长。这一切只是让程序工作变得困难。我们想方设法使代码更加一致和易懂。使用纯粹的自修改代码造成的结果就是:我们很难确定程序在做什么。它也难以测试:除非你想一点点测试输出,代码转换和修改等等过程?

然而,使用代码以某种方式操纵其他代码的想法也很有趣,只要能保证它更安全。从代码创建,维护和可靠性的角度来看,这个想法非常吸引人。我们不用从头开始编写大量代码,而是从易于理解、充分测试及可靠的现有小块开始,最后将它们组合在一起以创建新代码。难道这不会让我们更有效率,同时创造更健壮的代码吗?

这就是**函数式编程**(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。至少在某些情况下,这套理论似乎很有用。在这一过程中,一些非函数式语言已经习惯了使用函数式编程产生的优雅的语法
这就是**函数式编程**(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。至少在某些情况下,这套理论似乎很有用。在这一过程中,函数式语言已经产生了优雅的语法,这些语法对于非函数式语言也适用

你也可以这样想:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -512,7 +514,7 @@ public class MultiUnbound {
}
```

为了说明这一点,我将类命名为 **This** ,函数方法的第一个参数则是 **athis**,但是你应该选择其他名称以防止生产代码混淆
为了指明这一点,我将类命名为 **This**,将函数式方法的第一个参数命名为 **athis**,但你在生产代码中应该使用其他名字,以防止混淆

### 构造函数引用

Expand Down Expand Up @@ -558,7 +560,7 @@ public class CtorReference {

**注意**我们如何对 **[1]**,**[2]** 和 **[3]** 中的每一个使用 `Dog :: new`。 这 3 个构造函数只有一个相同名称:`:: new`,但在每种情况下都赋值给不同的接口。编译器可以检测并知道从哪个构造函数引用。

编译器能识别并调用你的构造函数( 在本例中为 `make()`)。
编译器知道调用函数式方法(本例中为 `make()`)就相当于调用构造函数

<!-- Functional Interfaces -->
## 函数式接口
Expand Down Expand Up @@ -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` 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。这主要是因为基本类型会产生一小部分接口。 如果你了解命名模式,顾名思义就能知道特定接口的作用。

Expand Down Expand Up @@ -790,7 +792,7 @@ someOtherName()

因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法名(在本例中为 `accept()`),而不是你的方法名。

现在我们来看看所有基于类的函数式,应用于方法引用(即那些不涉及基本类型的函数)。下例我们创建了一个最简单的函数式签名。代码示例
现在我们来看看,将方法引用应用于基于类的函数式接口(即那些不包含基本类型的函数式接口)。下面的例子中,我创建了适合函数式方法签名的最简单的方法

```java
// functional/ClassFunctionals.java
Expand Down Expand Up @@ -930,7 +932,7 @@ public interface IntToDoubleFunction {
}
```

我们可以简单地编写 `Function <Integer,Double>` 并达到合适的结果,所以,很明显,使用基本类型的函数式接口的唯一原因就是防止传递参数和返回结果过程中的自动装箱和自动拆箱 进而提升性能。
因为我们可以简单地写 `Function <Integer,Double>` 并产生正常的结果,所以用基本类型的唯一原因是可以避免传递参数和返回结果过程中的自动装箱和自动拆箱,进而提升性能。

似乎是考虑到使用频率,某些函数类型并没有预定义。

Expand Down Expand Up @@ -1050,7 +1052,7 @@ O

考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者叫作在词法上限定范围( 也使用术语*变量捕获* )。Java 8 提供了有限但合理的闭包支持,我们将用一些简单的例子来研究它。

首先,下例函数中,方法返回访问对象字段和方法参数。代码示例
首先,下列方法返回一个函数,该函数访问对象字段和方法参数

```java
// functional/Closure1.java
Expand All @@ -1065,7 +1067,7 @@ public class Closure1 {
}
```

但是,仔细考虑一下,`i` 的这种用法并非是个大难题,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上[^5]。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们共享 `i` 的存储空间:
但是,仔细考虑一下,`i` 的这种用法并非是个大难题,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,被现存函数以这种方式绑定的对象,垃圾收集器肯定会保留[^5]。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们共享 `i` 的存储空间:
```java
// functional/SharedStorage.java

Expand Down Expand Up @@ -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
Expand All @@ -1126,7 +1128,7 @@ public class Closure3 {
}
```

`x` 和 `i` 的操作都犯了同样的错误: Lambda 表达式引用的局部变量必须是 `final` 或者是等同 `final` 效果的。
`x` 和 `i` 的操作都犯了同样的错误: Lambda 表达式引用的局部变量必须是 `final` 或者是等同 `final` 效果的。

如果使用 `final` 修饰 `x`和 `i`,就不能再递增它们的值了。代码示例:

Expand Down Expand Up @@ -1189,7 +1191,7 @@ public class Closure6 {

上例中 `iFinal` 和 `xFinal` 的值在赋值后并没有改变过,因此在这里使用 `final` 是多余的。

如果这里是引用的话,需要把 **int** 型更改为 **Integer** 型。代码示例
如果函数式方法中使用的外部局部变量是引用,而不是基本类型的话,会是什么情况呢?我们可以把`int`类型改为`Integer`类型研究一下

```java
// functional/Closure7.java
Expand All @@ -1206,7 +1208,7 @@ public class Closure7 {
}
```

编译器非常智能,它能识别变量 `i` 的值被更改过了。 对于包装类型的处理可能比较特殊,因此我们尝试下 **List**:
编译器非常聪明地识别到变量 `i` 的值被更改过。 因为包装类型可能被特殊处理过了,所以我们尝试下 **List**:

```java
// functional/Closure8.java
Expand Down Expand Up @@ -1244,7 +1246,7 @@ public class Closure8 {
[1, 96]
```

可以看到,这次一切正常。我们改变了 **List** 的值却没产生编译时错误。通过观察本例的输出结果,我们发现这看起来非常安全。这是因为每次调用 `makeFun()` 时,其实都会创建并返回一个全新的 `ArrayList`。 也就是说,每个闭包都有自己独立的 `ArrayList`, 它们之间互不干扰。
可以看到,这次一切正常。我们改变了 **List** 的内容却没产生编译时错误。通过观察本例的输出结果,我们发现这看起来非常安全。这是因为每次调用 `makeFun()` 时,其实都会创建并返回一个全新而非共享的 `ArrayList`。也就是说,每个闭包都有自己独立的 `ArrayList`,它们之间互不干扰。

请**注意**我已经声明 `ai` 是 `final` 的了。尽管在这个例子中你可以去掉 `final` 并得到相同的结果(试试吧!)。 应用于对象引用的 `final` 关键字仅表示不会重新赋值引用。 它并不代表你不能修改对象本身。

Expand Down Expand Up @@ -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 特性一样,它的使用还是会让人感觉沮丧和鸡肋。

Expand Down