Effective Java(3rd)-Item46 在流中使用无副作用的函数

  如果你是stream的新手,你可能很难掌握它们.仅仅将计算表示为流管道可能很难。当您成功时,您的程序将运行,但是您可能没有意识到任何好处。Streams不仅仅是一个API,它还是一个基于函数式编程的范例。为了获得流必须提供的表达性、速度和某些情况下的并行性,您必须同时采用该范例和API。
  streams范例最重要的部分是将计算结构为一个转换序列,其中每个阶段的结果都尽可能接近前一个阶段结果的纯函数.纯函数的结果只依赖于它的输入:它不依赖于任何可变状态,也不更新任何状态.为了实现这一点,您传递到流操作(包括中间操作和终端操作)的任何函数对象都应该没有副作用。
  偶尔,您可能会看到类似于这个代码片段的流代码,它构建了文本文件中单词的频率表:


image.png

  这段代码有什么问题?毕竟,它使用了流、lambdas和方法引用,并得到了正确的答案。简单地说,它根本不是流代码;它是伪装成流代码的迭代代码。它没有从streams API中获得任何好处,而且它(有点)比相应的迭代代码更长、更难以阅读、更难以维护。这个问题源于这样一个事实:这段代码在一个终端forEach操作中执行所有的工作,使用一个改变外部状态的lambda(频率表).一个forEach操作除了显示流执行的计算结果之外,什么都不做,这是一个“代码中的坏味道”,就像一个改变状态的lambda一样。那么这段代码应该是什么样的呢?


image.png

  这段代码执行与前一段相同的操作,但是正确地使用了streams API.它更短更清晰。为什么会有人用另一种方式写呢?因为它使用了他们已经熟悉的工具。Java程序员知道如何使用for-each循环,forEach终端操作也类似。但是forEach操作是最不强大的终端操作之一,也是最不友好的流操作之一.它是显式迭代的,因此不适合并行化. forEach操作应该只用于报告流计算的结果,而不是执行计算.有时候,将forEach用于其他目的是有意义的,比如将流计算的结果添加到预先存在的集合中。

  改进后的代码使用collector,这是一个必须学习的新概念,以便使用流。Collectors API很吓人:它有39个方法,其中一些有多达5个类型参数.好消息是,您可以从这个API中获得大部分好处,而不必深入研究它的全部复杂性.对于初学者,可以忽略Collector 接口,将收集器看作封装了缩减策略的不透明对象.在此上下文中,缩减意味着将流的元素组合成单个对象。收集器生成的对象通常是一个集合(它解释了名称收集器)。
  用于将流的元素收集到真正的集合中的收集器非常简单。有三种这样的收集器:toList()、toSet()和toCollection(collectionFactory)。它们分别返回一个集合、一个列表和一个程序员指定的集合类型。有了这些知识,我们就可以编写一个流管道来从频率表中提取前十位列表。


