Skip to content

Commit

Permalink
更新排版
Browse files Browse the repository at this point in the history
  • Loading branch information
sjsdfg committed May 27, 2019
1 parent 8d88990 commit 4ddf1e6
Show file tree
Hide file tree
Showing 22 changed files with 77 additions and 82 deletions.
18 changes: 9 additions & 9 deletions docs/notes/46. 优先考虑流中无副作用的函数.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

  如果你是一个刚开始使用流的新手,那么很难掌握它们。仅仅将计算表示为流管道是很困难的。当你成功时,你的程序将运行,但对你来说可能没有意识到任何好处。流不仅仅是一个 API,它是基于函数式编程的范式(paradigm)。为了获得流提供的可表达性、速度和某些情况下的并行性,你必须采用范式和 API。

  流范式中最重要的部分是将计算结构化为一系列转换,其中每个阶段的结果尽可能接近前一阶段结果的纯函数( pure function)。 纯函数的结果仅取决于其输入:它不依赖于任何可变状态,也不更新任何状态。 为了实现这一点,你传递给流操作的任何函数对象(中间操作和终结操作)都应该没有副作用。
  流范式中最重要的部分是将计算结构化为一系列转换,其中每个阶段的结果尽可能接近前一阶段结果的纯函数(pure function)。 纯函数的结果仅取决于其输入:它不依赖于任何可变状态,也不更新任何状态。 为了实现这一点,你传递给流操作的任何函数对象(中间操作和终结操作)都应该没有副作用。

  有时,可能会看到类似于此代码片段的流代码,该代码构建了文本文件中单词的频率表:

