驳斥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
總結
以上是生活随笔為你收集整理的驳斥5条普通流Tropes的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 房子有没有备案怎么查询(房子有没有备案怎
- 下一篇: jvm ide_预热JVM –超快速生产