校订 第13章函数式编程-Lambda表达式 ISSUE #24

This commit is contained in:
LingCoder
2019-04-15 06:30:44 +08:00
parent 6a3c808d2f
commit c31e15c0a9
2 changed files with 118 additions and 115 deletions

View File

@@ -36,7 +36,7 @@ FP 思想值得融入非 FP 语言,如 Python。Java 8 也从中吸收并支
## 新旧对比
通常,方法会根据我们传递的数据产生不同的结果。如果我们希望某个方法在下一次调用时表现不同,该怎么办? 假设我们可以将代码传递给方法,我们就可以控制它的行为。此前,我们通过在方法中创建包含所需行为的对象,然后将该对象传递给我们想要控制的方法来完成此操作。下面我们用传统形式和 Java 8 的方法引用和 lambda 表达式分别演示。代码示例:
通常,方法会根据我们传递的数据产生不同的结果。如果我们希望某个方法在下一次调用时表现不同,该怎么办? 假设我们可以将代码传递给方法,我们就可以控制它的行为。此前,我们通过在方法中创建包含所需行为的对象,然后将该对象传递给我们想要控制的方法来完成此操作。下面我们用传统形式和 Java 8 的方法引用和 Lambda 表达式分别演示。代码示例:
```java
// functional/Strategize.java
@@ -102,7 +102,7 @@ Hello
Hello there Hello there
```
**Strategy** 接口提供了单一 `approach()` 方法来承载函数式功能。通过创建不同的 **Strategy** 对象,我们可以创建不同的行为。
**Strategy** 接口提供了单一 `approach()` 方法来承载函数式功能。通过实现不同的 **Strategy** 对象,我们可以创建不同的行为。
传统上,我们通过创建一个实现 **Strategy** 接口的类来实现此行为,比如在 **Soft**
@@ -125,13 +125,13 @@ Hello there Hello there
## Lambda表达式
Lambda表达式是使用最小可能语法编写的函数定义
Lambda 表达式是使用**最小可能**语法编写的函数定义:
1. Lambda 表达式产生函数,而不是类。 在 Java 虚拟机JVM)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 - 但作为程序员,你可以高兴地假装它们“只是函数”。
1. Lambda 表达式产生函数,而不是类。 在 JVMJava Virtual MachineJava 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”。
2. lambda语法尽可能多这正是为了使 lambda 易于编写和使用。
2. Lambda 语法尽可能多,这正是为了使 Lambda 易于编写和使用。
您在 **Strategize.java** 中看到了一个 lambda 表达式,但还有其他语法变体:
我们在 `Strategize.java` 中看到了一个 Lambda 表达式,但还有其他语法变体:
```java
// functional/LambdaExpressions.java
@@ -171,43 +171,46 @@ public class LambdaExpressions {
System.out.println(moreLines.brief());
}
}
/* Output:
```
输出结果:
```
Oh! No Parens!
Hi! More details
Short info
Pi! 3.14159
moreLines()
from moreLines()
*/
```
我们从三个接口开始,每个接口都有一个单独的方法(很快就会理解它的重要性)。 但是,每个方法都有不同数量的参数,以便演示 lambda 表达式语法。
我们从三个接口开始,每个接口都有一个单独的方法(很快就会理解它的重要性)。但是,每个方法都有不同数量的参数,以便演示 Lambda 表达式语法。
任何lambda表达式的基本语法是
任何 Lambda 表达式的基本语法是:
1.参数。
1. 参数。
2.接着是 **->**,您可以选择将其视为 “生成”。
2. 接着 `->`,可视为“产出”。
3.**->** 之后的所有东西都是方法体。
3. `->` 之后的内容都是方法体。
**[1] **当只用一个参数,可以不需要括号。 然而,这是一个特例。
- **[1]** 当只用一个参数,可以不需要括号 `()`。 然而,这是一个特例。
**[2]** 正常情况是在参数周围使用括号。 为了保持一致性,您还可以在单个参数周围使用括号,虽然这种情况并不常见。
- **[2]** 正常情况使用括号 `()` 包裹参数。 为了保持一致性,也可以使用括号 `()` 包裹单个参数,虽然这种情况并不常见。
**[3]** 如果没有参数,则必须使用括号表示空参数列表。
- **[3]** 如果没有参数,则必须使用括号 `()` 表示空参数列表。
**[4]** 对于多个参数,将它们放在带括号的参数列表中。
- **[4]** 对于多个参数,将参数列表放在括号 `()` 中。
到目前为止,所有 lambda 表达式方法体都是单行。 该表达式的结果自动成为 lambda 表达式的返回值,在此处使用 **return** 关键字是非法的。 这是 lambda 表达式缩写用于描述功能的语法的另一种方式。
到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 **return** 关键字是非法的。 这是 Lambda 表达式缩写用于描述功能的语法的另一种方式。
**[5]** 如果在 lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 **return**
**[5]** 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 **return**
Lambda 表达式通常比**匿名内部类**产生更易读的代码,因此我们将在本书中尽可能使用它们。
### 递归
递归函数是一个自我调用的函数。 可以编写递归的 lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。 我们将为每个案例创建一个示例。
递归函数是一个自我调用的函数。 可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。 我们将为每个案例创建一个示例。
这两个示例都需要一个接受 **int** 并生成 **int** 的接口:
@@ -249,7 +252,7 @@ public class RecursiveFactorial {
这里,**fact** 是一个静态变量。 注意使用三元 **if-else**。 递归函数将一直调用自己,直到 **i == 0**.所有递归函数都有某种 “停止条件”,否则它们将无限递归并产生异常。
我们可以将 **Fibonacci 序列** 实现为递归 lambda 表达式,这次使用实例变量:
我们可以将 **Fibonacci 序列** 实现为递归 Lambda 表达式,这次使用实例变量:
```java
// functional/RecursiveFibonacci.java
@@ -345,31 +348,31 @@ Help!
*/
```
**[1]** 我们从单一方法接口开始(同样,很快就会了解到这一点的重要性)。
**[1]** 我们从单一方法接口开始(同样,很快就会了解到这一点的重要性)。
**[2]** **show**的签名(参数类型和返回类型)符合 **Callable****call**的签名。
**[2]** `show()` 的签名(参数类型和返回类型)符合 **Callable**`call()` 的签名。
**[3]** **hello**也符合 **call**的签名。
**[3]** `hello()` 也符合 `call()` 的签名。
[4] ...就像是**help**,一个静态内部类中的非静态方法。(原文:… as is help(), a non-static method within a static inner class.)
[4] ...就像是 `help()`,一个静态内部类中的非静态方法。(原文:… as is help(), a non-static method within a static inner class.)
**[5]** **assist**是静态内部类中的静态方法。
**[5]** `assist()` 是静态内部类中的静态方法。
**[6]** 我们将 **Describe** 对象的方法引用分配给 **Callable** , 它没有**show**方法,而是 **call**方法。 但是Java似乎接受用这个看似奇怪的赋值因为方法引用符合 **Callable****call**方法的签名。
**[6]** 我们将 **Describe** 对象的方法引用分配给 **Callable** , 它没有 `show()` 方法,而是 `call()` 方法。 但是Java似乎接受用这个看似奇怪的赋值因为方法引用符合 **Callable**`call()` 方法的签名。
**[7]** 我们现在可以通过调用 **call**来调用 **show**,因为 Java 将 **call**映射到 **show**
**[7]** 我们现在可以通过调用 `call()` 来调用 `show()`,因为 Java 将 `call()` 映射到 `show()`
**[8]** 这是一个静态方法引用。
**[9] **这是 **[6]** 的另一个版本:附加到存活对象的方法的方法参考,有时称为*绑定方法引用*。
**[10]** 最后,获取静态内部类的静态方法的方法引用,用起来就像 **[8] **中的外部类。
**[10]** 最后,获取静态内部类的静态方法的方法引用,用起来就像 **[8]**中的外部类。
这不是一个详尽的例子; 我们很快就会看到方法参考的所有变化。
### Runnable
**Runnable** 接口自 1.0 版以来一直在 Java 中,因此不需要导入。 它也符合特殊的单方法接口格式它的方法run不带参数,也没有返回值。 因此,我们可以使用 lambda 表达式和方法引用作为 **Runnable**
**Runnable** 接口自 1.0 版以来一直在 Java 中,因此不需要导入。 它也符合特殊的单方法接口格式它的方法run()不带参数,也没有返回值。 因此,我们可以使用 Lambda 表达式和方法引用作为 **Runnable**
```java
// functional/RunnableMethodReference.java
@@ -405,13 +408,13 @@ Go::go()
*/
```
**Thread** 对象将 **Runnable** 作为其构造函数参数,并具有会调用 **run**的方法 **start**。 请注意,只有**匿名内部类**才需要具有名为**run**的方法。
**Thread** 对象将 **Runnable** 作为其构造函数参数,并具有会调用 `run()` 的方法 `start()`。 请注意,只有**匿名内部类**才需要具有名为 `run()` 的方法。
<!-- Unbound Method References -->
### 未绑定的方法引用
未绑定的方法引用是指没有关联对象的普通(非静态)方法。 要使用未绑定的引用,必须提供以下对象:
未绑定的方法引用是指没有关联对象的普通(非静态)方法。 要使用未绑定的引用,必须提供以下对象:
```java
// functional/UnboundMethodReference.java
@@ -445,11 +448,11 @@ X::f()
*/
```
到目前为止,我们已经看到了对与其关联接口具有相同签名的方法的引用。 在 **[1]**我们尝试对X中的 **f**做同样的事情,试图分配给 **MakeString**。 这会产生编译器关于“无效方法引用”的错误 (“invalid method reference),即使 **make**与**f**具有相同的签名。 问题是实际上还有另一个(隐藏的)参数:我们的老朋友 **this**。 你不能在没有 **X** 对象的情况下调用 **f**来调用它。 因此,**X :: f** 表示未绑定的方法引用,因为它尚未“绑定”到对象。
到目前为止,我们已经看到了对与其关联接口具有相同签名的方法的引用。 在 **[1]**我们尝试对X中的 `f()` 做同样的事情,试图分配给 **MakeString**。 这会产生编译器关于“无效方法引用”的错误 (“invalid method reference),即使 `make()``f()` 具有相同的签名。 问题是实际上还有另一个(隐藏的)参数:我们的老朋友 `this`。 你不能在没有 `X` 对象的情况下调用 `f()` 来调用它。 因此,`X :: f` 表示未绑定的方法引用,因为它尚未“绑定”到对象。
要解决这个问题,我们需要一个 **X** 对象,所以我们的接口实际上需要一个额外的参数,就像你在 **TransformX** 中看到的那样。 如果将 **X :: f** 分配给 **TransformX**Java 很高兴。 我们现在必须进行第二次心理调整 - 使用未绑定的引用时,函数方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 有一个很好的理由说服你,那就是你需要一个对象来调用方法。
要解决这个问题,我们需要一个 `X` 对象,所以我们的接口实际上需要一个额外的参数,就像你在 **TransformX** 中看到的那样。 如果将 `X :: f` 分配给 **TransformX**Java 很高兴。 我们现在必须进行第二次心理调整 - 使用未绑定的引用时,函数方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 有一个很好的理由说服你,那就是你需要一个对象来调用方法。
**[2]** 的结果有点像脑筋急转弯。 我接受未绑定的引用并对其调用 **transform**将其传递给X并以某种方式导致对 **x.f**的调用。 Java知道它必须采用第一个参数实际上是这个并在其上调用方法。
**[2]** 的结果有点像脑筋急转弯。 我接受未绑定的引用并对其调用 `transform()`将其传递给X并以某种方式导致对 `x.f()`的调用。 Java知道它必须采用第一个参数实际上是这个并在其上调用方法。
```java
// functional/MultiUnbound.java
@@ -488,11 +491,11 @@ public class MultiUnbound {
}
```
为了说明这一点,我将类命名为 **This** ,函数方法的第一个参数则是 **athis**,但是应该选择其他名称以防止生产代码混淆。
为了说明这一点,我将类命名为 **This** ,函数方法的第一个参数则是 **athis**,但是应该选择其他名称以防止生产代码混淆。
### 构造函数引用
还可以捕获构造函数的引用,然后通过引用调用该构造函数。
还可以捕获构造函数的引用,然后通过引用调用该构造函数。
```java
// functional/CtorReference.java
@@ -530,17 +533,17 @@ public class CtorReference {
}
```
**Dog** 有三个构造函数,函数接口内的 **make**方法反映了构造函数参数列表( make方法可以有不同的名称)。
**Dog** 有三个构造函数,函数接口内的 `make()` 方法反映了构造函数参数列表( make()方法可以有不同的名称)。
注意我们如何对 **[1]****[2]** 和 **[3]** 中的每一个使用 **Dog :: new**。 所有三个构造函数只有一个名称:**:: new**。 但是构造函数引用在每种情况下都分配给不同的接口,并且编译器可以知道从哪个构造函数引用中进行检测。
注意我们如何对 **[1]****[2]** 和 **[3]** 中的每一个使用 `Dog :: new`。 所有三个构造函数只有一个名称:`:: new`。 但是构造函数引用在每种情况下都分配给不同的接口,并且编译器可以知道从哪个构造函数引用中进行检测。
编译器可以看到调用函数方法( 在本例中为make)意味着调用构造函数。
编译器可以看到调用函数方法( 在本例中为make())意味着调用构造函数。
<!-- Functional Interfaces -->
## 函数式接口
方法引用和lambda表达式都是必须赋值的并且这些赋值需要编译器的类型信息以确保类型正确性。 Lambda表达式特别引入了新的要求。 考虑:
方法引用和 Lambda 表达式都是必须赋值的,并且这些赋值需要编译器的类型信息以确保类型正确性。 Lambda 表达式特别引入了新的要求。 考虑:
```java
x -> x.toString()
@@ -548,7 +551,7 @@ x -> x.toString()
我们看到返回类型必须是String但x是什么类型
因为 lambda 表达式包含一种类型推断形式(编译器会对类型进行描述,而不是要求程序员显式),编译器必须能够以某种方式推导出 x 的类型。
因为 Lambda 表达式包含一种类型推断形式(编译器会对类型进行描述,而不是要求程序员显式),编译器必须能够以某种方式推导出 x 的类型。
这是第二个例子:
@@ -556,13 +559,13 @@ x -> x.toString()
(x, y) -> x + y
```
现在 **x****y** 可以是支持 **+** 运算符的任何类型,包括两个不同的数字类型或一个 **String** 以及一些将自动转换为**String** 的类型(这包括大多数类型)。 但是,当分配此 lambda 表达式时,编译器必须确定 **x****y** 的确切类型以生成正确的代码。
现在 `x``y` 可以是支持 `+` 运算符的任何类型,包括两个不同的数字类型或一个 **String** 以及一些将自动转换为 **String** 的类型(这包括大多数类型)。 但是,当分配此 Lambda 表达式时,编译器必须确定 `x``y` 的确切类型以生成正确的代码。
同样的问题适用于方法引用。 假设你要传递 System.out :: println 到你正在编写的方法 ,你为方法的参数给出了什么类型?
为了解决这个问题Java 8 引入了 **java.util.function**,它包含一组接口,这些接口是 lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。
为了解决这个问题Java 8 引入了 `java.util.function`,它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。
在编写接口时,可以使用 **@FunctionalInterface** 注释强制执行此“函数方法”模式:
在编写接口时,可以使用 `@FunctionalInterface` 注释强制执行此“函数方法”模式:
```java
// functional/FunctionalAnnotation.java
@@ -604,27 +607,27 @@ public class FunctionalAnnotation {
}
```
**@FunctionalInterface **注释是可选的; Java 将 **Functional****FunctionalNoAnn** 视为 **main**中的函数接口。 **@FunctionalInterface** 的值在 **NotFunctional** 的定义中可见。接口中的如果有多个方法则会产生编译时错误消息。
`@FunctionalInterface` 注释是可选的; Java 将 `Functional``FunctionalNoAnn` 视为 `main()` 中的函数接口。 `@FunctionalInterface` 的值在 `NotFunctional` 的定义中可见。接口中的如果有多个方法则会产生编译时错误消息。
仔细观察 **f****fna** 定义中会发生什么。 **Functional****FunctionalNoAnn** 定义接口。 然而,分配的只是方法 **goodbye**()。首先,这只是一种方法而不是一种类。 其次,它甚至不是一个实现了该接口的类里的方法。这是添加到 Java 8 中的一些魔力:如果将方法引用或 lambda 表达式分配给函数接口以及类型适合Java将使的分配适应目标接口。 在背后,编译器将方法引用或 lambda 表达式包装在实现目标接口的类的实例中。
仔细观察 `f``fna` 定义中会发生什么。 `Functional``FunctionalNoAnn` 定义接口。 然而,分配的只是方法 `goodbye()`。首先,这只是一种方法而不是一种类。 其次,它甚至不是一个实现了该接口的类里的方法。这是添加到 Java 8 中的一些魔力:如果将方法引用或 Lambda 表达式分配给函数接口以及类型适合Java将使的分配适应目标接口。 在背后,编译器将方法引用或 Lambda 表达式包装在实现目标接口的类的实例中。
尽管 **FunctionalAnnotation** 确实适合 **Functional** 模型,但如果我们尝试将 **FunctionalAnnotation** 直接分配给 **Functional**,就像 **fac** 的定义一样Java 将不会让我们成功,因为它没有明确地实现 **Functional** 接口。 但令人惊讶的是 Java8 允许我们为接口分配函数,从而产生更好,更简单的语法。
尽管 `FunctionalAnnotation` 确实适合 `Functional` 模型,但如果我们尝试将 `FunctionalAnnotation` 直接分配给 `Functional`,就像 `fac` 的定义一样Java 将不会让我们成功,因为它没有明确地实现 `Functional` 接口。 但令人惊讶的是 Java8 允许我们为接口分配函数,从而产生更好,更简单的语法。
**java.util.function** 的目标是创建一组完整的目标接口,这样通常不需要定义自己的接口。 主要是因为原始类型,这会产生一小部分接口。 如果了解命名模式,通常可以通过查看名称来检测特定接口的作用。
`java.util.function` 的目标是创建一组完整的目标接口,这样通常不需要定义自己的接口。 主要是因为原始类型,这会产生一小部分接口。 如果了解命名模式,通常可以通过查看名称来检测特定接口的作用。
以下是基本命名准则:
1. 如果它只处理对象而不是原语,那么它只是一个简单的名称,如 **Function****Consumer****Predicate** 等。参数类型通过泛型添加。
1. 如果它只处理对象而不是原语,那么它只是一个简单的名称,如 `Function``Consumer``Predicate` 等。参数类型通过泛型添加。
2. 如果它采用原始参数,则由名称的第一部分表示,如 **LongConsumer****DoubleFunction****IntPredicate** 等。例外情况是原始的供应商类型。
2. 如果它采用原始参数,则由名称的第一部分表示,如 `LongConsumer``DoubleFunction``IntPredicate` 等。例外情况是原始的供应商类型。
3. 如果它返回原始结果,则用 **To** 表示,如 **ToLongFunction <T>****IntToLongFunction**
3. 如果它返回原始结果,则用 `To` 表示,如 `ToLongFunction <T>``IntToLongFunction`
4. 如果它返回与其参数相同的类型,则它是一个运算符,其中一个参数使用 **UnaryOperator**,两个参数使用 **BinaryOperator**
4. 如果它返回与其参数相同的类型,则它是一个运算符,其中一个参数使用 `UnaryOperator`,两个参数使用 `BinaryOperator`
5. 如果它需要两个参数并返回一个布尔值,那么它就是一个谓词( Predicate )。
6. 如果它需要两个不同类型的参数,则名称中有一个 **Bi**
6. 如果它需要两个不同类型的参数,则名称中有一个 `Bi`
该表描述了java.util.function中的目标类型
@@ -636,7 +639,7 @@ public class FunctionalAnnotation {
(java.lang) **Runnable** Returns nothing **run() Supplier<T> Supplier BooleanSupplier** No arguments;
**get() IntSupplier** Returns any type getAstype() LongSupplier DoubleSupplier Callable No arguments; (java.util.concurrent) Callable<V> Returns any type call() Consumer<T> One argument;
get() IntSupplier Returns any type getAstype() LongSupplier DoubleSupplier Callable No arguments; (java.util.concurrent) Callable<V> Returns any type call() Consumer<T> One argument;
Consumer IntConsumer Returns nothing accept() LongConsumer DoubleConsumer
@@ -644,13 +647,13 @@ Two-argument BiConsumer BiConsumer<T,U> Consumer accept() Two-argument Consumer;
可能会想到用于进一步添加更多行,但此表提供了基本概念,并且应该帮到或多或少地推断出需要的函数接口。
可能会想到用于进一步添加更多行,但此表提供了基本概念,并且应该帮到或多或少地推断出需要的函数接口。
可以看到在创建 **java.util.function** 时做出了一些选择。 例如,为什么没有 **IntComparator****LongComparator****DoubleComparator** 有一个 **BooleanSupplier**,但没有其他接口表示 **Boolean**。 有一个通用的 **BiConsumer**,但没有用于所有 **int****long** 和 **double****BiConsumers** 变体( 我可以支持他们为什么放弃那个)。 这是疏忽还是有人决定(他们是如何得出这个结论的)?
可以看到在创建 `java.util.function` 时做出了一些选择。 例如,为什么没有 `IntComparator``LongComparator``DoubleComparator` 有一个 `BooleanSupplier`,但没有其他接口表示 **Boolean**。 有一个通用的 `BiConsumer`,但没有用于所有 **int****long** 和 **double**`BiConsumers` 变体( 我可以支持他们为什么放弃那个)。 这是疏忽还是有人决定(他们是如何得出这个结论的)?
还可以看到原始类型为 Java 添加了多少复杂性。 由于效率问题,它们被包含在该语言的第一版中 - 这很快就得到了缓解。 现在,在语言的生命周期中,我们仍然受到语言设计选择不佳的影响。
还可以看到原始类型为 Java 添加了多少复杂性。 由于效率问题,它们被包含在该语言的第一版中 - 这很快就得到了缓解。 现在,在语言的生命周期中,我们仍然受到语言设计选择不佳的影响。
下面枚举基于lambda表达式的所有不同 Function 变体的示例:
下面枚举基于 Lambda 表达式的所有不同 Function 变体的示例:
```java
// functional/FunctionVariants.java
@@ -718,9 +721,9 @@ public class FunctionVariants {
}
```
这些 **lambda** 表达式尝试生成适合对应函数签名的最简代码。 在某些情况下,强制转换是必要的,否则编译器会抱怨截断错误。
这些 Lambda 表达式尝试生成适合对应函数签名的最简代码。 在某些情况下,强制转换是必要的,否则编译器会抱怨截断错误。
**main**中的每个测试都显示了 **Function** 接口中不同类型的 **apply** 方法。 每个都产生一个对其相关的**lambda** 表达式的调用。
`main()` 中的每个测试都显示了 `Function` 接口中不同类型的 `apply` 方法。 每个都产生一个对其相关的 Lambda 表达式的调用。
方法引用有自己的魔力:
@@ -756,9 +759,9 @@ someOtherName()
*/
```
查看 **BiConsumer** 的文档。 你会看到它的函数方法是 **accept**。 实际上,如果我们将方法命名为**accept**,它就可以作为方法引用。 但是我们可以给它一个完全不同的名称,比如**someOtherName**,它也可以运行,只要参数类型和返回类型与 **BiConsumer****accept**相同。
查看 `BiConsumer` 的文档。 你会看到它的函数方法是 `accept()`。 实际上,如果我们将方法命名为 `accept()`,它就可以作为方法引用。 但是我们可以给它一个完全不同的名称,比如 `someOtherName()`,它也可以运行,只要参数类型和返回类型与 `BiConsumer``accept()` 相同。
因此,在使用函数接口时,名称无关紧要 - 只有参数类型和返回类型相同。 Java 将的名称映射到接口的函数方法。 要调用方法,可以调用函数方法名称(在本例中为 **accept**),而不是你的方法名称。
因此,在使用函数接口时,名称无关紧要 - 只有参数类型和返回类型相同。 Java 将的名称映射到接口的函数方法。 要调用方法,可以调用函数方法名称(在本例中为 `accept()`),而不是你的方法名称。
现在我们将查看应用于方法引用的所有基于类的 Functionals即那些不涉及原始类型的函数。 我再次创建了适合函数签名的最简单方法:
@@ -808,13 +811,13 @@ public class ClassFunctionals {
}
```
请注意,每个方法名称都是任意的( **f1f2**等),但正如刚才看到的,一旦将方法引用分配给函数接口,就可以调用与该接口关联的函数方法。 在此示例中,这些是 **getcompareacceptapply**和 **test**
请注意,每个方法名称都是任意的( `f1()f2()`等),但正如刚才看到的,一旦将方法引用分配给函数接口,就可以调用与该接口关联的函数方法。 在此示例中,这些是 `get()compare()accept()apply()``test()`
<!-- Functional Interfaces with More Arguments -->
### 有着更多参数的函数接口
**java.util.functional** 中的接口是有限的。 比如有了 **BiFunction**,但它不能变化。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看 Java 库源代码并自行创建:
`java.util.functional` 中的接口是有限的。 比如有了 `BiFunction`,但它不能变化。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看 Java 库源代码并自行创建:
```java
// functional/TriFunction.java
@@ -840,11 +843,11 @@ public class TriFunctionTest {
}
```
这里我们测试方法引用和 lambda 表达式。
这里我们测试方法引用和 Lambda 表达式。
### 缺少原始类型的函数
让我们重温一下 **BiConsumer**,看看我们如何创建缺少 **intlong** 和 **double **的各种排列:
让我们重温一下 `BiConsumer`,看看我们如何创建缺少 **int****long** 和 **double** 的各种排列:
```java
// functional/BiConsumerPermutations.java
@@ -871,7 +874,7 @@ public class BiConsumerPermutations {
*/
```
为了显示,我使用 **System.out.format**,它类似于 **System.out.println**,除了它提供了更多的显示选项。 这里,**f** 表示我将 **n** 作为浮点值给出,**d** 表示 **n** 是一个整数值。 我能够包含空格,并且它不会添加换行符,除非你输入n - 它也会接受传统 **\ n** 换行符,但 **n** 是自动跨平台的,这是使用的 **format()** 的另一个原因。
为了显示,我使用 `System.out.format()`,它类似于 `System.out.println()`,除了它提供了更多的显示选项。 这里,`%f` 表示我将 `n` 作为浮点值给出,`%d` 表示 `n` 是一个整数值。 我能够包含空格,并且它不会添加换行符,除非你输入 `%n` - 它也会接受传统 `\n` 换行符,但 `%n` 是自动跨平台的,这是使用的 `format()` 的另一个原因。
该示例仅使用适当的包装器类型,装箱和拆箱负责在原始类型之间来回转换。 我们也可以使用包装类型,例如 Function而不是预定义的原始类型
@@ -897,11 +900,11 @@ public interface IntToDoubleFunction {
}
```
因为我们可以简单地编写 **Function <IntegerDouble>** 并产生工作结果,所以很明显,函数的原始类型的唯一原因是为了防止传递参数和返回结果所涉及的自动装箱和自动装箱。 也就是说,为了性能。
因为我们可以简单地编写 `Function <IntegerDouble>` 并产生工作结果,所以很明显,函数的原始类型的唯一原因是为了防止传递参数和返回结果所涉及的自动装箱和自动装箱。 也就是说,为了性能。
似乎可以安全地推测,某些函数类型具有定义而其他类型没有定义是因为考虑到了使用频率。
当然,如果由于缺少原始类型的函数而导致性能实际上成为问题,可以轻松编写自己的接口( 使用Java库源进行参考 - 尽管这似乎不太可能是的性能瓶颈。
当然,如果由于缺少原始类型的函数而导致性能实际上成为问题,可以轻松编写自己的接口( 使用Java库源进行参考 - 尽管这似乎不太可能是的性能瓶颈。
<!-- Higher-Order Functions-->
## 高阶函数
@@ -933,13 +936,13 @@ yelling
*/
```
这里,**produce**是高阶函数。
这里,`produce()` 是高阶函数。
**[1]** 使用继承,可以轻松地为你的专用接口创建别名。
**[1]** 使用继承,可以轻松地为你的专用接口创建别名。
**[2]** 使用 **lambda** 表达式,在方法中创建和返回一个函数几乎毫不费力。
**[2]** 使用 Lambda 表达式,在方法中创建和返回一个函数几乎毫不费力。
**consume** 函数consume a function其对应的 **consuming** 方法 the consuming method的参数列表必须正确描述函数类型
`consume` 函数consume a function其对应的 `consuming` 方法 the consuming method的参数列表必须正确描述函数类型
```java
// functional/ConsumeFunction.java
@@ -959,7 +962,7 @@ public class ConsumeFunction {
}
```
根据 **consume** 的函数生成新函数时,事情变得特别有趣,比如:
根据 `consume` 的函数生成新函数时,事情变得特别有趣,比如:
```java
// functional/TransformFunction.java
@@ -997,17 +1000,17 @@ O
*/
```
在这里,**transform**生成一个与传入的函数具有相同签名的函数,但是可以生成任何想要的类型。这在 **Function** 接口中使用名为 **andThen**的默认方法,该方法专门用于操作函数。 顾名思义在调用in函数之后调用 **toThen**(还有 **compose**,它在 **in** 函数之前应用新函数)。 要附加一个**andThen**函数,我们只需将该函数作为参数传递。 **transform**产生的是一个新函数,它将 **in** 的动作与 **andThen**参数的动作结合起来。
在这里,`transform()` 生成一个与传入的函数具有相同签名的函数,但是可以生成任何想要的类型。这在 `Function` 接口中使用名为 `andThen()` 的默认方法,该方法专门用于操作函数。 顾名思义在调用in函数之后调用 `toThen()`(还有 `compose()`,它在 `in` 函数之前应用新函数)。 要附加一个 `andThen()` 函数,我们只需将该函数作为参数传递。 `transform()` 产生的是一个新函数,它将 `in` 的动作与 `andThen()` 参数的动作结合起来。
<!-- Closures -->
## 闭包
在上一节的 **ProduceFunction.java** 中,我们从方法返回了一个 lambda 函数。 这个例子让事情变得简单,但是我们必须在返回 lambdas 时探讨一些问题。
在上一节的 `ProduceFunction.java` 中,我们从方法返回了一个 Lambda 函数。 这个例子让事情变得简单,但是我们必须在返回 Lambdas 时探讨一些问题。
*闭包*一词概括了这些问题。 闭包非常重要,因为它们可以轻松生成函数。
考虑一个更复杂的 lambda它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者在词法上限定范围( 也使用术语变量捕获 )。Java 8提供了有限但合理的闭包支持
考虑一个更复杂的 Lambda它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者在词法上限定范围( 也使用术语变量捕获 )。Java 8提供了有限但合理的闭包支持
我们将用一些简单的例子来研究它。
@@ -1026,7 +1029,7 @@ public class Closure1 {
}
```
但是,仔细考虑一下,**i** 的这种用法并不是一个很大的挑战,因为对象很可能在调用 **makeFun()** 之后就存在了——实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上。当然,如果你对同一个对象多次调用 **makeFun()** ,你最终会得到多个函数,它们都为 **i** 共享相同的存储空间:
但是,仔细考虑一下,`i` 的这种用法并不是一个很大的挑战,因为对象很可能在调用 `makeFun()` 之后就存在了——实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们都为 `i` 共享相同的存储空间:
```java
// functional/SharedStorage.java
@@ -1051,9 +1054,9 @@ public class SharedStorage {
*/
```
每次调用 **getAsInt**都会增加 **i **,表明存储是共享的。
每次调用 `getAsInt()` 都会增加 `i`,表明存储是共享的。
如果 **i****makeFun**的本地怎么办? 在正常情况下,当 **makeFun**完成时 **i** 就消失。 但它仍然编译:
如果 `i``makeFun()` 的本地怎么办? 在正常情况下,当 `makeFun()` 完成时 `i` 就消失。 但它仍然编译:
```java
// functional/Closure2.java
@@ -1068,7 +1071,7 @@ public class Closure2 {
}
```
**makeFun**返回的 **IntSupplier** “关闭” **i****x**,因此当调用返回的函数时两者仍然有效。 但请注意,我没有像 **Closure1.java** 那样增加i。 尝试递增它会产生编译时错误:
`makeFun()` 返回的 `IntSupplier` “关闭” `i``x`,因此当调用返回的函数时两者仍然有效。 但请注意,我没有像 `Closure1.java` 那样增加i。 尝试递增它会产生编译时错误:
```java
// functional/Closure3.java
@@ -1085,9 +1088,9 @@ public class Closure3 {
}
```
**x****i** 的操作都犯了同样的错误:
`x``i` 的操作都犯了同样的错误:
显然,从 lambda 表达式引用的局部变量必须是 **final** 或者 实际的 **final** effectively final如果我们声明 **x**和 **i****final** ,它将起作用,因为那时我们不能增加任何一个:
显然,从 Lambda 表达式引用的局部变量必须是 `final` 或者 实际的 `final` effectively final如果我们声明 `x``i``final` ,它将起作用,因为那时我们不能增加任何一个:
```java
// functional/Closure4.java
@@ -1102,11 +1105,11 @@ public class Closure4 {
}
```
但是为什么 **Closure2.java****x****i **不是 **final** 却可以运行?
但是为什么 `Closure2.java``x``i `不是 `final` 却可以运行?
这就是 “实际” **final **effectively final的含义出现的地方。 这个术语是为 Java 8 创建的,表示你没有明确地声明变量是 **final** 的,但你仍然是这样对待它 - 你没有改变它。 如果局部变量的初始值永远不会改变,那么它实际上是最终的。
这就是 “实际” `final `effectively final的含义出现的地方。 这个术语是为 Java 8 创建的,表示你没有明确地声明变量是 `final` 的,但你仍然是这样对待它 - 你没有改变它。 如果局部变量的初始值永远不会改变,那么它实际上是最终的。
如果 **x****i** 在方法中的其他位置更改(但不在返回函数内部),则编译器仍将其视为错误。 每个增量产生一个单独的错误消息:
如果 `x``i` 在方法中的其他位置更改(但不在返回函数内部),则编译器仍将其视为错误。 每个增量产生一个单独的错误消息:
```java
/ functional/Closure5.java
@@ -1124,9 +1127,9 @@ public class Closure5 {
}
```
要成为 “effectively final” ,意味着可以将 final 关键字应用于变量声明而不更改任何其余代码。 它实际上是 **final**的,你只是没有明说。
要成为 “effectively final” ,意味着可以将 final 关键字应用于变量声明而不更改任何其余代码。 它实际上是 `final`的,你只是没有明说。
我们实际上可以通过在闭包中使用它们之前将 **x****i** 分配给 **final** 变量来解决 **Closure5.java** 中的问题:
我们实际上可以通过在闭包中使用它们之前将 `x``i` 分配给 `final` 变量来解决 `Closure5.java` 中的问题:
```java
@@ -1146,9 +1149,9 @@ public class Closure6 {
}
```
由于我们在分配后永远不会更改 **iFinal****xFinal** ,因此在这里使用 **final** 是多余的。
由于我们在分配后永远不会更改 `iFinal``xFinal` ,因此在这里使用 `final` 是多余的。
如果使用引用怎么办? 我们可以从 **int** 更改为 **Integer**
如果使用引用怎么办? 我们可以从 **int** 更改为 **Integer**
```java
// functional/Closure7.java
@@ -1165,7 +1168,7 @@ public class Closure7 {
}
```
编译器仍然足够聪明,可以看到 **i** 正在被更改。 包装器类型可能正在进行特殊处理所以让我们尝试一下List
编译器仍然足够聪明,可以看到 `i` 正在被更改。 包装器类型可能正在进行特殊处理所以让我们尝试一下List
```java
// functional/Closure8.java
@@ -1200,11 +1203,11 @@ public class Closure8 {
*/
```
这次它可以运行:我们修改 **List** 的内容而没产生编译时错误。 当查看此示例的输出时,它看起来确实非常安全,因为每次调用 **makeFun**时,都会创建并返回一个全新的 **ArrayList** - 这意味着它不会被共享,因此每个生成的闭包都有自己独立的 **ArrayList** 他们不能互相干扰。
这次它可以运行:我们修改 `List` 的内容而没产生编译时错误。 当查看此示例的输出时,它看起来确实非常安全,因为每次调用 `makeFun()`时,都会创建并返回一个全新的 `ArrayList` - 这意味着它不会被共享,因此每个生成的闭包都有自己独立的 `ArrayList` 他们不能互相干扰。
并且请注意我已经声明 **ai****final** 的,尽管在这个例子中你可以去掉 **final** 并得到相同的结果(试试吧!)。 应用于对象引用的 **final** 关键字仅表示不会重新分配引用。 它并没有说你无法修改对象本身。
并且请注意我已经声明 `ai``final` 的,尽管在这个例子中你可以去掉 `final` 并得到相同的结果(试试吧!)。 应用于对象引用的 `final` 关键字仅表示不会重新分配引用。 它并没有说你无法修改对象本身。
看看 **Closure7.java****Closure8.java** 之间的区别,我们看到 **Closure7.java** 实际上有一个 **i** 的重新分配。 也许这是 “effectively final” 错误消息的触发点:
看看 `Closure7.java``Closure8.java` 之间的区别,我们看到 `Closure7.java` 实际上有一个 `i` 的重新分配。 也许这是 “effectively final” 错误消息的触发点:
```java
// functional/Closure9.java
@@ -1222,9 +1225,9 @@ public class Closure9 {
}
```
引用的重新分配确实会触发错误消息。 如果只修改指向的对象Java 会接受它。 只要没有其他人获得对该对象的引用(这意味着有多个可以修改对象的实体,此时事情会变得非常混乱),这可能是安全的。[^6]
引用的重新分配确实会触发错误消息。 如果只修改指向的对象Java 会接受它。 只要没有其他人获得对该对象的引用(这意味着有多个可以修改对象的实体,此时事情会变得非常混乱),这可能是安全的。[^6]
然而,如果我们现在回顾一下 **Closure1.java.** ,那就有一个难题:**i** 被修改却没有编译器投诉。 它既不是 **final**也不是“effectively final"的。因为 **i** 是外围类的成员,所以这样做肯定是安全的( 除非你正在创建共享可变内存的多个函数)。实际上,可以争辩说在这种情况下不会发生变量捕获variable capture。 可以肯定的是,**Closure3.java** 的错误消息专门针对局部变量。 因此,规则并不像说“在lambda之外定义的任何变量必须是 **final** 的或 **effectively final** 那么简单。相反,你必须考虑捕获的变量是否实际 **final**。 如果它是对象中的字段,那么它有一个独立的生存期,并且不需要任何特殊的捕获,以便稍后在调用 **lambda** 时存在。
然而,如果我们现在回顾一下 `Closure1.java.` ,那就有一个难题:`i` 被修改却没有编译器投诉。 它既不是 `final`也不是“effectively final"的。因为 `i` 是外围类的成员,所以这样做肯定是安全的( 除非你正在创建共享可变内存的多个函数)。实际上,可以争辩说在这种情况下不会发生变量捕获variable capture。 可以肯定的是,`Closure3.java` 的错误消息专门针对局部变量。 因此,规则并不像说“在Lambda之外定义的任何变量必须是 `final` 的或 `effectively final` 那么简单。相反,你必须考虑捕获的变量是否实际 `final`。 如果它是对象中的字段,那么它有一个独立的生存期,并且不需要任何特殊的捕获,以便稍后在调用 Lambda 时存在。
<!-- Inner Classes as Closures -->
@@ -1250,13 +1253,13 @@ public class AnonymousClosure {
}
```
事实证明只要有内部类就会有闭包Java 8只 会使闭包变得更容易)。 在 Java 8 之前,要求是 **x****i** 被明确声明为 **final**。 使用 Java 8内部类的规则已经放宽包括 “effectively final”。
事实证明只要有内部类就会有闭包Java 8只 会使闭包变得更容易)。 在 Java 8 之前,要求是 `x``i` 被明确声明为 `final`。 使用 Java 8内部类的规则已经放宽包括 “effectively final”。
<!-- Function Composition -->
## 函数组合
函数组合基本上意味着“将函数粘贴在一起以创建新函数”,它通常被认为是函数编程的一部分。您在**TransformFunction.java** 中看到了一个使用 **andThen()** 的函数组合示例。一些 **java.util** 的函数接口包含支持函数组合的方法。
函数组合基本上意味着“将函数粘贴在一起以创建新函数”,它通常被认为是函数编程的一部分。你在`TransformFunction.java` 中看到了一个使用 `andThen()` 的函数组合示例。一些 `java.util` 的函数接口包含支持函数组合的方法。
Compositional Supporting Method Interfaces Function BiFunction Consumer BiConsumer IntConsumer andThen(argument) Performs the original LongConsumer operation followed by DoubleConsumer the argument operation.
@@ -1264,7 +1267,7 @@ UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator BinaryOpera
( 待整理 )
此示例使用 **Function** 里的 **compose**和 **andThen**
此示例使用 `Function` 里的 `compose()``andThen()`
```java
// functional/FunctionComposition.java
@@ -1291,9 +1294,9 @@ _fter _ll _mbul_nces
*/
```
这里要看的重要一点是我们正在创建一个新函数 **f4**,然后可以使用 **apply**(几乎)像任何其他函数一样调用它。[^8]
这里要看的重要一点是我们正在创建一个新函数 `f4`,然后可以使用 `apply()`(几乎)像任何其他函数一样调用它。[^8]
**f1** 获得String时它已经被**f2** 剥离了前三个字符。 这是因为对 **composef2**的调用意味着在 **f1** 之前调用 **f2**
`f1` 获得String时它已经被`f2` 剥离了前三个字符。 这是因为对 `composef2`的调用意味着在 `f1` 之前调用 `f2`
这是Predicate逻辑运算的演示
@@ -1321,9 +1324,9 @@ foobaz
*/
```
**p4** 获取所有谓词并将它们组合成一个更复杂的谓词,其中包含:“如果 **String** 不包含 'bar' 且长度小于5或者它包含 'foo' ,则结果为 **true**。”因为它产生如此清晰的语法,我在**main**中作了一些小伎俩,并借用了下一章的内容。 首先,我创建一个 **String** 对象的 “流”(序列),然后将每个对象提供给 **filter**操作。 **filter**使用我们的 **p4** 谓词来决定要保留流中的哪个对象以及要丢弃的对象。 最后,我使用 **forEach**将 **println** 方法引用应用于每个幸存的对象。
`p4` 获取所有谓词并将它们组合成一个更复杂的谓词,其中包含:“如果 `String` 不包含 'bar' 且长度小于5或者它包含 'foo' ,则结果为 `true`。”因为它产生如此清晰的语法,我在`main()`中作了一些小伎俩,并借用了下一章的内容。 首先,我创建一个 `String` 对象的 “流”(序列),然后将每个对象提供给 `filter()`操作。 `filter()`使用我们的 `p4` 谓词来决定要保留流中的哪个对象以及要丢弃的对象。 最后,我使用 `forEach()``println` 方法引用应用于每个幸存的对象。
你可以从输出中看到 **p4** 是如何工作的:任何带有 “foo ”的东西都会存活即使它的长度大于5。 “fongopuckey” 太长了,没有 “bar” 来保存它。
你可以从输出中看到 `p4` 是如何工作的:任何带有 “foo ”的东西都会存活即使它的长度大于5。 “fongopuckey” 太长了,没有 “bar” 来保存它。
<!-- Currying and Partial Evaluation -->
## Currying和Partial-Evaluation
@@ -1371,7 +1374,7 @@ Hup Hey
**[2]** currying 的目标是能够通过提供一个参数来创建一个新函数,所以现在有了一个 “带参函数” 和剩下的 “无参函数” 。实际上,你从一个双参数函数开始,最后得到一个单参数函数。
可以通过添加另一个级别来 curry 一个三参数函数:
可以通过添加另一个级别来 curry 一个三参数函数:
```java
// functional/Curry3Args.java
@@ -1397,7 +1400,7 @@ Hi Ho Hup
*/
```
对于每个级别的箭头级联arrow-cascading可以围绕类型声明包装另一个函数。
对于每个级别的箭头级联arrow-cascading可以围绕类型声明包装另一个函数。
处理原始类型和装箱时,请使用适当的功能接口:
@@ -1419,27 +1422,27 @@ public class CurriedIntAdd {
*/
```
可以在因特网上找到更多 currying 示例。 通常这些是 Java 以外的语言,但如果你理解它们的基本概念,它们应该很容易翻译。
可以在因特网上找到更多 currying 示例。 通常这些是 Java 以外的语言,但如果你理解它们的基本概念,它们应该很容易翻译。
<!-- Pure Functional Programming -->
## 纯函数式编程
在没有函数支持的情况下即使用像C这样的原始语言也可以按照一定的原则编写纯函数程序。Java使它比这更容易但是必须小心地使一切都成为 **final**,并确保的所有方法和函数没有副作用。因为 Java 本质上不是一种不可变的语言,所以如果犯了错误,编译器不会提供任何帮助。
在没有函数支持的情况下即使用像C这样的原始语言也可以按照一定的原则编写纯函数程序。Java使它比这更容易但是必须小心地使一切都成为 `final`,并确保的所有方法和函数没有副作用。因为 Java 本质上不是一种不可变的语言,所以如果犯了错误,编译器不会提供任何帮助。
有第三方工具可以帮助[^9],但是使用 **Scala****Clojure** 这样的语言可能更容易,因为它们从一开始就是为保持不变性而设计的。这些语言使可以用 Java 编写项目的一部分,如果必须用纯函数式编写,则可以用**Scala** 编写其他部分 (这需要一些规则) 或 **Clojure** (这需要的少得多)。虽然将在并发编程一章中看到 Java确实支持并发但是如果这是项目的核心部分,可能会考虑至少在项目的一部分中使用 **Scala****Clojure**之类的语言。
有第三方工具可以帮助[^9],但是使用 `Scala``Clojure` 这样的语言可能更容易,因为它们从一开始就是为保持不变性而设计的。这些语言使可以用 Java 编写项目的一部分,如果必须用纯函数式编写,则可以用 `Scala` 编写其他部分 (这需要一些规则) 或 `Clojure` (这需要的少得多)。虽然将在并发编程一章中看到 Java确实支持并发但是如果这是项目的核心部分,可能会考虑至少在项目的一部分中使用 `Scala``Clojure`之类的语言。
<!-- Summary -->
## 本章小结
Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。它们对 Java 是一个巨大的改进,因为它们允许编写更简洁、更干净、更容易理解的代码。在下一章中,将看到它们如何启用流。如果你像我一样,你会喜欢流媒体。
Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。它们对 Java 是一个巨大的改进,因为它们允许编写更简洁、更干净、更容易理解的代码。在下一章中,将看到它们如何启用流。如果你像我一样,你会喜欢流媒体。
这些特性可能会满足大部分 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 表达式或方法引用 (并且经常为重写代码!)
遇到学习困难时,请记住,可以从 ide (如 NetBeans、IntelliJ Idea 和 Eclipse )获得帮助,这些 ide 将建议何时可以使用 Lambda 表达式或方法引用 (并且经常为重写代码!)
<!--下面是脚注-->
@@ -1448,8 +1451,8 @@ Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而
3. 有时函数语言将其描述为“代码即数据”。“
4. 这个语法来自 C++。
5. 我还没有验证过这种说法。
6. 在并发编程一章中,当理解更改共享变量 “不是线程安全的” 时,这将更有意义。
7. 接口能够支持方法的原因是它们是 Java 8 默认方法,将在下一章中了解到。
6. 在并发编程一章中,当理解更改共享变量 “不是线程安全的” 时,这将更有意义。
7. 接口能够支持方法的原因是它们是 Java 8 默认方法,将在下一章中了解到。
8. 一些语言,例如 Python允许像调用其他函数一样调用组合函数。但这是 Java所以我们取我们能得到的。
9. 见,例如,不可变和可变性检测器。( Immutables and Mutability Detector)

View File

@@ -997,7 +997,7 @@ Released Lock
<!-- Locking Portions of a Mapped File -->
### 映射文件的部分锁定
文件映射通常用于非常大的文件。可能需要锁定此类文件的某些部分,以便其他进程可以修改未锁定的部分。例如,数据库必须同时对许多用户可用。这里你可以看到两个线程,每个线程都锁定文件的不同部分:
文件映射通常用于非常大的文件。可能需要锁定此类文件的某些部分,以便其他进程可以修改未锁定的部分。例如,数据库必须同时对许多用户可用。这里你可以看到两个线程,每个线程都锁定文件的不同部分:
```java