Expand All @@ -16,7 +16,7 @@ try (Stream<String> words = new Scanner(file).tokens()) {
}
```

  这段代码出了什么问题? 毕竟,它使用了流,lambdas 和方法引用,并得到正确的答案。 简而言之,它根本不是流代码; 它是伪装成流代码的迭代代码。 它没有从流 API 中获益,并且它比相应的迭代代码更长,更难读,并且更难于维护。 问题源于这样一个事实:这个代码在一个终结操作 `forEach` 中完成所有工作,使用一个改变外部状态(频率表)的 lambda。`forEach` 操作除了表示由一个流执行的计算结果外,什么都不做,这是代码中的臭味,就像一个改变状态的 lambda 一样。那么这段代码应该是什么样的呢?
  这段代码出了什么问题? 毕竟,它使用了流,lambdas 和方法引用,并得到正确的答案。 简而言之,它根本不是流代码; 它是伪装成流代码的迭代代码。 它没有从流 API 中获益,并且它比相应的迭代代码更长,更难读,并且更难于维护。 问题源于这样一个事实:这个代码在一个终结操作 `forEach` 中完成所有工作,使用一个改变外部状态(频率表)的 lambda。`forEach` 操作除了表示由一个流执行的计算结果外,什么都不做,这是代码中的臭味,就像一个改变状态的 lambda 一样。那么这段代码应该是什么样的呢?

```java
// Proper use of streams to initialize a frequency table
Expand All @@ -31,7 +31,7 @@ try (Stream<String> words = new Scanner(file).tokens()) {

  改进后的代码使用了收集器(collector),这是使用流必须学习的新概念。`Collectors` 的 API 令人生畏:它有 39 个方法,其中一些方法有多达 5 个类型参数。好消息是,你可以从这个 API 中获得大部分好处,而不必深入研究它的全部复杂性。对于初学者来说,可以忽略收集器接口,将收集器看作是封装缩减策略(reduction strategy)的不透明对象。在此上下文中,reduction 意味着将流的元素组合为单个对象。 收集器生成的对象通常是一个集合(它代表名称收集器)。

  将流的元素收集到真正的集合中的收集器非常简单。有三个这样的收集器:`toList()``toSet()``toCollection(collectionFactory)`。它们分别返回集合、列表和程序员指定的集合类型。有了这些知识,我们就可以编写一个流管道从我们的频率表中提取出现频率前 10 个单词的列表。
  将流的元素收集到真正的集合中的收集器非常简单。有三个这样的收集器`toList()``toSet()``toCollection(collectionFactory)`。它们分别返回集合、列表和程序员指定的集合类型。有了这些知识,我们就可以编写一个流管道从我们的频率表中提取出现频率前 10 个单词的列表。

```java
// Pipeline to get a top-ten list of words from a frequency table
Expand All @@ -43,11 +43,11 @@ List<String> topTen = freq.keySet().stream()

  注意,我们没有对 `toList` 方法的类收集器进行限定。**静态导入收集器的所有成员是一种惯例和明智的做法,因为它使流管道更易于阅读。**

  这段代码中唯一比较棘手的部分是我们把 `comparing(freq::get).reverse()` 传递给 `sort` 方法。comparing 是一种比较器构造方法 (条目 14),它具有一个 key 的提取方法。该函数接受一个单词,而“提取”实际上是一个表查找:绑定方法引用 `freq::get``frequency` 表中查找单词,并返回单词出现在文件中的次数。最后,我们在比较器上调用 `reverse` 方法,因此我们将单词从最频繁到最不频繁进行排序。然后,将流限制为 10 个单词并将它们收集到一个列表中就很简单了。
  这段代码中唯一比较棘手的部分是我们把 `comparing(freq::get).reverse()` 传递给 `sort` 方法。comparing 是一种比较器构造方法(详见第 14 条),它具有一个 key 的提取方法。该函数接受一个单词,而“提取”实际上是一个表查找:绑定方法引用 `freq::get``frequency` 表中查找单词,并返回单词出现在文件中的次数。最后,我们在比较器上调用 `reverse` 方法,因此我们将单词从最频繁到最不频繁进行排序。然后,将流限制为 10 个单词并将它们收集到一个列表中就很简单了。

  前面的代码片段使用 `Scanner``stream` 方法在 `scanner` 实例上获取流。这个方法是在 Java 9 中添加的。如果正在使用较早的版本,可以使用类似于条目 47 中 (`streamOf(Iterable<E>)`) 的适配器将实现了 `Iterator``scanner` 序转换为流。

  那么收集器中的其他 36 种方法呢?它们中的大多数都是用于将流收集到 map 中的,这比将流收集到真正的集合中要复杂得多。每个流元素都与一个键和一个值相关联,多个流元素可以与同一个键相关联。
  那么收集器中的其他 36 种方法呢它们中的大多数都是用于将流收集到 map 中的,这比将流收集到真正的集合中要复杂得多。每个流元素都与一个键和一个值相关联,多个流元素可以与同一个键相关联。

  最简单的映射收集器是 toMap(keyMapper、valueMapper),它接受两个函数,一个将流元素映射到键,另一个映射到值。在条目 34 中的 `fromString` 实现中,我们使用这个收集器从 enum 的字符串形式映射到 enum 本身:

Expand All @@ -60,7 +60,7 @@ private static final Map<String, Operation> stringToEnum =

  如果流中的每个元素都映射到唯一键,则这种简单的 `toMap` 形式是完美的。 如果多个流元素映射到同一个键,则管道将以 `IllegalStateException` 终止。

  `toMap` 更复杂的形式,以及 `groupingBy` 方法,提供了处理此类冲突 (collisions) 的各种方法。一种方法是向 `toMap` 方法提供除键和值映射器 (mappers) 之外的 `merge` 方法。`merge` 方法是一个 `BinaryOperator<V>`,其中 V是 map 的值类型。与键关联的任何附加值都使用 `merge` 方法与现有值相结合,因此,例如,如果 merge 方法是乘法,那么最终得到的结果是是值 `mapper` 与键关联的所有值的乘积。
  `toMap` 更复杂的形式,以及 `groupingBy` 方法,提供了处理此类冲突 (collisions) 的各种方法。一种方法是向 `toMap` 方法提供除键和值映射器mappers之外的 `merge` 方法。`merge` 方法是一个 `BinaryOperator<V>`,其中 V是 map 的值类型。与键关联的任何附加值都使用 `merge` 方法与现有值相结合,因此,例如,如果 merge 方法是乘法,那么最终得到的结果是是值 `mapper` 与键关联的所有值的乘积。

  `toMap` 的三个参数形式对于从键到与该键关联的选定元素的映射也很有用。例如,假设我们有一系列不同艺术家(artists)的唱片集(albums),我们想要一张从唱片艺术家到最畅销专辑的 map。这个收集器将完成这项工作。

Expand All @@ -70,7 +70,7 @@ Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
```

  请注意,比较器使用静态工厂方法 `maxBy`,它是从 BinaryOperator 静态导入的。 此方法将 `Comparator<T>` 转换为 `BinaryOperator<T>`,用于计算指定比较器隐含的最大值。 在这种情况下,比较器由比较器构造方法 comparing 返回,它采用 key 提取器函数 `Album::sales`。 这可能看起来有点复杂,但代码可读性很好。 简而言之,它说,将专辑(albums)流转换为地 map,将每位艺术家(artist)映射到销售量最佳的专辑。这与问题陈述出奇得接近。
  请注意,比较器使用静态工厂方法 `maxBy`,它是从 BinaryOperator 静态导入的。 此方法将 `Comparator<T>` 转换为 `BinaryOperator<T>`,用于计算指定比较器隐含的最大值。 在这种情况下,比较器由比较器构造方法 comparing 返回,它采用 key 提取器函数 `Album::sales`。 这可能看起来有点复杂,但代码可读性很好。 简而言之,它说,将专辑(albums)流转换为地 map,将每位艺术家(artist)映射到销售量最佳的专辑。这与问题陈述出奇得接近。

  `toMap` 的三个参数形式的另一个用途是产生一个收集器,当发生冲突时强制执行 last-write-wins 策略。 对于许多流,结果是不确定的,但如果映射函数可能与键关联的所有值都相同,或者它们都是可接受的,则此收集器的行为可能正是您想要的:

Expand All @@ -83,7 +83,7 @@ toMap(keyMapper, valueMapper, (oldVal, newVal) ->newVal)

  `toMap` 的前三个版本也有变体形式,名为 `toConcurrentMap`,它们并行高效运行并生成 `ConcurrentHashMap` 实例。

  除了 toMap 方法之外,Collectors API 还提供了 `groupingBy` 方法,该方法返回收集器以生成基于分类器函数 (classifier function) 将元素分组到类别中的 map。 分类器函数接受一个元素并返回它所属的类别。 此类别来用作元素的 map 的键。 `groupingBy` 方法的最简单版本仅采用分类器并返回一个 map,其值是每个类别中所有元素的列表。 这是我们在条目 45 中的 `Anagram` 程序中使用的收集器,用于生成从按字母顺序排列的单词到单词列表的 map:
  除了 toMap 方法之外,Collectors API 还提供了 `groupingBy` 方法,该方法返回收集器以生成基于分类器函数 classifier function将元素分组到类别中的 map。 分类器函数接受一个元素并返回它所属的类别。 此类别来用作元素的 map 的键。 `groupingBy` 方法的最简单版本仅采用分类器并返回一个 map,其值是每个类别中所有元素的列表。 这是我们在条目 45 中的 `Anagram` 程序中使用的收集器,用于生成从按字母顺序排列的单词到单词列表的 map:

```java
Map<String, Long> freq = words
Expand All @@ -93,7 +93,7 @@ Map<String, Long> freq = words

  `groupingByConcurrent` 方法提供了 `groupingBy` 的所有三个重载的变体。 这些变体并行高效运行并生成 `ConcurrentHashMap` 实例。 还有一个很少使用的 grouping 的亲戚称为 `partitioningBy`。 代替分类器方法,它接受 `predicate` 并返回其键为布尔值的 map。 此方法有两种重载,除了 `predicate` 之外,其中一种方法还需要 `downstream` 收集器。

  通过 `counting` 方法返回的收集器仅用作下游收集器。 Stream 上可以通过 count 方法直接使用相同的功能,因此没有理由说 `collect(counting())`。 此属性还有十五种收集器方法。 它们包括九个方法,其名称以 `summing``averaging``summarizing` 开头(其功能在相应的原始流类型上可用)。 它们还包括 `reduce` 方法的所有重载,以及 filter,`mapping``flatMapping``collectingAndThen` 方法。 大多数程序员可以安全地忽略大多数这些方法。 从设计的角度来看,这些收集器代表了尝试在收集器中部分复制流的功能,以便下游收集器可以充当迷你流 (ministreams)”
  通过 `counting` 方法返回的收集器仅用作下游收集器。 Stream 上可以通过 count 方法直接使用相同的功能,因此没有理由说 `collect(counting())`。 此属性还有十五种收集器方法。 它们包括九个方法,其名称以 `summing``averaging``summarizing` 开头(其功能在相应的原始流类型上可用)。 它们还包括 `reduce` 方法的所有重载,以及 filter,`mapping``flatMapping``collectingAndThen` 方法。 大多数程序员可以安全地忽略大多数这些方法。 从设计的角度来看,这些收集器代表了尝试在收集器中部分复制流的功能,以便下游收集器可以充当迷你流ministreams)」

  我们还有三种收集器方法尚未提及。 虽然他们在收 `Collectors` 类中,但他们不涉及集合。 前两个是 `minBy``maxBy`,它们取比较器并返回比较器确定的流中的最小或最大元素。 它们是 `Stream` 接口中 `min``max` 方法的次要总结,是 `BinaryOperator` 中类似命名方法返回的二元运算符的类似收集器。 回想一下,我们在最畅销的专辑中使用了 `BinaryOperator.maxBy` 方法。

Expand Down
2 changes: 1 addition & 1 deletion docs/notes/48. 谨慎使用流并行.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ static Stream<BigInteger> primes() {

  即使假设正在使用一个高效的可拆分的源流、一个可并行化的或廉价的终端操作以及非干扰的函数对象,也无法从并行化中获得良好的加速效果,除非管道做了足够的实际工作来抵消与并行性相关的成本。作为一个非常粗略的估计,流中的元素数量乘以每个元素执行的代码行数应该至少是 100,000 [Lea14]

  重要的是要记住并行化流是严格的性能优化。 与任何优化一样,必须在更改之前和之后测试性能,以确保它值得做( 67 )。 理想情况下,应该在实际的系统设置中执行测试。 通常,程序中的所有并行流管道都在公共 fork-join 池中运行。 单个行为不当的管道可能会损害系统中不相关部分的其他行为。
  重要的是要记住并行化流是严格的性能优化。 与任何优化一样,必须在更改之前和之后测试性能,以确保它值得做(详见第 67 )。 理想情况下,应该在实际的系统设置中执行测试。 通常,程序中的所有并行流管道都在公共 fork-join 池中运行。 单个行为不当的管道可能会损害系统中不相关部分的其他行为。

  如果在并行化流管道时,这种可能性对你不利,那是因为它们确实存在。一个认识的人,他维护一个数百万行代码库,大量使用流,他发现只有少数几个地方并行流是有效的。这并不意味着应该避免并行化流。**在适当的情况下,只需向流管道添加一个 `parallel` 方法调用,就可以实现处理器内核数量的近似线性加速。** 某些领域,如机器学习和数据处理,特别适合这些加速。

Expand Down
Loading

0 comments on commit 4ddf1e6

Please sign in to comment.