驳斥5条普通流Tropes

我刚读完“ JDK 8收集器的强大功能的一种例外” ,我不得不说我很失望。 Java冠军 Simon Ritter是Oracle的前Java推广者,现在是Oracle的Java传播者,现在是Azul Systems的副CTO(使用JVM的人 )写了它,因此我希望对流有一些有趣的见解。 相反,帖子归结为:

  • 使用流减少行数
  • 你可以和收藏家一起做花哨的东西
  • 流中的异常很烂

这不仅是肤浅的,而且文章还采用了一些不合标准的开发实践。 现在,西蒙(Simon)写道,这只是一个小型演示项目,所以我想他并没有将所有的专业知识投入其中。 尽管如此,它还是很草率的,而且,更糟的是,那里的许多人犯了同样的错误并重复了相同的比喻。

看到它们被引用在许多不同的地方(即使各自的作者在按下时可能无法捍卫这些观点),也肯定不会帮助开发人员对如何使用流获得良好的印象。 因此,我决定借此机会写一篇反驳的文章-不仅是针对这篇文章,而且是对所有重复的文章的反驳。

(总是指出我的观点是多余的(毕竟这是我的博客)并且很累人,所以我不会这样做。但是请记住这一点,因为我说的某些东西即使它们是事实也是如此。仅是我的观点。)

问题

关于发生了什么事情以及为什么发生的原因有很多解释,但最终归结为:我们有一个来自HTTP POST请求的查询字符串,并且想要将参数解析为更方便的数据结构。 例如,给定字符串a = foo&b = bar&a = fu,我们希望得到类似a〜> {foo,fu} b〜> {bar}的名称。

我们也有一些在网上找到的已经执行此操作的代码:

private void parseQuery(String query, Map parameters)throws UnsupportedEncodingException {if (query != null) {String pairs[] = query.split("[&]");for (String pair : pairs) {String param[] = pair.split("[=]");String key = null;String value = null;if (param.length > 0) {key = URLDecoder.decode(param[0],System.getProperty("file.encoding"));}if (param.length > 1) {value = URLDecoder.decode(param[1],System.getProperty("file.encoding"));}if (parameters.containsKey(key)) {Object obj = parameters.get(key);if(obj instanceof List) {List values = (List)obj;values.add(value);} else if(obj instanceof String) {List values = new ArrayList();values.add((String)obj);values.add(value);parameters.put(key, values);}} else {parameters.put(key, value);}}}
}

我认为没有提及作者的名字是一种好意,因为此代码段在很多层面上都是错误的,因此我们甚至都不会讨论。

我的牛肉

从这里开始,本文将说明如何向流重构。 这就是我开始不同意的地方。

简洁流

这就是重构的动机:

看完这个之后,我认为我可以[...]使用流来使其更加简洁。

当人们把它作为使用流的第一个动机时,我讨厌它! 认真地说,我们是Java开发人员,如果可以提高可读性,我们习惯于编写一些额外的代码。

信息流与简洁无关

因此,信息流与简洁无关。 相反,我们习惯于循环,因此我们经常将大量操作塞入循环的主体行中。 当向流重构时,我经常将操作分开,从而导致更多的行。

相反,流的神奇之处在于它们如何支持思维模式匹配。 因为他们只使用了少数几个概念(主要是map / flatMap,filter,reduce / collect / find),所以我可以快速了解正在发生的事情并集中精力进行操作,最好是一个接一个地进行。