image.png

  注意,我们还没有对toList方法及其类collector进行限定。 静态导入收集器的所有成员是一种习惯,也是一种明智的做法,因为它使流管道更具可读性。
   这段代码中惟一棘手的部分是传递给已排序比较器compare (freq::get).reverse()的比较器。comparing 方法是一种比较器构造方法(item14),它具有一个关键的提取函数。函数接受一个单词,而“提取”实际上是一个表查找:绑定方法引用freq::get在频率表中查找单词,并返回单词在文件中出现的次数。最后,我们在comparator上调用reverse方法.我们把单词从最频繁的到最不频繁的排序.然后,很简单的事情就是把单词量限制在10个以内,并把它们收集到一个列表中。

  前面的代码片段使用Scanner 的流方法获得scanner 上的流。这个方法是在Java 9中添加的。如果使用较早的版本,可以使用与第47项(streamOf(Iterable<E>)中的适配器将实现Iterator的扫描器转换成流。
  那么Collectors中的其他36种方法呢?大多数流的存在是为了让您将流收集到map中,这比将流收集到真正的集合要复杂得多。每个流元素与一个键和一个值关联,多个流元素可以与同一个键关联。
  最简单的映射收集器是toMap(keyMapper, valueMapper),它接受两个函数,其中一个函数将流元素映射到键,另一个映射到值。我们在项目34的fromString实现中使用了这个收集器,从枚举的字符串形式映射到枚举本身:


image.png

  如果流中的每个元素都映射到一个惟一的键,那么这种简单形式的toMap是完美的。如果多个流元素映射到同一个键,管道将使用IllegalStateException终止。
  更复杂的toMap形式以及group by方法为您提供了各种方法来提供处理此类冲突的策略。一种方法是,除了键和值映射器之外,还为toMap方法提供一个合并函数。merge函数是一个BinaryOperator<V>,其中V是映射的值类型。任何与键关联的附加值都将使用merge函数与现有值组合在一起,因此,例如,如果merge函数是乘法,那么最终得到的值就是value mapper与键关联的所有值的乘积。
  toMap的三参数形式对于从键到与该键关联的所选元素的映射也很有用。例如,假设我们有一个由不同艺术家录制的唱片流,并且我们想要一个从唱片艺术家到畅销唱片的映射。这个收集器将完成这项工作。


image.png

  注意,比较器使用静态工厂方法maxBy,该方法从BinaryOperator静态导入。此方法将Comparator<T>转换为BinaryOperator<T>,该操作符计算指定比较器所隐含的最大值。在这种情况下,比较器是通过比较器构造方法返回比较器的,比较器构造方法取key extractor的函数Album::sales。这看起来有点复杂,但是代码读起来很好。粗略地说,它是这样说的:“将专辑流转换为一张Album,将每个艺人映射到销量最好的专辑。”“这与问题陈述惊人地接近。
  toMap的三参数形式的另一个用途是生成一个收集器,当发生冲突时,它强制执行last-write-wins策略。对于许多流,结果将是不确定的,但如果映射函数可能与键关联的所有值都是相同的,或者它们都是可接受的,那么这个收集器的行为可能正是您想要的:


image.png

  toMap的第三个也是最后一个版本采用了第四个参数,这是一个map factory,当您想要指定一个特定的map实现(如EnumMap或TreeMap)时,可以使用它。
  还有前三个版本的toMap的变体形式,名为toConcurrentMap,它们可以有效地并行运行并生成ConcurrentHashMap实例。
  除了toMap方法之外,collector API还提供groupingBy方法,该方法返回收集器,以生成基于分类器函数将元素分组为类别的映射。分类器函数接受一个元素并返回它所属的类别。这个类别用作元素的map键。groupingBy方法的最简单版本只接受一个分类器并返回一个map,其值是每个类别中所有元素的列表。这是我们在第45项的字谜程序中使用的收集器,用于生成从按字母顺序排列的单词到共享字母顺序的单词列表的映射:


image.png

  如果您希望groupingBy返回一个使用列表之外的值生成映射的收集器,您可以指定一个下游收集器和一个分类器。下游收集器从包含所有类别中的元素。这个参数最简单的用法是传递toSet(),这会生成一个map,其值是元素集,而不是列表。
  或者,您可以传递toCollection(collectionFactory),它允许您创建集合,将每个类别的元素放入其中。这使您可以灵活地选择所需的任何集合类型。groupingBy的两参数形式的另一个简单用法是将counting()作为下游收集器传递。这将生成一个映射,该映射将每个类别与类别中的元素数量相关联,而不是包含元素的集合。这是你在这一项开始的频率表例子中看到的:


image.png

  groupingBy的第三个版本允许您指定除了下游收集器之外的映射工厂。注意,这个方法违反了标准的可伸缩参数列表模式:mapFactory参数位于下游参数之前,而不是之后。groupingBy的这个版本允许您控制包含的映射和包含的集合,因此,例如,您可以指定一个收集器,该收集器返回一个树形图,其值为树形集。
  groupingByConcurrent方法提供了groupingBy的所有三种重载的变体。这些变体可以有效地并行运行,并生成ConcurrentHashMap实例。还有一个与groupingBy关系不大的词,叫做partitioningBy。代替分类器方法,它接受一个谓词并返回一个键为布尔值的映射。此方法有两个重载,其中一个除了谓词外还接受下游收集器。
  计数方法返回的收集器仅用于作为下游收集器。相同的功能可以通过count方法直接在流上使用,所以从来没有理由说collect(counting())。还有15个具有此属性的收集器方法。它们包括9个方法,它们的名称以求和、平均和汇总开头(它们的功能在相应的原始流类型上可用)。它们还包括reduce方法的所有重载,以及过滤、映射、平面映射和collectingAndThen方法。大多数程序员可以安全地忽略这些方法中的大多数。从设计的角度来看,这些收集器试图部分复制收集器中的流的功能,以便下游收集器可以充当"ministreams"
  我们还没有提到三种收集器方法。虽然它们是在收集器中,但它们不涉及收集。前两个是minBy和maxBy,它们接受比较器并返回其中的最小或最大元素,由比较器确定的流。它们是流接口中最小和最大方法的一些小泛化,是BinaryOperator中同名方法返回的二进制操作符的收集器类似物。回想一下,我们使用了BinaryOperator。maxBy在我们最畅销的专辑示例中。
  最后一个收集器方法是join,它只对CharSequence实例流(如字符串)执行操作。在其无参数形式中,它返回一个收集器,该收集器只是将元素连接起来。它的一个参数形式接受一个名为delimiter的CharSequence参数,并返回一个连接流元素的收集器,在相邻元素之间插入分隔符。如果传入逗号作为分隔符,收集器将返回逗号分隔的值字符串(但是要注意,如果流中的任何元素包含逗号,该字符串将是不明确的)。比如 [came, saw, conquered].
  总之,流管道编程的本质是无副作用的函数对象。这适用于传递给流和相关对象的所有函数对象。终端操作forEach只应用于报告由流执行的计算结果,而不应用于执行计算。为了正确使用流,您必须了解收集器。最重要的收集工厂是toList、toSet、toMap、groupingBy和join。

本文写于2019.7.15,历时1天

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容