目錄
前言
Stream流 是什么?
為什么要用Steam流
常見stream流使用案例
映射 map()? & 集合 collect()
單字段映射
多字段映射
映射為其他的對象
映射為 Map
去重 distinct()
過濾 filter()
Stream流的其他方法
使用Stream流的弊端
前言
當你某天看到舍友的代碼不再寫for循環時,你的反應:
你還在 new Collection<>() ,寫著for循環的時候,舍友已經開始偷偷卷你,更改代碼風格了
本文將帶著大家簡單理解 Stream 流,并通過部分案例描述 Stream 流?的實用方法
Stream流 是什么?
Stream 流是 Java 8 引入的一個強大工具,它提供了一種全新的方式來處理集合和數組等數據源,使得數據處理變得更加簡單、高效和易于理解。
通俗的理解起來就是提供了一種更加便利的遍歷處理方式。
如果你要問我 Stream流用起來什么感覺?
那我只能說,,這種感覺就像飛翔在~~
噢不對,,感覺就是:
為什么要用Steam流
?Stream 流的主要用途是提供一種高效且表達力高的方式來處理集合和數組等數據源。通過使用 Stream 流,可以避免顯式的迭代器和循環,使得代碼更加簡潔、易讀。Stream 流支持復雜的查詢/過濾、映射/轉換、歸約/匯總等操作,能夠極大地簡化數據處理的復雜度。
總結起來還是:簡潔、易讀
當然,這也讓你的代碼看起來更高級那么一點點~~
如下案例,拿到所有的評論的id 集合的兩種方法。
- 第一種-for循環便利獲取
List<Comment> list = commentMapper.selectList(wrapper);List<Integer> commentId = new ArrayList<>();for(Comment c : list){commentId.add(c.getId());}
- 第二種-Stream流獲取
List<Comment> list = commentMapper.selectList(wrapper);List<Integer> commentId = list.stream().map(Comment::getId).collect(Collectors.toList());
兩種方法的區別顯而易見
下面介紹stream流比較實用的方法
常見stream流使用案例
在這里我們準備一個簡單的對象來進行案例測試,只約定兩個字段。
@Data
@AllArgsConstructor
public class StreamTestObject {Integer id1;Integer id2;}
映射 map()? & 集合 collect()
map() 方法是最常用的方法之一,它可以將流中的每個元素轉換成另一種形式,返回轉換后的 Stream。
如前文的例子所示,
單字段映射
List<StreamTestObject> streamTestObjects = Arrays.asList(new StreamTestObject(1, 2),new StreamTestObject(3, 4),new StreamTestObject(5, 6));// 便于觀察變化Stream<StreamTestObject> stream = streamTestObjects.stream();Stream<Integer> id1Stream = streamTestObjects.stream().map(StreamTestObject::getId1);
看代碼我們可以看到,map方法將對象的stream流映射為了其中? id1 這個字段的stream流
拿到這個字段的流后,可以做些什么呢?
最常用的方法之一就是與 集合 collect() 搭配起來使用。
那么 collect() 方法能做寫什么呢?
- 用途:將流中的元素累積成一個匯總結果,我們可以按照自己的需求將結果匯總為一個 List、Set、Map 等
如下代碼所示
List<StreamTestObject> streamTestObjects = Arrays.asList(new StreamTestObject(1, 2),new StreamTestObject(3, 4),new StreamTestObject(5, 6));Stream<StreamTestObject> stream = streamTestObjects.stream();Stream<Integer> id1Stream = streamTestObjects.stream().map(StreamTestObject::getId1);List<Integer> collectList = id1Stream.collect(Collectors.toList());
// Set<Integer> collectSet = id1Stream.collect(Collectors.toSet());// 連起來使用一行代碼可以寫成這樣collectList = streamTestObjects.stream().map(StreamTestObject::getId1).collect(Collectors.toList());
// collectSet = streamTestObjects.stream().map(StreamTestObject::getId1).collect(Collectors.toSet());System.out.println("collectList:" + collectList);
// 輸出結果 collectList:[1, 3, 5]
結果能夠把 id1 成功收集起來,代碼的易讀性也體現在其中。我們一眼就能看出這行代碼映射了id1 這個字段為一個 List 或 Set。
多字段映射
那如果我們想要對象集合中的 id1 和 id2 都匯總到一個 List<Integer> 集合里,應該如何操作呢。
這里我們可以使用一個 flatMap() 方法
- 用途:將流中的每個元素都轉換成另一個流,然后將所有流連接成一個流。
直接上代碼
List<StreamTestObject> streamTestObjects = Arrays.asList(new StreamTestObject(1, 2),new StreamTestObject(3, 4),new StreamTestObject(5, 6));List<Integer> collectList = streamTestObjects.stream().flatMap(object ->Stream.of(object.getId1(), object.getId2())).collect(Collectors.toList());System.out.println("collectList:" + collectList);//輸出結果 collectList:[1, 2, 3, 4, 5, 6]
在這個例子中,Stream.of(obj.getId1(), obj.getId2())為每個對象生成了一個包含兩個ID的流,它在map中 形成了一個臨時的流
然后flatMap將這些流“展平”成了一個包含所有ID的流,最后我們通過collect(Collectors.toList())將這個流收集到了一個列表中。
映射為其他的對象
有的時候的業務需求需要我們把一個對象集合轉化為另外一個集合對象,如果是單純的 字段copy,我們可以使用 BeanUtils?或者 MapStruct 等方法實現。
如果轉化的過程中設計業務邏輯,那么就需要 Stream流出手了。
這里需要設計到一個顯示 return 的寫法,上代碼,先準備一個另外的對象,只包含一個id字段
@Data
@NoArgsConstructor
@AllArgsConstructor
public class StreamOtherObject {Integer id;}
然后 我們將上述測試對象的id1,轉化為這里的id字段。
List<StreamTestObject> streamTestObjects = Arrays.asList(new StreamTestObject(1, 2),new StreamTestObject(3, 4),new StreamTestObject(4, 5));List<StreamOtherObject> collectList = streamTestObjects.stream().map(streamTestObject -> {StreamOtherObject object = new StreamOtherObject();object.setId(streamTestObject.getId1());return object;}).collect(Collectors.toList());System.out.println("collectList:" + collectList);//輸出結果 collectList:[StreamOtherObject(id=1), StreamOtherObject(id=3), StreamOtherObject(id=4)]
注意看返回的集合對象已經是我在表達式里return 的 StreamOtherObject了。
也就是 return 的內容就是集合的具體對象
映射為 Map
Stream流還能把集合映射為一個Map,這里我們測試用例為將映射結果設置為 key 為 id1, value 為對象本身
List<StreamTestObject> streamTestObjects = Arrays.asList(new StreamTestObject(1, 2),new StreamTestObject(3, 4));Map<Integer, StreamTestObject> map = streamTestObjects.stream().collect(Collectors.toMap(StreamTestObject::getId1,Function.identity(),(existsOne, replaceOne) -> replaceOne));System.out.println("collectMap:" + map);//輸出結果 collectMap:{1=StreamTestObject(id1=1, id2=2), 3=StreamTestObject(id1=3, id2=4)}
可以看到,toMap() 方法中,傳遞了3個參數,前兩個分別為 key , value
第三個參數傳了一個表達式,這里的邏輯表示如果發生沖突,就保留 Map中新的那個對象,而不是保留它。同時,第三個參數也處理了沖突,如果你沒有對于 key 相同的情況做處理,也就是 key 沖突了,方法將拋出一個IllegalStateException。
所以你需要做對應的處理,如try catch 下來,或者進行沖突處理,即傳遞第三個參數。
去重 distinct()
對于拿到的流結果,我們有的時候有去重的需求,當然我們可以轉為 toSet()?進行去重
stream流同樣提供了一個方法進行去重,就是 distinct() 方法
- 用途:去除流中的重復元素,返回包含不同元素的 Stream。
這里比較好理解,我們直接看案例
List<StreamTestObject> streamTestObjects = Arrays.asList(new StreamTestObject(1, 2),new StreamTestObject(3, 4),new StreamTestObject(4, 5));List<Integer> collectList = streamTestObjects.stream().flatMap(object ->Stream.of(object.getId1(), object.getId2())).distinct().collect(Collectors.toList());System.out.println("collectList:" + collectList);//輸出結果 collectList:[1, 2, 3, 4, 5]
過濾 filter()
- 用途:根據提供的條件過濾元素,返回滿足條件的 Stream。
過濾的方法也是比較常用的方法,也是比較多業務中有這個需求的。這里介紹兩種方法
在拿到一個流后,也許不是所有的元素我們都需要。我們需要保存滿足特定條件的元素,這時候就可以使用 filter方法來實現。
這里的案例表示篩選除 id1 為 1,id2 為 4 的數據,代碼如下:
List<StreamTestObject> streamTestObjects = Arrays.asList(new StreamTestObject(1, 2),new StreamTestObject(3, 4),new StreamTestObject(4, 5));List<StreamTestObject> collectList = streamTestObjects.stream().filter(object -> object.getId1().equals(1) || object.getId2().equals(4)).collect(Collectors.toList());System.out.println("collectList:" + collectList);//輸出結果 collectList:[StreamTestObject(id1=1, id2=2), StreamTestObject(id1=3, id2=4)]
如果你的過濾邏輯比較復雜可以使用顯示 return 寫法來過濾
List<StreamTestObject> streamTestObjects = Arrays.asList(new StreamTestObject(1, 2),new StreamTestObject(3, 4),new StreamTestObject(4, 5));List<StreamTestObject> collectList = streamTestObjects.stream().filter(object -> {int id1 = 1;int id2 = 4;return object.getId1().equals(id1) || object.getId2().equals(id2);}).collect(Collectors.toList());System.out.println("collectList:" + collectList);//輸出結果 collectList:[StreamTestObject(id1=1, id2=2), StreamTestObject(id1=3, id2=4)]
在代碼塊里可以編輯自己自定義的過濾邏輯
這里要注意返回值是一個布爾值,如果為 true,則保留這項數據,不滿足,則進行一項數據處理。
Stream流的其他方法
前文是 Stream 流比較常見的方法案例,它還提供了很多其他的接口來實現對應的場景,如:
- sorted()
- 用途:對流中的元素進行自然排序(需實現 Comparable 接口),返回排序后的 Stream。
- 示例:對用戶列表按年齡進行排序。
- limit(long maxSize)
- 用途:截斷流,使其包含不超過給定數量的元素,返回截斷后的 Stream。
- 示例:只取用戶列表中的前三個用戶。
- skip(long n)
- 用途:跳過流中的前 n 個元素,返回剩下的元素的 Stream。
- 示例:跳過用戶列表中的前兩個用戶,取后面的用戶。
- forEach(Consumer<? super T> action)
- 用途:這是大家比較熟悉的操作,在代碼編寫中可以省去 .Stream() 的寫法。意為對流中的每個元素執行提供的操作,這是一個終結操作。
- 示例:遍歷用戶列表并打印每個用戶的名字。
使用Stream流的弊端
學習了Stream流 的優點之后,也需要知道隨之產生的弊端有短些,這里我列舉幾個主要的內容
- 性能問題:
- 多次遍歷:有時為了完成一個操作,可能需要多次遍歷數據源。例如,先過濾(filter)再映射(map)最后收集(collect),這會導致數據被多次遍歷。
- 并行流開銷:雖然并行流可以加速處理過程,但它們引入了額外的線程管理開銷,并且不總是能帶來性能提升,尤其是在數據源較小或操作相對簡單時。
- 懶加載導致的意外行為:Stream操作是懶加載的,這意味著它們直到需要結果時才會執行。這可能導致在調試時難以追蹤問題的源頭,或者在某些情況下,當流操作依賴于外部狀態時,可能導致不可預測的行為。
- 可讀性和維護性:
??:前面不是可讀性強嗎,怎么有問題了?如果嵌套太多層的操作方法,也會使得表達式的可讀性降低
- 復雜邏輯難以追蹤:對于包含多個復雜操作(如多重過濾、映射、歸約等)的Stream鏈,其邏輯可能變得難以理解和追蹤。
- 調試困難:由于Stream操作的延遲執行和中間操作的無狀態性,調試Stream代碼可能會比傳統循環更加困難。
- 錯誤處理:
- 異常處理復雜:在Stream操作中處理異常(如嘗試映射一個可能拋出異常的函數)比在傳統循環中更復雜。Stream API沒有直接支持異常處理機制,通常需要通過try-catch塊或自定義函數來處理。
- 內存消耗:
- 中間結果存儲:Stream API在執行過程中可能會創建中間結果的臨時集合,尤其是在進行復雜操作時,這可能會增加內存消耗。
到這里,同學們可以多實操一下這些方法來鞏固知識。文章如有遺漏或建議更改的部分歡迎佬們指出。