驳斥5条普通流Tropes
我剛讀完“ JDK 8收集器的強(qiáng)大功能的一種例外” ,我不得不說我很失望。 Java冠軍 Simon Ritter是Oracle的前Java推廣者,現(xiàn)在是Oracle的Java傳播者,現(xiàn)在是Azul Systems的副CTO(使用JVM的人 )寫了它,因此我希望對流有一些有趣的見解。 相反,帖子歸結(jié)為:
- 使用流減少行數(shù)
- 你可以和收藏家一起做花哨的東西
- 流中的異常很爛
這不僅是膚淺的,而且文章還采用了一些不合標(biāo)準(zhǔn)的開發(fā)實(shí)踐。 現(xiàn)在,西蒙(Simon)寫道,這只是一個小型演示項(xiàng)目,所以我想他并沒有將所有的專業(yè)知識投入其中。 盡管如此,它還是很草率的,而且,更糟的是,那里的許多人犯了同樣的錯誤并重復(fù)了相同的比喻。
看到它們被引用在許多不同的地方(即使各自的作者在按下時(shí)可能無法捍衛(wèi)這些觀點(diǎn)),也肯定不會幫助開發(fā)人員對如何使用流獲得良好的印象。 因此,我決定借此機(jī)會寫一篇反駁的文章-不僅是針對這篇文章,而且是對所有重復(fù)的文章的反駁。
(總是指出我的觀點(diǎn)是多余的(畢竟這是我的博客)并且很累人,所以我不會這樣做。但是請記住這一點(diǎn),因?yàn)槲艺f的某些東西即使它們是事實(shí)也是如此。僅是我的觀點(diǎn)。)
問題
關(guān)于發(fā)生了什么事情以及為什么發(fā)生的原因有很多解釋,但最終歸結(jié)為:我們有一個來自HTTP POST請求的查詢字符串,并且想要將參數(shù)解析為更方便的數(shù)據(jù)結(jié)構(gòu)。 例如,給定字符串a(chǎn) = foo&b = bar&a = fu,我們希望得到類似a?> {foo,fu} b?> {bar}的名稱。
我們也有一些在網(wǎng)上找到的已經(jīng)執(zhí)行此操作的代碼:
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);}}} }我認(rèn)為沒有提及作者的名字是一種好意,因?yàn)榇舜a段在很多層面上都是錯誤的,因此我們甚至都不會討論。
我的牛肉
從這里開始,本文將說明如何向流重構(gòu)。 這就是我開始不同意的地方。
簡潔流
這就是重構(gòu)的動機(jī):
看完這個之后,我認(rèn)為我可以[...]使用流來使其更加簡潔。
當(dāng)人們把它作為使用流的第一個動機(jī)時(shí),我討厭它! 認(rèn)真地說,我們是Java開發(fā)人員,如果可以提高可讀性,我們習(xí)慣于編寫一些額外的代碼。
信息流與簡潔無關(guān)
因此,信息流與簡潔無關(guān)。 相反,我們習(xí)慣于循環(huán),因此我們經(jīng)常將大量操作塞入循環(huán)的主體行中。 當(dāng)向流重構(gòu)時(shí),我經(jīng)常將操作分開,從而導(dǎo)致更多的行。
相反,流的神奇之處在于它們?nèi)绾沃С炙季S模式匹配。 因?yàn)樗麄冎皇褂昧松贁?shù)幾個概念(主要是map / flatMap,filter,reduce / collect / find),所以我可以快速了解正在發(fā)生的事情并集中精力進(jìn)行操作,最好是一個接一個地進(jìn)行。
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 */ );在代碼中,遵循通用的“客戶映射到帳戶,過濾透支的帳戶映射到警告郵件”,然后費(fèi)時(shí)費(fèi)力地“為從客戶那里獲得的帳戶創(chuàng)建警告郵件,但前提是透支,才容易得多”。
但是,為什么這是抱怨的理由? 每個人都有自己的喜好,對嗎? 是的,但是專注于簡潔性會導(dǎo)致錯誤的設(shè)計(jì)決策。
例如,我經(jīng)常決定通過為其創(chuàng)建一個方法并使用一個方法引用來總結(jié)一個或多個操作(如連續(xù)映射)。 這樣可以帶來不同的好處,例如將流管道中的所有操作都保持在相同的抽象級別上,或者簡單地命名本來就很難理解的操作(您知道,意圖顯示名稱和內(nèi)容)。 如果我專注于簡潔性,我可能不會這樣做。
減少代碼行數(shù)也可能導(dǎo)致將多個操作組合到一個lambda中,從而節(jié)省了兩個映射或過濾器。 再次,這打敗了背后的目的!
因此,當(dāng)您看到一些代碼并考慮將其重構(gòu)為流時(shí),不要數(shù)行來確定您的成功!
使用丑陋的力學(xué)
循環(huán)要做的第一件事也是啟動流的方法:我們將查詢字符串與&符分開,然后對結(jié)果鍵值對進(jìn)行操作。 該文章如下
Arrays.stream(query.split("[&]"))看起來不錯? 老實(shí)說,沒有。 我知道,這是創(chuàng)建流的最佳方式,但只是因?yàn)槲覀儽仨氝@樣做 ,這樣并不意味著我們來看看它。 而且我們在這里所做的(沿著正則表達(dá)式分割字符串)似乎也很普通。 那么為什么不將其推入實(shí)用程序功能呢?
public static Stream<String> splitIntoStream(String s, String regex) {return Arrays.stream(s.split(regex)); }然后,我們使用splitIntoStream(query,“ [&]”)啟動流。 一種簡單的“提取方法”重構(gòu),但效果更好。
次優(yōu)數(shù)據(jù)結(jié)構(gòu)
還記得我們想做什么? 將類似a = foo&b = bar&a = fu的內(nèi)容解析為a?> {foo,fu} b?> {bar}。 現(xiàn)在,我們怎么可能代表結(jié)果呢? 看起來我們正在將單個字符串映射到許多字符串,所以也許我們應(yīng)該嘗試Map <String,List <String >>?
那絕對是個不錯的初衷……但這絕不是我們能做的最好的! 首先,為什么要列出清單? 訂單真的很重要嗎? 我們需要重復(fù)的值嗎? 我猜這兩項(xiàng)都不對,所以也許我們應(yīng)該嘗試一套?
無論如何,如果您曾經(jīng)創(chuàng)建過一個以值為集合的地圖,那么您就會知道這有些不愉快。 總是存在這樣的極端情況:“這是第一個要素嗎?” 考慮。 盡管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的角度來看,還遠(yuǎn)遠(yuǎn)不夠完美。 例如,迭代或流式傳輸所有值是一個兩步過程:
private <T> Stream<T> streamValues() {// `map` could be a `Map<?, Collection<T>>`return map.values().stream().flatMap(Collection::stream); }!
長話短說,我們正在將需要的東西(從鍵到多個值的映射)變成我們想到的第一件事(從鍵到單個值的映射)。 那不是一個好的設(shè)計(jì)!
尤其是因?yàn)槲覀兊男枨蠓浅Fヅ?#xff1a; Guava的Multimap 。 也許有充分的理由不使用它,但在這種情況下,至少應(yīng)該提及它。 畢竟,本文的目的是找到一種處理和表示輸入的好方法,因此它應(yīng)該在為輸出選擇數(shù)據(jù)結(jié)構(gòu)方面做得很好。
(雖然一般來說,這是一個反復(fù)出現(xiàn)的主題,但它并不是非常特定于流的。我沒有將其歸類為5個常見的對立部分,但仍然想提及它,因?yàn)檫@樣可以使最終結(jié)果更好。)
康妮插圖
說到常見的比喻...一種是使用溪流的老照片為帖子添加一些顏色。 有了這個,我很樂意承擔(dān)!
由Dan Zen在CC-BY 2.0下發(fā)布
貧血管道
您是否曾經(jīng)看到幾乎什么都不做但突然將所有功能塞入單個操作的管道? 這篇文章對我們的小解析問題的解決方案是一個完美的例子(我刪除了一些空處理以提高可讀性):
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()))); }這是我在閱讀本文時(shí)的思考過程:“好吧,所以我們用&符分隔查詢字符串,然后,耶穌在他媽的棍子上,那是什么?!” 然后我冷靜下來,意識到這里隱藏著一個抽象-通常不追求它,而讓我們大膽地做到這一點(diǎn)。
在這種情況下,我們將請求參數(shù)a = foo拆分為[a,foo],然后分別處理這兩個部分。 因此,在流中包含該對的流水線中不應(yīng)該走一步嗎?
但這是一種罕見的情況。 流的元素通常是某種類型,我想用其他信息來豐富它。 也許我有大量的客戶,并希望將其與他們居住的城市配對。請注意,我不想用城市代替客戶-這是一個簡單的地圖-但需要同時(shí)使用這兩個功能,例如將城市映射到居住的客戶在其中。
正確表示中間結(jié)果是可讀性的福音。
兩種情況有什么共同點(diǎn)? 他們需要代表一對。 他們?yōu)槭裁床荒?#xff1f; 因?yàn)镴ava沒有慣用的方法。 當(dāng)然,您可以使用數(shù)組(適用于我們的請求參數(shù)), Map.Entry ,某些庫的元組類甚至特定于域的東西。 但很少有人做,這使得代碼做到的是通過一個有點(diǎn)令人驚訝脫穎而出。
不過,我還是喜歡這種方式。 正確表示中間結(jié)果是可讀性的福音。 使用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]); }我們?nèi)匀挥心g(shù)收藏家要處理,但那里發(fā)生的事情最少。
收藏魔術(shù)
Java 8附帶了一些瘋狂的收集器 (尤其是那些轉(zhuǎn)發(fā)給下游收集器的收集器),我們已經(jīng)看到如何濫用它們來創(chuàng)建不可讀的代碼。 如我所見,它們之所以存在是因?yàn)闆]有元組,就沒有辦法準(zhǔn)備復(fù)雜的約簡。 所以這是我的工作:
- 我嘗試通過正確準(zhǔn)備流的元素來使收集器盡可能簡單(如有必要,我為此使用元組或特定于域的數(shù)據(jù)類型)。
- 如果仍然需要做一些復(fù)雜的事情,可以將其放入實(shí)用程序方法中。
吃我自己的狗糧,這怎么辦?
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())); }它仍然很丑陋-盡管不是那么可怕-但至少我不必一直都在看它。 如果我愿意,返回類型和合同注釋將使您更容易了解發(fā)生的情況。
或者,如果我們決定使用Multimap,我們會四處尋找匹配的收集器 :
private Multimap<String, String> parseQuery(String query) {return splitIntoStream(query, "[&]").map(this::parseParameter).collect(toMultimap(Entry::getKey, Entry::getValue)); }在這兩種情況下,我們甚至可以更進(jìn)一步,對條目流進(jìn)行特殊處理。 我將其留給您練習(xí)。 :)
異常處理
本文在處理流時(shí)面臨的最大挑戰(zhàn)是異常處理。 它說:
不幸的是,如果您回頭查看原始代碼,將會發(fā)現(xiàn)我方便地省略了一個步驟:使用URLDecoder將參數(shù)字符串轉(zhuǎn)換為其原始格式。
問題是URLDecoder :: decode會引發(fā)檢查的UnsupportedEncodingException,因此無法將其簡單地添加到代碼中。 那么本文采用哪種方法解決這一相關(guān)問題? 鴕鳥之一 :
最后,我決定保留我的第一個超薄方法。 由于在這種情況下我的Web前端未進(jìn)行任何編碼,因此我的代碼仍然可以正常工作。
嗯...文章標(biāo)題沒有提到例外嗎? 因此,不應(yīng)該為此多花點(diǎn)時(shí)間嗎?
無論如何,錯誤處理總是很困難,流增加了一些約束和復(fù)雜性。 討論不同的方法需要花費(fèi)時(shí)間,而且具有諷刺意味的是,我并不熱衷于將其壓縮到帖子的最后部分。 因此,讓我們詳細(xì)討論如何使用運(yùn)行時(shí)異常,欺騙或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,并獲取可以拆分和解碼的條目流(以及一堆日志消息,告訴我們在什么情況下出現(xiàn)問題)。
攤牌
這是文章的最終版本:
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()))); }摘要說:
由此得出的結(jié)論是,使用流和收集器的靈活性,可以大大減少復(fù)雜處理所需的代碼量。 缺點(diǎn)是,當(dāng)這些令人討厭的異常抬起頭來時(shí),這種方法就不能很好地工作了。
這是我的:
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行更多,是的,但是流管道具有更少的技術(shù)組合,通過URL解碼參數(shù)來設(shè)置完整的功能集,可接受(或至少存在)異常處理,適當(dāng)?shù)闹虚g結(jié)果,明智的收集器,以及良好的性能結(jié)果類型。 它帶有兩個通用實(shí)用程序功能,可以幫助其他開發(fā)人員改善其開發(fā)流程。 我認(rèn)為多余的幾行值得所有。
因此,我的收獲有所不同:使用流以簡單可預(yù)測的方式使用流的構(gòu)建塊來使代碼揭示其意圖。 抓住機(jī)會尋找可重用的操作(尤其是那些創(chuàng)建或收集流的操作),不要害羞地調(diào)用小方法以保持管道可讀。 最后但并非最不重要的一點(diǎn):忽略行數(shù)。
圣經(jīng)后
順便說一下,借助Java 9對流API的增強(qiáng) ,我們不必對空查詢字符串進(jìn)行特殊情況處理:
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
總結(jié)
以上是生活随笔為你收集整理的驳斥5条普通流Tropes的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 房子有没有备案怎么查询(房子有没有备案怎
- 下一篇: jvm ide_预热JVM –超快速生产