From 4cf57da5fca73fac66a0a4895747323ff1b72799 Mon Sep 17 00:00:00 2001 From: andyphone <792998301@qq.com> Date: Tue, 8 Dec 2020 15:46:27 +0800 Subject: [PATCH] =?UTF-8?q?13-14=20=E4=BF=AE=E6=94=B9=E8=AF=AD=E5=8F=A5?= =?UTF-8?q?=E6=8B=97=E5=8F=A3=E9=97=AE=E9=A2=98=20(#631)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 1 * 2 * 没有第三种形式吧? * 修改语句拗口问题 * 修改语句拗口问题 2 * 修改语句拗口问题 3 * 修改语句拗口问题 4 * 修改语句拗口问题 5 * 修改语句拗口问题 6 * 修改语句拗口问题 7 * 修改语句拗口问题 8 * 修改语句拗口问题 9 * 拗口问题 * 修改语句拗口问题 10 * 修改语句拗口问题 11 * 修改语句拗口问题 12 * 修改语句拗口问题 13 * 修改语句拗口问题 14 * 修改语句拗口问题 14 * 修改语句拗口问题 15 * 修改语句拗口问题 16 * 修改语句拗口问题 17 * 十三章修改语句拗口问题 * 13-14 修改语句拗口问题 * 13-14 修改语句拗口问题 2 * 13-14 修改语句拗口问题 3 * 13-14 修改语句拗口问题 4 * Update 13-Functional-Programming.md Co-authored-by: Joe <736777445@qq.com> --- docs/book/11-Inner-Classes.md | 2 +- docs/book/13-Functional-Programming.md | 16 ++++----- docs/book/14-Streams.md | 50 +++++++++++++------------- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/docs/book/11-Inner-Classes.md b/docs/book/11-Inner-Classes.md index caf557e8..66d3d5a1 100755 --- a/docs/book/11-Inner-Classes.md +++ b/docs/book/11-Inner-Classes.md @@ -1411,7 +1411,7 @@ Anonymous inner 8 Anonymous inner 9 ``` -**Counter** 返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能用于实例初始化。 +**Counter** 返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能使用实例初始化。 所以使用局部内部类而不使用匿名内部类的另一个理由就是,需要不止一个该内部类的对象。 diff --git a/docs/book/13-Functional-Programming.md b/docs/book/13-Functional-Programming.md index 9cef7164..65f3ece4 100644 --- a/docs/book/13-Functional-Programming.md +++ b/docs/book/13-Functional-Programming.md @@ -1429,9 +1429,9 @@ Hup Hey **[1]** 这一连串的箭头很巧妙。*注意*,在函数接口声明中,第二个参数是另一个函数。 -**[2]** 柯里化的目的是能够通过提供一个参数来创建一个新函数,所以现在有了一个“带参函数”和剩下的 “自由函数”(free argumnet) 。实际上,你从一个双参数函数开始,最后得到一个单参数函数。 +**[2]** 柯里化的目的是能够通过提供单个参数来创建一个新函数,所以现在有了一个“带参函数”和剩下的 “自由函数”(free argument) 。实际上,你从一个双参数函数开始,最后得到一个单参数函数。 -我们可以通过添加级别来柯里化一个三参数函数: +我们可以通过继续添加层级来柯里化一个三参数函数: ```java // functional/Curry3Args.java @@ -1460,7 +1460,7 @@ public class Curry3Args { Hi Ho Hup ``` -对于每个级别的箭头级联(Arrow-cascading),你都要在类型声明中包裹另一层 **Function**。 +对于每一级的箭头级联(Arrow-cascading),你都会在类型声明周围包裹另一个 **Function** 。 处理基本类型和装箱时,请使用适当的函数式接口: @@ -1491,17 +1491,17 @@ 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` 之类的语言。 ## 本章小结 -Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。这对 Java 来说是一个巨大的改进。因为这允许你编写更简洁明了,易于理解的代码。在下一章中,你会看到它们在流式编程中的应用。相信你会像我一样,喜欢上流式编程。 +Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。这对 Java 来说是一个巨大的改进。因为这允许你编写更简洁明了,易于理解的代码。在下一章中,你会看到它们在 *流式编程(streams)* 中的应用。相信你会像我一样,喜欢上流式编程。 -这些特性满足了很多羡慕Clojure、Scala 这类更函数化语言的程序员,并且阻止了Java程序员转向那些更函数化的语言(就算不能阻止,起码提供了更好的选择)。 +这些特性满足了很大一部分的、羡慕Clojure 和 Scala 这类更函数化语言的Java程序员。阻止了他们投奔向那些语言(或者至少让他们在投奔之前做好准备)。 但是,Lambdas 和方法引用远非完美,我们永远要为 Java 设计者早期的草率决定付出代价。特别是没有泛型 Lambda,所以 Lambda 在 Java 中并非一等公民。虽然我不否认 Java 8 的巨大改进,但这意味着和许多 Java 特性一样,它终究还是会让人感觉沮丧和鸡肋。 @@ -1516,7 +1516,7 @@ Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而 [^5]: 我还没有验证过这种说法。 [^6]: 当你理解了[并发编程](./24-Concurrent-Programming.md)章节的内容,你就能明白为什么更改共享变量 “不是线程安全的” 的了。 [^7]: 接口能够支持方法的原因是它们是 Java 8 默认方法,你将在下一章中了解到。 -[^8]: 一些语言,如 Python,允许像调用其他函数一样调用组合函数。但这是 Java,所以我们做做可为之事。 +[^8]: 一些语言,如 Python,允许像调用其他函数一样调用组合函数。但这是 Java,所以我们要量力而行。 [^9]: 例如,[Immutables](https://immutables.github.io/) 和 [Mutability Detector](https://mutabilitydetector.github.io/MutabilityDetector/)。 diff --git a/docs/book/14-Streams.md b/docs/book/14-Streams.md index ba2fd7c6..253968be 100644 --- a/docs/book/14-Streams.md +++ b/docs/book/14-Streams.md @@ -3,15 +3,17 @@ # 第十四章 流式编程 -> 集合优化了对象的存储,而流和对象的处理有关。 +> 集合优化了对象的存储,而流(Streams)则是关于一组组对象的处理。 -流是一系列与特定存储机制无关的元素——实际上,流并没有“存储”之说。 +流(Streams)是与任何特定存储机制无关的元素序列——实际上,我们说流是 "没有存储 "的。 -使用流,无需迭代集合中的元素,就可以从管道提取和操作元素。这些管道通常被组合在一起,形成一系列对流进行操作的管道。 +取代了在集合中迭代元素的做法,使用流即可从管道中提取元素并对其操作。这些管道通常被串联在一起形成一整套的管线,来对流进行操作。 -在大多数情况下,将对象存储在集合中是为了处理他们,因此你将会发现你将把编程的主要焦点从集合转移到了流上。流的一个核心好处是,它使得程序更加短小并且更易理解。当 Lambda 表达式和方法引用(method references)和流一起使用的时候会让人感觉自成一体。流使得 Java 8 更具吸引力。 +在大多数情况下,将对象存储在集合中就是为了处理它们,因此你会发现你把编程的主要焦点从集合转移到了流上。 -举个例子,假如你要随机展示 5 至 20 之间不重复的整数并进行排序。实际上,你的关注点首先是创建一个有序集合。围绕这个集合进行后续的操作。但是使用流式编程,你就可以简单陈述你想做什么: +流的一个核心好处是,它使得程序更加短小并且更易理解。当 Lambda 表达式和方法引用(method references)和流一起使用的时候会让人感觉自成一体。流使得 Java 8 更具吸引力。 + +举个例子,假如你要随机展示 5 至 20 之间不重复的整数并进行排序。你要对它们进行排序的事实,会使你首先关注选用哪个有序集合,然后围绕这个集合进行后续的操作。但是使用流式编程,你就可以简单陈述你要做什么: ```java // streams/Randoms.java @@ -40,11 +42,11 @@ public class Randoms { 19 ``` -首先,我们给 **Random** 对象一个种子(以便程序再次运行时产生相同的输出)。`ints()` 方法产生一个流并且 `ints()` 方法有多种方式的重载 — 两个参数限定了产生的数值的边界。这将生成一个随机整数流。我们用中间流操作(intermediate stream operation) `distinct()` 使流中的整数不重复,然后使用 `limit()` 方法获取前 7 个元素。接下来使用 `sorted()` 方法排序。最终使用 `forEach()` 方法遍历输出,它根据传递给它的函数对流中的每个对象执行操作。在这里,我们传递了一个可以在控制台显示每个元素的方法引用:`System.out::println` 。 +首先,我们给 **Random** 对象一个种子值47(以便程序再次运行时产生相同的输出)。`ints()` 方法产生一个流并且 `ints()` 方法有多种方式的重载 —— 两个参数限定了产生的数值的边界。这将生成一个随机整数流。我们用 *流的中间操作*(intermediate stream operation) `distinct()` 使流中的整数不重复,然后使用 `limit()` 方法获取前 7 个元素。接下来使用 `sorted()` 方法排序。最终使用 `forEach()` 方法遍历输出,它根据传递给它的函数对流中的每个对象执行操作。在这里,我们传递了一个可以在控制台显示每个元素的方法引用:`System.out::println` 。 -注意 `Randoms.java` 中没有声明任何变量。流可以在不使用赋值或可变数据的情况下对有状态的系统建模,这非常有用。 +注意 `Randoms.java` 中没有声明任何变量。流可以在不曾使用赋值或可变数据的情况下,对有状态的系统建模,这非常有用。 -声明式编程(Declarative programming)是一种编程风格,它声明想要做什么,而非指明如何做。正如我们在函数式编程中所看到的。你会发现命令式编程的形式更难以理解。代码示例: +*声明式编程*(Declarative programming)是一种编程风格——它声明了要做什么,而不是指明(每一步)如何做。而这正是我们在函数式编程中所看到的(编程风格)。你会注意到,命令式(Imperative)编程的形式(指明每一步如何做)会更难理解: ```java // streams/ImperativeRandoms.java @@ -71,9 +73,9 @@ public class ImperativeRandoms { 在 `Randoms.java` 中,我们无需定义任何变量,但在这里我们定义了 3 个变量: `rand`,`rints` 和 `r`。由于 `nextInt()` 方法没有下限的原因(其内置的下限永远为 0),这段代码实现起来更复杂。所以我们要生成额外的值来过滤小于 5 的结果。 -注意,你必须用力的研究才能弄明白`ImperativeRandoms.java`程序在干什么。而在 `Randoms.java` 中,代码直接告诉了你它正在做什么。这种语义的清晰性是使用Java 8 流式编程的重要原因之一。 +注意,你必须研究代码才能搞清楚`ImperativeRandoms.java`程序在做什么。而在 `Randoms.java` 中,代码会直接告诉你它在做什么。这种语义的清晰性是使用Java 8 流式编程的重要原因之一。 -像在 `ImperativeRandoms.java` 中那样显式地编写迭代过程的方式称为外部迭代。而在 `Randoms.java` 中,你看不到任何上述的迭代过程,所以它被称为内部迭代,这是流式编程的一个核心特征。内部迭代产生的代码可读性更强,而且能更简单的使用多核处理器。通过放弃对迭代过程的控制,可以把控制权交给并行化机制。我们将在[并发编程](24-Concurrent-Programming.md)一章中学习这部分内容。 +像在 `ImperativeRandoms.java` 中那样显式地编写迭代过程的方式称为*外部迭代(external iteration)*。而在 `Randoms.java` 中,你看不到任何上述的迭代过程,所以它被称为*内部迭代(internal iteration)*,这是流式编程的一个核心特征。内部迭代产生的代码可读性更强,而且能更简单的使用多核处理器。通过放弃对迭代过程的控制,可以把控制权交给并行化机制。我们将在[并发编程](24-Concurrent-Programming.md)一章中学习这部分内容。 另一个重要方面,流是懒加载的。这代表着它只在绝对必要时才计算。你可以将流看作“延迟列表”。由于计算延迟,流使我们能够表示非常大(甚至无限)的序列,而不需要考虑内存问题。 @@ -87,7 +89,7 @@ Java 设计者面临着这样一个难题:现存的大量类库不仅为 Java 一个大的挑战来自于使用接口的库。集合类是其中关键的一部分,因为你想把集合转为流。但是如果你将一个新方法添加到接口,那就破坏了每一个实现接口的类,因为这些类都没有实现你添加的新方法。 -Java 8 采用的解决方案是:在[接口](10-Interfaces.md)中添加被 `default`(`默认`)修饰的方法。通过这种方案,设计者们可以将流式(*stream*)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是到集合中)。 +Java 8 采用的解决方案是:在[接口](10-Interfaces.md)中添加被 `default`(`默认`)修饰的方法。通过这种方案,设计者们可以将流式(*stream*)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是汇入一个集合)。 下面我们来看下每种类型的流操作。 @@ -265,7 +267,7 @@ public class RandomGenerators { 为了消除冗余代码,我创建了一个泛型方法 `show(Stream stream)` (在讲解泛型之前就使用这个特性,确实有点作弊,但是回报是值得的)。类型参数 `T` 可以是任何类型,所以这个方法对 **Integer**、**Long** 和 **Double** 类型都生效。但是 **Random** 类只能生成基本类型 **int**, **long**, **double** 的流。幸运的是, `boxed()` 流操作将会自动地把基本类型包装成为对应的装箱类型,从而使得 `show()` 能够接受流。 -我们可以使用 **Random** 为任意对象集合创建 **Supplier**。如下是一个文本文件提供字符串对象的例子。 +我们可以使用 **Random** 为任意对象集合创建 **Supplier**。从文本文件提供字符串对象的例子如下。 Cheese.dat 文件内容: @@ -321,9 +323,9 @@ public class RandomWords implements Supplier { it shop sir the much cheese by conclusion district is ``` -在这里可以看到 `split()` 更复杂的运用。在构造器里,每一行都被 `split()` 通过方括号内的空格或其它标点符号分割。在方括号后面的 `+` 表示 `+` 前面的东西可以出现一次或者多次。 +在这里可以看到 `split()` 更复杂的运用。在构造器里,每一行都被 `split()` 通过方括号内的空格或其它标点符号分割。在方括号后面的 `+` 表示 `+` 前面的东西可以出现一次或者多次(正则表达式)。 -你会发现构造函数使用命令式编程(外部迭代)进行循环。在以后的例子中,你会看到我们是如何去除命令式编程的使用。这种旧的形式虽不是特别糟糕,但使用流会让人感觉更好。 +你会发现构造函数使用命令式编程(外部迭代)进行循环。在以后的例子中,你会看到我们是如何去除命令式编程。这种旧的形式虽不是特别糟糕,但使用流会让人感觉更好。 在`toString()` 和`main()`方法中你看到了 `collect()` 操作,它根据参数来结合所有的流元素。当你用 `Collectors.joining()`作为 `collect()` 的参数时,将得到一个`String` 类型的结果,该结果是流中的所有元素被`joining()`的参数隔开。还有很多不同的 `Collectors` 用于产生不同的结果。 @@ -365,7 +367,7 @@ public class Ranges { 在主方法中的第一种方式是我们传统编写 `for` 循环的方式;第二种方式,我们使用 `range()` 创建了流并将其转化为数组,然后在 `for-in` 代码块中使用。但是,如果你能像第三种方法那样全程使用流是更好的。我们对范围中的数字进行求和。在流中可以很方便的使用 `sum()` 操作求和。 -注意 **IntStream.**`range()` 相比 `onjava.Range.range()` 拥有更多的限制。这是由于其可选的第三个参数,后者允许步长大于 1,并且可以从大到小来生成。 +注意 **IntStream.**`range()` 相比 `onjava.Range.range()` 受更多限制。这是由于其可选的第三个参数,后者允许步长大于 1,并且可以从大到小来生成。 实用小功能 `repeat()` 可以用来替换简单的 `for` 循环。代码示例: @@ -406,7 +408,7 @@ Hi! Hi! ``` -原则上,在代码中包含并解释 `repeat()` 并不值得。诚然它是一个相当透明的工具,但结果取决于你的团队和公司的运作方式。 +原则上,在代码中包含和解释 `repeat()` 并不值得。诚然它是一个相当透明的工具,但这取决于你的团队和公司的运作方式。 ### generate() @@ -441,7 +443,7 @@ public class Generator implements Supplier { YNZBRNYGCFOWZNTCQRGSEGZMMJMROE ``` -使用 `Random.nextInt()` 方法来挑选字母表中的大写字母。`Random.nextInt()` 的参数代表可以接受的最大的随机数范围,所以使用数组边界是经过深思熟虑的。 +使用 `Random.nextInt()` 方法来挑选字母表中的大写字母。`Random.nextInt()` 的参数代表可以接受的最大的随机数范围,所以使用数组边界是经过慎重考虑的。 如果要创建包含相同对象的流,只需要传递一个生成那些对象的 `lambda` 到 `generate()` 中: @@ -513,11 +515,11 @@ Bubble(3) Bubble(4) ``` -这是创建单独工厂类(Separate Factory class)的另一种方式。在很多方面它更加整洁,但是这是一个对于代码组织和品味的问题——你总是可以创建一个完全不同的工厂类。 +这是创建单独工厂类(Separate Factory class)的另一种方式。在很多方面它更加整洁,但是这是一个关于代码组织和品味的问题——你总是可以创建一个完全不同的工厂类。 ### iterate() -`Stream.iterate()` 产生的流的第一个元素是种子(iterate方法的第一个参数),然后将种子传递给方法(iterate方法的第二个参数)。方法运行的结果被添加到流(作为流的第二个元素),并存储起来作为下次调用 `iterate()`时的第一个参数,以此类推。我们可以利用 `iterate()` 生成一个斐波那契数列。代码示例: +`Stream.iterate()` 产生的流的第一个元素是种子(iterate方法的第一个参数),然后将种子传递给方法(iterate方法的第二个参数)。方法运行的结果被添加到流(作为流的下一个元素),并被存储起来,作为下次调用 `iterate()`方法时的第一个参数,以此类推。我们可以利用 `iterate()` 生成一个斐波那契数列(上一章已经遇到过Fibonacci)。代码示例: ```java // streams/Fibonacci.java @@ -563,7 +565,7 @@ public class Fibonacci { ### 流的建造者模式 -在建造者模式(Builder design pattern)中,首先创建一个 `builder` 对象,然后将创建流所需的多个信息传递给它,最后`builder` 对象执行”创建“流的操作。**Stream** 库提供了这样的 `Builder`。在这里,我们重新审视文件读取并将其转换成为单词流的过程。代码示例: +在*建造者模式*(Builder design pattern)中,首先创建一个 `builder` 对象,然后将创建流所需的多个信息传递给它,最后`builder` 对象执行”创建“流的操作。**Stream** 库提供了这样的 `Builder`。在这里,我们重新审视文件读取并将其转换成为单词流的过程。代码示例: ```java // streams/FileToWordsBuilder.java @@ -722,7 +724,7 @@ public class FileToWordsRegexp { Not much of a cheese shop really is it ``` -在构造器中我们读取了文件中的所有内容(跳过第一行注释,并将其转化成为单行字符串)。现在,当你调用 `stream()` 的时候,可以像往常一样获取一个流,但这次你可以多次调用 `stream()` 在已存储的字符串中创建一个新的流。这里有个限制,整个文件必须存储在内存中;在大多数情况下这并不是什么问题,但是这损失了流操作非常重要的优势: +在构造器中我们读取了文件中的所有内容(跳过第一行注释,并将其转化成为单行字符串)。现在,当你调用 `stream()` 的时候,可以像往常一样获取一个流,但这回你可以多次调用 `stream()` ,每次从已存储的字符串中创建一个新的流。这里有个限制,整个文件必须存储在内存中;在大多数情况下这并不是什么问题,但是这丢掉了流操作非常重要的优势: 1. “不需要把流存储起来。”当然,流确实需要一些内部存储,但存储的只是序列的一小部分,和存储整个序列不同。 2. 它们是懒加载计算的。 @@ -795,11 +797,11 @@ you what to the that sir leads in district And ### 移除元素 -* `distinct()`:在 `Randoms.java` 类中的 `distinct()` 可用于消除流中的重复元素。相比创建一个 **Set** 集合,该方法的工作量要少得多。 +* `distinct()`:在 `Randoms.java` 类中的 `distinct()` 可用于消除流中的重复元素。相比创建一个 **Set** 集合来消除重复,该方法的工作量要少得多。 -* `filter(Predicate)`:若元素传递给过滤函数产生的结果为`true` ,则过滤操作保留这些元素。 +* `filter(Predicate)`:过滤操作,保留如下元素:若元素传递给过滤函数产生的结果为`true` 。 -在下例中,`isPrime()` 作为过滤器函数,用于检测质数。 +在下例中,`isPrime()` 作为过滤函数,用于检测质数。 ```java // streams/Prime.java