for (Customer customer : customers) {if (customer.getAccount().isOverdrawn()) {WarningMail mail = WarningMail.createFor(customer.getAccount());// do something with mail}
}customers.stream().map(Customer::getAccount).filter(Account::isOverdrawn).map(WarningMail::createFor).forEach(/* do something with mail */ );

在代码中,遵循通用的“客户映射到帐户,过滤透支的帐户映射到警告邮件”,然后费时费力地“为从客户那里获得的帐户创建警告邮件,但前提是透支,才容易得多”。

但是,为什么这是抱怨的理由? 每个人都有自己的喜好,对吗? 是的,但是专注于简洁性会导致错误的设计决策。

例如,我经常决定通过为其创建一个方法并使用一个方法引用来总结一个或多个操作(如连续映射)。 这样可以带来不同的好处,例如将流管道中的所有操作都保持在相同的抽象级别上,或者简单地命名本来就很难理解的操作(您知道,意图显示名称和内容)。 如果我专注于简洁性,我可能不会这样做。

减少代码行数也可能导致将多个操作组合到一个lambda中,从而节省了两个映射或过滤器。 再次,这打败了背后的目的!

因此,当您看到一些代码并考虑将其重构为流时,不要数行来确定您的成功!

使用丑陋的力学

循环要做的第一件事也是启动流的方法:我们将查询字符串与&符分开,然后对结果键值对进行操作。 该文章如下

Arrays.stream(query.split("[&]"))

看起来不错? 老实说,没有。 我知道,这是创建流的最佳方式,但只是因为我们必须这样 ,这样并不意味着我们来看看它。 而且我们在这里所做的(沿着正则表达式分割字符串)似乎也很普通。 那么为什么不将其推入实用程序功能呢?

public static Stream<String> splitIntoStream(String s, String regex) {return Arrays.stream(s.split(regex));
}

然后,我们使用splitIntoStream(query,“ [&]”)启动流。 一种简单的“提取方法”重构,但效果更好。

次优数据结构

还记得我们想做什么? 将类似a = foo&b = bar&a = fu的内容解析为a〜> {foo,fu} b〜> {bar}。 现在,我们怎么可能代表结果呢? 看起来我们正在将单个字符串映射到许多字符串,所以也许我们应该尝试Map <String,List <String >>?

那绝对是个不错的初衷……但这绝不是我们能做的最好的! 首先,为什么要列出清单? 订单真的很重要吗? 我们需要重复的值吗? 我猜这两项都不对,所以也许我们应该尝试一套?

无论如何,如果您曾经创建过一个以值为集合的地图,那么您就会知道这有些不愉快。 总是存在这样的极端情况:“这是第一个要素吗?” 考虑。 尽管Java 8减轻了麻烦……

public void addPair(String key, String value) {// `map` is a `Map<String, Set<String>>`map.computeIfAbsent(key, k -> new HashSet<>()).add(value);
}

…从API的角度来看,还远远不够完美。 例如,迭代或流式传输所有值是一个两步过程:

private <T> Stream<T> streamValues() {// `map` could be a `Map<?, Collection<T>>`return map.values().stream().flatMap(Collection::stream);
}

长话短说,我们正在将需要的东西(从键到多个值的映射)变成我们想到的第一件事(从键到单个值的映射)。 那不是一个好的设计!

尤其是因为我们的需求非常匹配: Guava的Multimap 。 也许有充分的理由不使用它,但在这种情况下,至少应该提及它。 毕竟,本文的目的是找到一种处理和表示输入的好方法,因此它应该在为输出选择数据结构方面做得很好。

(虽然一般来说,这是一个反复出现的主题,但它并不是非常特定于流的。我没有将其归类为5个常见的对立部分,但仍然想提及它,因为这样可以使最终结果更好。)

康妮插图

说到常见的比喻...一种是使用溪流的老照片为帖子添加一些颜色。 有了这个,我很乐意承担!

反流向

由Dan Zen在CC-BY 2.0下发布

贫血管道

您是否曾经看到几乎什么都不做但突然将所有功能塞入单个操作的管道? 这篇文章对我们的小解析问题的解决方案是一个完美的例子(我删除了一些空处理以提高可读性):

private Map<String, List<String>> parseQuery(String query) {return Arrays.stream(query.split("[&]")).collect(groupingBy(s -> (s.split("[=]"))[0],mapping(s -> (s.split("[=]"))[1], toList())));
}

这是我在阅读本文时的思考过程:“好吧,所以我们用&符分隔查询字符串,然后,耶稣在他妈的棍子上,那是什么?!” 然后我冷静下来,意识到这里隐藏着一个抽象-通常不追求它,而让我们大胆地做到这一点。

在这种情况下,我们将请求参数a = foo拆分为[a,foo],然后分别处理这两个部分。 因此,在流中包含该对的流水线中不应该走一步吗?

但这是一种罕见的情况。 流的元素通常是某种类型,我想用其他信息来丰富它。 也许我有大量的客户,并希望将其与他们居住的城市配对。请注意,我不想用城市代替客户-这是一个简单的地图-但需要同时使用这两个功能,例如将城市映射到居住的客户在其中。

正确表示中间结果是可读性的福音。

两种情况有什么共同点? 他们需要代表一对。 他们为什么不呢? 因为Java没有惯用的方法。 当然,您可以使用数组(适用于我们的请求参数), Map.Entry ,某些库的元组类甚至特定于域的东西。 但很少有人做,这使得代码做到的是通过一个有点令人惊讶脱颖而出。

不过,我还是喜欢这种方式。 正确表示中间结果是可读性的福音。 使用Entry看起来像这样:

private Map<String, List<String>> parseQuery(String query) {return splitIntoStream(query, "[&]").map(this::parseParameter).collect(groupingBy(Entry::getKey,mapping(Entry::getValue, toList())));
}private Entry<String, String> parseParameter(String parameterString) {String[] split = parameterString.split("[=]");// add all kinds of verifications herereturn new SimpleImmutableEntry<>(split[0], split[1]);
}

我们仍然有魔术收藏家要处理,但那里发生的事情最少。

收藏魔术

Java 8附带了一些疯狂的收集器 (尤其是那些转发给下游收集器的收集器),我们已经看到如何滥用它们来创建不可读的代码。 如我所见,它们之所以存在是因为没有元组,就没有办法准备复杂的约简。 所以这是我的工作:

  • 我尝试通过正确准备流的元素来使收集器尽可能简单(如有必要,我为此使用元组或特定于域的数据类型)。
  • 如果仍然需要做一些复杂的事情,可以将其放入实用程序方法中。

吃我自己的狗粮,这怎么办?

private Map<String, List<String>> parseQuery(String query) {return splitIntoStream(query, "[&]").map(this::parseParameter).collect(toListMap(Entry::getKey, Entry::getValue));
}/** Beautiful JavaDoc comment explaining what the collector does. */
public static <T, K, V> Collector<T, ?, Map<K, List<V>>> toListMap(Function<T, K> keyMapper, Function<T, V> valueMapper) {return groupingBy(keyMapper, mapping(valueMapper, toList()));
}

它仍然很丑陋-尽管不是那么可怕-但至少我不必一直都在看它。 如果我愿意,返回类型和合同注释将使您更容易了解发生的情况。

或者,如果我们决定使用Multimap,我们会四处寻找匹配的收集器 :

private Multimap<String, String> parseQuery(String query) {return splitIntoStream(query, "[&]").map(this::parseParameter).collect(toMultimap(Entry::getKey, Entry::getValue));
}

在这两种情况下,我们甚至可以更进一步,对条目流进行特殊处理。 我将其留给您练习。 :)

