Java 8 Friday:使用Streams API时的10个细微错误
在Data Geekery ,我們喜歡Java。 而且,由于我們真的很喜歡jOOQ的流暢的API和查詢DSL ,我們對Java 8將為我們的生態系統帶來什么感到非常興奮。
Java 8星期五
每個星期五,我們都會向您展示一些不錯的教程風格的Java 8新功能,這些功能利用了lambda表達式,擴展方法和其他好東西。 您可以在GitHub上找到源代碼 。
使用Streams API時的10個細微錯誤
我們已經完成了所有SQL錯誤列表:
- Java開發人員在編寫SQL時常犯的10個錯誤
- Java開發人員在編寫SQL時犯的10個常見錯誤
- Java開發人員在編寫SQL時又犯了10個常見錯誤(您不會相信最后一個)
但是我們還沒有用Java 8列出十大錯誤列表! 在今天的情況下( 13日星期五 ),我們將趕上您使用Java 8時應用程序中出現的問題(這不會發生在我們身上,因為我們將Java 6留在了另一個Java 6上)而)。
1.意外重用流
想打賭,這至少每個人都會發生一次。 像現有的“流”(例如InputStream )一樣,您只能使用一次流。 以下代碼不起作用:
IntStream stream = IntStream.of(1, 2); stream.forEach(System.out::println);// That was fun! Let's do it again! stream.forEach(System.out::println);您會得到:
java.lang.IllegalStateException: stream has already been operated upon or closed因此,在使用流時要小心。 只能執行一次。
2.意外創建“無限”流
您可以很容易地創建無限流而無需注意。 請看以下示例:
// Will run indefinitely IntStream.iterate(0, i -> i + 1).forEach(System.out::println);如果您將流設計為無限的,那么流的全部要點就是事實。 唯一的問題是,您可能不需要這樣做。 因此,請確保始終設置適當的限制:
// That's better IntStream.iterate(0, i -> i + 1).limit(10).forEach(System.out::println);3.意外地創建“微妙”的無限流
我們不能這么說。 您最終將意外地創建無限流。 以以下流為例:
IntStream.iterate(0, i -> ( i + 1 ) % 2).distinct().limit(10).forEach(System.out::println);所以…
- 我們生成交替的0和1
- 那么我們只保留不同的值,即單個0和單個1
- 那么我們將流的大小限制為10
- 然后我們消耗它
好吧…… distinct()操作不知道提供給iterate()方法的函數只會產生兩個不同的值。 它可能會期望更多。 因此它將永遠消耗流中的新值,并且永遠不會達到limit(10) 。 不幸的是,您的應用程序停頓了。
4.意外地創建“微妙”的并行無限流
我們確實需要堅持,您可能會意外地嘗試消耗無限的流。 讓我們假設您認為 distinct()操作應該并行執行。 您可能正在寫:
IntStream.iterate(0, i -> ( i + 1 ) % 2).parallel().distinct().limit(10).forEach(System.out::println);現在,我們已經看到,這種情況將永遠發生。 但至少在以前,您僅消耗計算機上的一個CPU。 現在,您可能會消耗其中的四個,可能會意外地無限消耗流,從而幾乎占據整個系統。 真不好 之后,您可能可以硬重啟服務器/開發計算機。 在爆炸之前,最后查看一下我的筆記本電腦的外觀:
如果我是筆記本電腦,這就是我想要的方式。
5.混合操作順序
那么,為什么我們堅持要您絕對無意中創建無限流? 這很簡單。 因為您可能只是偶然地這樣做。 如果您切換limit()和distinct()的順序,則可以完美地使用上述流:
IntStream.iterate(0, i -> ( i + 1 ) % 2).limit(10).distinct().forEach(System.out::println);現在產生:
0 1為什么? 因為我們首先將無限流限制為10個值(0 1 0 1 0 1 0 1 0 1),然后再將有限流減少為無限值包含在其中(0 1)。
當然,這在語義上可能不再正確,因為您確實希望從一組數據中獲得前10個不同的值(您剛好“忘記”了數據是無限的)。 沒有人真正想要10個隨機值,然后才將它們減小到與眾不同。
如果您來自SQL背景,那么您可能不會想到這樣的差異。 以SQL Server 2012為例。 以下兩個SQL語句相同:
-- Using TOP SELECT DISTINCT TOP 10 * FROM i ORDER BY ..-- Using FETCH SELECT * FROM i ORDER BY .. OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY因此,作為SQL專家,您可能沒有意識到流操作順序的重要性。
6.再次混合操作順序
說到SQL,如果您是MySQL或PostgreSQL的人,則可能會習慣LIMIT .. OFFSET子句。 SQL充滿了微妙的怪癖,這就是其中之一。 該OFFSET子句應用首先 ,在SQL Server 2012中的(即建議的SQL:2008標準的)語法。
如果將MySQL / PostgreSQL的方言直接轉換為流,則可能會出錯:
IntStream.iterate(0, i -> i + 1).limit(10) // LIMIT.skip(5) // OFFSET.forEach(System.out::println);以上收益
5 6 7 8 9是。 它不會在9之后繼續,因為現在先應用limit() ,生成(0 1 2 3 4 5 6 7 8 9)。 之后應用skip() ,將流減少到(5 6 7 8 9)。 不是您可能想要的。
注意LIMIT .. OFFSET與"OFFSET .. LIMIT"陷阱!
7.使用過濾器遍歷文件系統
以前我們已經在博客上寫過 。 似乎一個好主意是使用過濾器遍歷文件系統:
Files.walk(Paths.get(".")).filter(p -> !p.toFile().getName().startsWith(".")).forEach(System.out::println);上面的流似乎僅在非隱藏目錄(即不以點開頭的目錄)中移動。 不幸的是,您又犯了#5和#6錯誤。 walk()已經產生了當前目錄的整個子目錄流。 雖然懶惰,但邏輯上包含所有子路徑。 現在,過濾器將正確過濾出名稱以點“。”開頭的路徑。 例如.git或.idea將不屬于結果流。 但是這些路徑將是: .\.git\refs或.\.idea\libraries 。 不是你想要的。
現在,不要通過編寫以下內容解決此問題:
Files.walk(Paths.get(".")).filter(p -> !p.toString().contains(File.separator + ".")).forEach(System.out::println);盡管這將產生正確的輸出,但仍將通過遍歷完整的目錄子樹,然后遞歸到“隱藏”目錄的所有子目錄中來實現。
我猜您將不得不再次使用舊的JDK 1.0 File.list() 。 好消息是, FilenameFilter和FileFilter都是功能接口。
8.修改流的后備集合
在迭代List ,一定不能在迭代主體中修改相同的列表。 在Java 8之前確實如此,但是對于Java 8流,它可能變得更加棘手。 考慮以下來自0..9的列表:
// Of course, we create this list using streams: List<Integer> list = IntStream.range(0, 10).boxed().collect(toCollection(ArrayList::new));現在,假設我們要在使用每個元素時將其刪除:
list.stream()// remove(Object), not remove(int)!.peek(list::remove).forEach(System.out::println);有趣的是,這將適用于某些元素! 您可能獲得的輸出是以下內容:
0 2 4 6 8 null null null null null java.util.ConcurrentModificationException如果我們在捕獲到異常之后對列表進行內省,那么就會發現一個有趣的發現。 我們會得到:
[1, 3, 5, 7, 9]嘿,它對所有奇數都有效。 這是錯誤嗎? 不,它看起來像個功能。 如果您正在研究JDK代碼,則可以在ArrayList.ArraListSpliterator找到以下注釋:
/** If ArrayLists were immutable, or structurally immutable (no* adds, removes, etc), we could implement their spliterators* with Arrays.spliterator. Instead we detect as much* interference during traversal as practical without* sacrificing much performance. We rely primarily on* modCounts. These are not guaranteed to detect concurrency* violations, and are sometimes overly conservative about* within-thread interference, but detect enough problems to* be worthwhile in practice. To carry this out, we (1) lazily* initialize fence and expectedModCount until the latest* point that we need to commit to the state we are checking* against; thus improving precision. (This doesn't apply to* SubLists, that create spliterators with current non-lazy* values). (2) We perform only a single* ConcurrentModificationException check at the end of forEach* (the most performance-sensitive method). When using forEach* (as opposed to iterators), we can normally only detect* interference after actions, not before. Further* CME-triggering checks apply to all other possible* violations of assumptions for example null or too-small* elementData array given its size(), that could only have* occurred due to interference. This allows the inner loop* of forEach to run without any further checks, and* simplifies lambda-resolution. While this does entail a* number of checks, note that in the common case of* list.stream().forEach(a), no checks or other computation* occur anywhere other than inside forEach itself. The other* less-often-used methods cannot take advantage of most of* these streamlinings.*/現在,檢查當我們告訴流產生sorted()結果時會發生什么:
list.stream().sorted().peek(list::remove).forEach(System.out::println);現在將產生以下“預期”輸出
0 1 2 3 4 5 6 7 8 9和流消費后的清單? 它是空的:
[]因此,所有元素都被消耗并正確刪除。 sorted()操作是“有狀態中間操作” ,這意味著后續操作不再對后備集合進行操作,而是對內部狀態進行操作。 現在從列表中刪除元素是“安全的”!
好吧,我們真的可以嗎? 讓我們繼續進行parallel() , sorted()移除:
list.stream().sorted().parallel().peek(list::remove).forEach(System.out::println);現在產生:
7 6 2 5 8 4 1 0 9 3并且列表包含
[8]真是的 我們沒有刪除所有元素! 解決此流難題的任何人都可以免費獲得啤酒( 和jOOQ貼紙 )!
所有這些看起來都是非常隨機和微妙的,我們只能建議您在使用流時不要真正修改后備集合。 就是行不通。
9.忘記實際消耗流
您如何看待以下信息流?
IntStream.range(1, 5).peek(System.out::println).peek(i -> { if (i == 5) throw new RuntimeException("bang");});閱讀此內容時,您可能會認為它將打印(1 2 3 4 5),然后引發異常。 但這是不正確的。 它什么也不會做。 流只是坐在那里,從來沒有被消耗過。
與任何流暢的API或DSL一樣,您實際上可能會忘記調用“終端”操作。 當您使用peek()時尤其如此,因為peek()與forEach()非常相似。
當您忘記調用execute()或fetch()時, jOOQ可能會發生同樣的情況:
DSL.using(configuration).update(TABLE).set(TABLE.COL1, 1).set(TABLE.COL2, "abc").where(TABLE.ID.eq(3));哎呀。 沒有execute()
是的,“最佳”方法-1-2次警告!
10.并行流死鎖
現在這真是個好東西!
如果您未正確同步所有事物,則所有并發系統都可能陷入死鎖。 雖然找不到真實的例子很明顯,但找到強制的例子很明顯。 保證以下parallel()流會陷入死鎖:
Object[] locks = { new Object(), new Object() };IntStream.range(1, 5).parallel().peek(Unchecked.intConsumer(i -> {synchronized (locks[i % locks.length]) {Thread.sleep(100);synchronized (locks[(i + 1) % locks.length]) {Thread.sleep(50);}}})).forEach(System.out::println);請注意Unchecked.intConsumer()的使用,該函數將IntConsumer函數的接口轉換為org.jooq.lambda.fi.util.function.CheckedIntConsumer ,允許拋出已檢查的異常。
好。 您的機器運氣不好。 這些線程將永遠被阻塞!
好消息是,用Java編寫死鎖的教科書示例從未如此簡單!
有關更多詳細信息,另請參見Brian Goetz對Stack Overflow的此問題的回答 。
結論
借助流和功能性思維,我們將遇到大量新的,細微的錯誤。 除了實踐和保持專注之外,幾乎無法避免這些錯誤。 您必須考慮如何訂購您的手術。 您必須考慮流是否可能是無限的。
流(和lambda)是一個非常強大的工具。 但是首先我們需要掌握一種工具。
翻譯自: https://www.javacodegeeks.com/2014/06/java-8-friday-10-subtle-mistakes-when-using-the-streams-api.html
總結
以上是生活随笔為你收集整理的Java 8 Friday:使用Streams API时的10个细微错误的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何购买一台性价比较高的电脑如何买好电脑
- 下一篇: 张钹: 硅基机器是否能产生意识?目前只有