性能篇:解密Stream,提升集合遍歷效率的秘訣!
大家好,我是小米,一個熱愛技術分享的小伙伴。今天我們來聊一聊 Java 中的 Stream,以及如何通過 Stream 來提高遍歷集合的效率。
什么是Stream?
在開始深入討論之前,我們先來了解一下什么是 Stream。
Stream 是 Java 8 中引入的一種新的抽象概念,用于處理數據序列。它為我們提供了一種更加便捷、高效的方式來操作集合數據,實現了函數式編程的特性。在之前的 Java 版本中,我們通常使用迭代器或者循環來處理集合,代碼顯得冗長且難以閱讀。而引入 Stream 后,我們可以采用聲明式的方式描述數據的處理流程,使代碼更加簡潔、清晰。
Stream 的本質是一種數據流,它不是一種數據結構,因此不會改變原有的數據集合。相反,它提供了一系列的中間操作和終端操作,這些操作可以被串聯起來形成一條處理流水線。中間操作用于對數據進行轉換和處理,而終端操作則觸發整個處理流程的執行,產生最終的結果。
使用 Stream,我們可以輕松進行各種操作,如篩選、映射、過濾、排序等,而無需手動編寫繁瑣的迭代代碼。這種聲明式的編程風格不僅提高了代碼的可讀性,還有助于并行處理,充分發揮多核 CPU 的性能優勢。
以下是一個簡單的代碼示例,演示了使用Stream對集合進行過濾、映射和打印操作的好處:
圖片
這個簡單的示例展示了Stream的優勢,實際應用中,Stream還可以進行更復雜的操作,如分組、排序等,為集合處理提供了更多靈活性。
Stream操作分類
在使用 Stream 進行集合操作時,我們通常將其分為兩種操作:中間操作和終端操作。
中間操作是在數據源上進行的轉換和處理,但并不立即觸發流的遍歷。這些操作包括 filter、map、distinct 等。通過 filter 我們可以輕松篩選出符合條件的元素,而 map 則用于轉換元素,使得處理過程更為靈活。
圖片
在上述示例中,filter 用于選擇偶數,map 則將這些偶數平方,形成了中間操作的鏈式調用。
終端操作是觸發流的遍歷并產生最終結果的操作,結束流的處理。這些操作包括 forEach、collect、reduce 等。通過 collect 我們可以將流中的元素收集到一個新的集合中。
圖片
在這個示例中,collect 將處理后的結果收集到一個新的列表中,結束了整個流的處理過程。
Stream源碼實現
Stream 的源碼實現是 Java 8 中引入的一項復雜而精妙的特性,它為處理集合數據提供了一種全新的方式。在深入探討 Stream 的源碼實現之前,我們首先需要了解幾個關鍵的類和接口,它們構成了 Stream 操作的基礎結構。
首先,BaseStream 接口是 Stream API 中的基礎,它定義了一些基本的操作,例如串行執行和并行執行。這個接口為不同類型的 Stream,如 Stream、IntStream、DoubleStream 等提供了一致的接口定義,使得操作在不同類型的流之間能夠得到復用。
接著,AbstractPipeline 類是 Stream 的核心類之一,它封裝了操作的基本邏輯,包括遍歷、過濾等。這個類為具體的操作提供了抽象基類,簡化了新操作的添加。它還定義了流水線的基本結構,使得我們能夠串聯多個操作形成一個完整的處理流程。
在針對對象引用流的處理中,ReferencePipeline 繼承自 AbstractPipeline,通過一系列方法(如 filter、map 等)生成不同類型的中間操作,形成操作鏈。而 Sink 類則負責接收元素并進行實際的處理。這種流水線的設計充分體現了函數式編程的思想,每個操作都是不可變的,而且在進行終端操作前,中間操作只是構建了一個操作鏈而并未實際執行。
在具體的操作實現中,以 filter 為例,通過 ReferencePipeline 類的 filter 方法生成一個新的流水線,其中定義了過濾的邏輯,形成了一個中間操作。這個設計使得我們能夠以鏈式的方式組織多個操作,從而更加靈活地構建數據處理流程。
Stream操作疊加源碼解析
在實際應用中,我們常常需要對集合進行多個操作,這時候就涉及到 Stream 操作的疊加。通過源碼解析,我們可以深入了解這一過程的執行。
首先,讓我們看一下一個簡單的例子:
圖片
這個例子中,我們對數字集合進行了篩選(filter)和映射(mapToInt)的兩個操作,然后求和。讓我們逐步分析這個過程。
filter操作
首先,filter 操作創建了一個新的 Stream,其中包含了符合條件的元素。這是通過 ReferencePipeline 類的 filter 方法實現的,具體代碼如下:
圖片
這段代碼展示了如何創建一個新的 Stream,其中的 Sink 對象通過 predicate.test(u) 來判斷是否滿足條件,然后將符合條件的元素傳遞給下游。
mapToInt操作
接著,mapToInt 操作對上一個操作的結果進行了映射,將元素乘以2。這是通過 ReferencePipeline 類的 mapToInt 方法實現的,具體代碼如下:
圖片
這段代碼展示了如何創建一個新的 IntStream,其中的 Sink 對象通過 mapper.applyAsInt(u) 來進行映射操作,將元素乘以2后傳遞給下游。
sum操作
最后,sum 操作對上一個操作的結果進行了求和。這是通過 SummingInt 類的 evaluate 方法實現的,具體代碼如下:
圖片
這段代碼展示了如何對映射后的元素進行求和操作,最終得到結果。
通過這個簡單的例子,我們可以看到 Stream 操作的疊加是通過創建新的 Stream,并在每個操作的 Sink 中對元素進行處理和傳遞的。這種鏈式調用的方式使得我們可以靈活組合多個操作,構建出復雜的數據處理流程。
Stream并行處理源碼解析
Stream 的一個顯著特點是能夠支持并行處理。在多核 CPU 的環境下,Stream 的并行迭代方式可以顯著提高性能。通過分析源碼,我們可以了解并行處理是如何實現的,以及在何種場景下使用更為合適。
首先,讓我們看一個簡單的例子:
圖片
在這個例子中,我們使用了 parallelStream() 方法將 Stream 轉換為并行流,然后進行映射和求和操作。接下來,我們將逐步分析這個過程。
parallelStream操作
首先,parallelStream() 方法是通過 BaseStream 接口的 parallel() 方法實現的,具體代碼如下:
圖片
這段代碼通過 StreamSupport.stream(spliterator(), true) 來創建一個支持并行的 Stream。
并行處理的實現
在并行處理過程中,Stream 會被分割成多個子任務,每個子任務在一個獨立的線程中執行。這是通過 ForkJoinTask 框架實現的,具體代碼如下:
圖片
invoke() 方法用于執行任務,每個子任務都是一個 ForkJoinTask,它們會在多個線程中同時執行,最后將結果合并起來。
并行處理的Sink
在并行處理中,每個子任務都有自己的 Sink 對象,用于處理元素。這是通過 ForkingSink 類實現的,具體代碼如下:
圖片
ForkingSink 中的 accept() 方法用于接收元素,然后通過 split() 方法將任務進行分割。
通過這個簡單的例子,我們可以看到 Stream 的并行處理是通過 ForkJoin 框架實現的,每個子任務都在獨立的線程中執行,最后將結果合并。這種方式能夠更好地利用多核 CPU 的性能,提高處理速度。
性能測試
為了更直觀地比較兩者的性能,我們使用JMH(Java Microbenchmarking Harness)進行測試。
以下是一個簡單的示例代碼,假設我們有一個包含一系列數字的列表,我們將對這些數字進行過濾,然后按照奇偶性進行分組:
圖片
測試結論:
圖片
通過以上測試結果,我們可以看到:
- 在循環迭代次數較少的情況下,常規的迭代方式性能反而更好;
- 在單核 CPU 服務器配置環境中,也是常規迭代方式更有優勢;
- 而在大數據循環迭代中,如果服務器是多核 CPU 的情況下,Stream 的并行迭代優勢明顯。
所以我們在平時處理大數據的集合時,應該盡量考慮將應用部署在多核 CPU 環境下,并且使用 Stream 的并行迭代方式進行處理。
總結
用事實說話,我們看到其實使用 Stream 未必可以使系統性能更佳,還是要結合應用場景進行選擇,也就是合理地使用 Stream。
總的來說,Stream 是一個強大而靈活的工具,但并不是適用于所有場景。在選擇使用 Stream 時,我們需要根據實際情況進行權衡和取舍。
通過深入了解 Stream 的底層實現,我們可以更好地運用這一特性,提高代碼的可讀性和性能。