异常处理

本文在处理流时面临的最大挑战是异常处理。 它说:

不幸的是,如果您回头查看原始代码,将会发现我方便地省略了一个步骤:使用URLDecoder将参数字符串转换为其原始格式。

问题是URLDecoder :: decode会引发检查的UnsupportedEncodingException,因此无法将其简单地添加到代码中。 那么本文采用哪种方法解决这一相关问题? 鸵鸟之一 :

最后,我决定保留我的第一个超薄方法。 由于在这种情况下我的Web前端未进行任何编码,因此我的代码仍然可以正常工作。

嗯...文章标题没有提到例外吗? 因此,不应该为此多花点时间吗?

无论如何,错误处理总是很困难,流增加了一些约束和复杂性。 讨论不同的方法需要花费时间,而且具有讽刺意味的是,我并不热衷于将其压缩到帖子的最后部分。 因此,让我们详细讨论如何使用运行时异常,欺骗或monad来解决该问题,而不是考虑最简单的解决方案。

一个操作要做的最简单的事情就是筛选出引起麻烦的元素。 因此,该操作不是将每个元素映射到一个新元素,而是将一个元素映射到零或一个元素。 在我们的情况下:

private static Stream<Entry<String, String>> parseParameter(String parameterString) {try {return Stream.of(parseValidParameter(parameterString));} catch (IllegalArgumentException | UnsupportedEncodingException ex) {// we should probably log the exception herereturn Stream.empty();}
}private static Entry<String, String> parseValidParameter(String parameterString)throws UnsupportedEncodingException {String[] split = parameterString.split("[=]");if (split.length != 2) {throw new IllegalArgumentException(/* explain what's going on */);}return new SimpleImmutableEntry<>(URLDecoder.decode(split[0], ENCODING),URLDecoder.decode(split[1], ENCODING));
}

然后,我们在flatMap而不是地图中使用parseParameter,并获取可以拆分和解码的条目流(以及一堆日志消息,告诉我们在什么情况下出现问题)。

摊牌

这是文章的最终版本:

private Map<String, List> parseQuery(String query) {return (query == null) ? null : Arrays.stream(query.split("[&]")).collect(groupingBy(s -> (s.split("[=]"))[0],mapping(s -> (s.split("[=]"))[1], toList())));
}

摘要说:

由此得出的结论是,使用流和收集器的灵活性,可以大大减少复杂处理所需的代码量。 缺点是,当这些令人讨厌的异常抬起头来时,这种方法就不能很好地工作了。

这是我的:

private Multimap<String, String> parseQuery(String query) {if (query == null)return ArrayListMultimap.create();return splitIntoStream(query, "[&]").flatMap(this::parseParameter).collect(toMultimap(Entry::getKey, Entry::getValue));
}// plus `parseParameter` and `parseValidParameter` as above// plus the reusable methods `splitIntoStream` and `toMultimap

行更多,是的,但是流管道具有更少的技术组合,通过URL解码参数来设置完整的功能集,可接受(或至少存在)异常处理,适当的中间结果,明智的收集器,以及良好的性能结果类型。 它带有两个通用实用程序功能,可以帮助其他开发人员改善其开发流程。 我认为多余的几行值得所有。

因此,我的收获有所不同:使用流以简单可预测的方式使用流的构建块来使代码揭示其意图。 抓住机会寻找可重用的操作(尤其是那些创建或收集流的操作),不要害羞地调用小方法以保持管道可读。 最后但并非最不重要的一点:忽略行数。

圣经后

顺便说一下,借助Java 9对流API的增强 ,我们不必对空查询字符串进行特殊情况处理:

private Multimap<String, String> parseQuery(String query) {return Stream.ofNullable(query).flatMap(q -> splitIntoStream(q, "[&]")).flatMap(this::parseParameter).collect(toMultimap(Entry::getKey, Entry::getValue));
}

等不及了!

翻译自: https://www.javacodegeeks.com/2016/09/rebutting-5-common-stream-tropes.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/352238.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

私网IP如何访问Internet

公网、内网是两种Internet的接入方式。 内网接入方式&#xff1a;上网的计算机得到的IP地址是Inetnet上的保留地址&#xff0c;保留地址有如下3种形式&#xff1a; 10.x.x.x 172.16.x.x至172.31.x.x 192.168.x.x 内网的计算机以NAT&#xff08;网络地址转换&#xf…

钉钉机器人发送图片 python_python封装钉钉Webhook机器人消息发送逻辑

python封装钉钉Webhook机器人消息发送逻辑&#xff0c;目前仅支持python2。安装pip install dingmsgapi初始化实例from ding_msg_api import MsgClient# Webhook机器人access_tokenmsgClient MsgClient("****************")发送Text消息from ding_msg_api import Te…

[LevelDB] 写批处理过程详解

leveldb的write代码初看瞎搞一堆&#xff0c;细看则实为短小精悍。1 Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) { 2  // -----A begin------- 3 Writer w(&mutex_); 4 w.batch my_batch; 5 w.sync options.sync; 6 w.d…

关于excel vba 使用CopyFromRecordset出现格式问题的解决方法

关于excel vba 使用CopyFromRecordset出现格式问题的解决方法 出现问题的写法&#xff1a; With ActiveSheet .Name k(i) For num 1 To UBound(myArray) .Cells(1, num) myArray(num, 1) Next num .rang…

python histo 改变 bins 大小_在Python中显示具有非常不均匀的bin宽度的直方图

这是直方图为了生成这个图,我做了&#xff1a;bins np.array([0.03, 0.3, 2, 100])plt.hist(m, bins bins, weightsnp.zeros_like(m) 1. / m.size)但是,正如您所注意到的,我想绘制每个数据点的相对频率的直方图,只有3个不同大小的区间&#xff1a;bin1 0.03 – > 0.3bin…

parted工具详解

通常我们用的比较多的一般都是fdisk工具来进行分区&#xff0c;但是现在由于磁盘越来越廉价&#xff0c;而且磁盘空间越来越大&#xff1b;而fdisk工具他对分区是有大小限制的&#xff0c;它只能划分小于2T的磁盘。但是现在的磁盘空间很多都已经是远远大于2T了&#xff0c;甚至…

Python安装pyinstaller模块的错误:NO module name “setuptools“

出现改pyinstaller安装错误常见问题是&#xff1a;pip版本或者setuptools包版本过低。 出现上图提示的错误后&#xff0c;升级一下setuptools包&#xff1a; &#xff08;1&#xff09;pip install --upgrade setuptools &#xff08;2&#xff09;pip install pyinstaller

jvm ide_预热JVM –超快速生产服务器和IDE

jvm ide几个月前&#xff0c;我正在阅读Java中的复杂事件处理以及实现低延迟的方法。 在我长达一个小时的研究结束时&#xff0c;我发现即使您的应用程序编写正确并且您的方法主要在0&#xff08;log n&#xff09;的时间内运行&#xff0c;并且您使用的是某些尖端的硬件解决方…

Python 项目打包成可执行程序命令

一、安装pyinstaller (1)winR输入cmd&#xff0c;打开命令窗口 2&#xff09;安装pyinstaller&#xff0c;安装指令&#xff1a;pip install pyinstaller 二、打包 1&#xff0c;切换到打包程序目录 例&#xff1a;需要打包程序目录为&#xff1a;D:\pythonfun\useringfunct…

软RAID-mdadm折腾小记

RAID --- 磁盘阵列,简言之,用来提高硬盘的利用率和速度RAID种类(理论):RAID 0 : 读写性能(最少两块硬盘) --- 硬盘使用量是所有硬盘大小之和,性能是所有硬盘之和RAID 1 : 读写性能,冗余性(最少两块硬盘) --- 空间利用率:所有磁盘中最小的那块(n/2); 读性能接近RAID0,写性能较r…

python学习笔记:第19天 类的约束、异常、MD5和logging

目录 一、类的约束二、异常处理&#xff1a;三、MD5加密四、日志&#xff08;logging模块&#xff09;一、类的约束 真正写写项目的代码时都是多人协作的&#xff0c;所以有些地方需要约束程序的结构。也就是说&#xff0c;在分配任务之前就应该把功能定义好&#xff0c;然后分…

新ANTLR 4.6的重要更改

自从上一个主要版本发布以来&#xff0c;已经过去了将近一年的时间&#xff0c;推出了新的ANTLR版本&#xff1a; 4.6 。 有很多新闻&#xff1a;新的目标&#xff0c;更好的性能&#xff0c;更好的错误处理以及ANTLR本身开发中的一些改进。 新目标 影响最大的新闻可能是新目标…

strcmp可以比较数组么_C语言数组越界了,后果很严重,如何避免?

素材来源&#xff1a;嵌入式ARM所谓的数组越界&#xff0c;简单地讲就是指数组下标变量的取值超过了初始定义时的大小&#xff0c;导致对数组元素的访问出现在数组的范围之外&#xff0c;这类错误也是 C 语言程序中最常见的错误之一。在 C 语言中&#xff0c;数组必须是静态的。…

MongoDB 问题123

MongoDB 是非关系型数据库中的一种。 出于某些原因&#xff0c;我们用了Mongo。他们说Mongo的最大特点是快。 不过这种快是以空间换时间的代价而得来的。 这个空间代价包括 1.DB至少占用64M(好像是这个数字&#xff0c;因为我们的DB实际往往只有几M,但却要占用那么多&#xff0…

两个excel文档查找相同选项后替换_看似普通的查找和替换功能,用好了,能让你的工作效率翻一番...

关注【新精英充电站】能力提升看得见&#xff01;在Word中&#xff0c;查找和替换功能是编辑文档时时常要用到的重要功能&#xff0c;它能帮助我们快速将文档或表格中查找到的内容或格式等替换为指定的内容或格式&#xff0c;特别是遇到大量需要修改的相同文字内容或格式时非常…

Mongodb内存管理和使用情况情况查询

overview MongoDB使用的是内存映射存储引擎,即Memory Mapped Storage Engine&#xff0c;简称MMAP。MMAP可以把磁盘文件的一部分或全部内容直接映射到内存&#xff0c;这样文件中的信息位置就会在内存中有对应的地址空间&#xff0c;这时对文件的读写可以直接用指针来做&#…

mapreduce文本排序_MapReduce:通过数据密集型文本处理

mapreduce文本排序自上次发布以来已经有一段时间了&#xff0c;因为我一直忙于Coursera提供的一些课程。 有一些非常有趣的产品&#xff0c;值得一看。 前一段时间&#xff0c;我购买了Jimmy Lin和Chris Dyer的MapReduce数据密集型处理程序 。 本书以伪代码格式介绍了几种关键的…

nginx根据参数转发到不同服务器_Nginx服务器之负载均衡策略

一、关于Nginx的负载均衡在服务器集群中&#xff0c;Nginx起到一个代理服务器的角色&#xff08;即反向代理&#xff09;&#xff0c;为了避免单独一个服务器压力过大&#xff0c;将来自用户的请求转发给不同的服务器。二、Nginx负载均衡策略负载均衡用于从“upstream”模块定义…

同步异步与协程

目录&#xff1a; 同步/异步 异步回调 协成 线程队列 同步|异步: 线程的三种状态:   1.就绪   2.运行   3.阻塞阻塞和非阻塞描述的是运行的状态阻塞 :遇到了IO操作,代码卡住,无法执行下一行,CPU会切换到其他任务非阻塞 :与阻塞相反,代码正在执行(运行状态) 或处于就绪状态…

Linux禁止非WHEEL用户使用SU命令

通常情况下&#xff0c;一般用户通过执行“su -”命令、输入正确的root密码&#xff0c;可以登录为root用户来对系统进行管理员级别的配置。 但是&#xff0c;为了更进一步加强系统的安全性&#xff0c;有必要建立一个管理员的 组&#xff0c;只允许这个组的用户来执行“su -”…