Java 16 中新增的 Stream 介面的一些思考

乾貨滿滿張雜湊發表於2022-01-05

這裡先提一個題外話,如果想看 JDK 不同版本之間有何差異,增加或者刪除了哪些 API,可以通過下面這個連結檢視:

路徑中的兩個版本就是要對比的兩個版本,其介面如下:

image

同時,我們也可以通過 JDK 內建 jdeps 工具查詢過期以及廢棄API以及對應的替換


jdeps --jdk-internals -R --class-path 'libs/*' $project

libs是你的所有依賴的目錄,$project是你的專案jar包,示例輸出:

...
JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.misc.BASE64Encoder                   Use java.util.Base64 @since 1.8
sun.reflect.Reflection                   Use java.lang.StackWalker @since 9

Java 16 中針對 Stream API 有兩個更新:

image

增加了 mapMultitoList 這兩個 API。

mapMulti

mapMulti 其實主要是對現有的 flatMap 在某些場景使用起來不夠合適的補充。flatMap 是將一個物件對映為多個物件之後繼續 Stream,例如將 List<List<Integer>> 裡面的每一個數字取出,轉換成一個新的 List<Integer>

integerLists.stream().flatMap(integers -> integers.stream()).collect(Collectors.toList());

這對於每個元素本身就是集合型別的場景來說,非常適用。我們再來看一個例子,假設有郵件這個 Record 類,包含 id,以及傳送到的郵箱和抄送到的郵箱:

record Mail(int id, Set<String> sendTo, Set<String> cc) {}

我們想找到一批郵件的所有不同的聯絡人,最後放到一個 List 中,可能會這麼寫:

Set<String> collect = mails.stream().flatMap(mail -> {
	Set<String> result = new HashSet<>();
	result.addAll(mail.sendTo());
	result.addAll(mail.cc());
	return result.stream();
}).collect(Collectors.toSet());

但是,這樣寫顯然很不優雅,首先是對於每一個 Mail 都建立了額外的 Set 和對應的 Stream,並且,對於每個 mail 的 sendTo 還有 cc 都遍歷了兩遍(addAll 一遍,後續 Stream 又一遍)。其實我們的目前只是將 mail 中的 cc 以及 sendTo 取出來,用於參與後續的 Stream。在這種場景下,就非常適合用 mapMulti:

Set<String> collect = mails.stream().<String>mapMulti((mail, consumer) -> {
	mail.cc().forEach(consumer::accept);
	mail.sendTo().forEach(consumer::accept);
}).collect(Collectors.toSet());

可以看出:

  • mapMulti 的入參是一個 BiConsumer,其實就是使用其引數中的 consumer 接收參與 Stream 後續的物件
  • mapMulti 的思路就是將引數中的需要參與後續 Stream 的物件傳入 consumer 來繼續 Stream
  • consumer 沒有限制物件型別,想要限制必須加上形參 <String> 否則最後返回的是 Set<Object> 而不是 Set<String>

假設 mail 的 sendTo 還有 cc 都需要去其他地方獲取,使用 mapMulti 還可以實現:

Set<String> collect = mailIds.stream().<String>mapMulti((mailId, consumer) -> {
	mailService.getCCById(mailId).forEach(consumer::accept);
	mailService.getSendToById(mailId).forEach(consumer::accept);
}).collect(Collectors.toSet());

還有一些比較有意思的用法,例如混合型別的 List 轉換成統一型別:

class C {
     static void expandIterable(Object e, Consumer<Object> c) {
         if (e instanceof Iterable<?> elements) {
             for (Object ie : elements) {
                 expandIterable(ie, c);
             }
         } else if (e != null) {
             c.accept(e);
         }
     }
    
     public static void main(String[] args) {
         var nestedList = List.of(1, List.of(2, List.of(3, 4)), 5);
         Stream<Object> expandedStream = nestedList.stream().mapMulti(C::expandIterable);
     }
}

活用 Optional.ifPresent(Consumer<? super T> action) 方法:

Stream.of(Optional.of("0"), Optional.of("1"), Optional.empty())
	.mapMulti(Optional::ifPresent)
	.forEach(System.out::print);

toList

對於 Stream 增加了 toList 直接轉換成 List,由於不涉及 collect 裡面的截斷操作,所以比 collect 佔用的記憶體更小,需要的操作更少並且更快

之前轉換成 List,需要 collect(Collectors.toList())生成的 List 是 ArrayList,是可變的

但是這次新加的 Api,toList 生成的是 UnmodifiableList,是不可變的

所以這兩個 API 不能直接互相替換,需要做一些檢查確認沒有更改才能替換。

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

相關文章