<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          全面吃透JAVA Stream流操作,讓代碼更加的優(yōu)雅

          共 16538字,需瀏覽 34分鐘

           ·

          2022-07-24 16:34

          在JAVA中,涉及到對數(shù)組Collection等集合類中的元素進行操作的時候,通常會通過循環(huán)的方式進行逐個處理,或者使用Stream的方式進行處理。

          例如,現(xiàn)在有這么一個需求:

          從給定句子中返回單詞長度大于5的單詞列表,按長度倒序輸出,最多返回3個

          JAVA7及之前的代碼中,我們會可以照如下的方式進行實現(xiàn):

          /**
           * 【常規(guī)方式】
           * 從給定句子中返回單詞長度大于5的單詞列表,按長度倒序輸出,最多返回3個
           *
           * @param sentence 給定的句子,約定非空,且單詞之間僅由一個空格分隔
           * @return 倒序輸出符合條件的單詞列表
           */

          public List<String> sortGetTop3LongWords(@NotNull String sentence) {
              // 先切割句子,獲取具體的單詞信息
              String[] words = sentence.split(" ");
              List<String> wordList = new ArrayList<>();
              // 循環(huán)判斷單詞的長度,先過濾出符合長度要求的單詞
              for (String word : words) {
                  if (word.length() > 5) {
                      wordList.add(word);
                  }
              }
              // 對符合條件的列表按照長度進行排序
              wordList.sort((o1, o2) -> o2.length() - o1.length());
              // 判斷l(xiāng)ist結果長度,如果大于3則截取前三個數(shù)據(jù)的子list返回
              if (wordList.size() > 3) {
                  wordList = wordList.subList(03);
              }
              return wordList;
          }

          JAVA8及之后的版本中,借助Stream流,我們可以更加優(yōu)雅的寫出如下代碼:

              /**
               * 【Stream方式】
               * 從給定句子中返回單詞長度大于5的單詞列表,按長度倒序輸出,最多返回3個
               *
               * @param sentence 給定的句子,約定非空,且單詞之間僅由一個空格分隔
               * @return 倒序輸出符合條件的單詞列表
               */

              public List<String> sortGetTop3LongWordsByStream(@NotNull String sentence) {
                  return Arrays.stream(sentence.split(" "))
                          .filter(word -> word.length() > 5)
                          .sorted((o1, o2) -> o2.length() - o1.length())
                          .limit(3)
                          .collect(Collectors.toList());
              }

          直觀感受上,Stream的實現(xiàn)方式代碼更加簡潔、一氣呵成。很多的同學在代碼中也經(jīng)常使用Stream流,但是對Stream流的認知往往也是僅限于會一些簡單的filtermapcollect等操作,但JAVA的Stream可以適用的場景與能力遠不止這些。

          那么問題來了:Stream相較于傳統(tǒng)的foreach的方式處理stream,到底有啥優(yōu)勢

          這里我們可以先擱置這個問題,先整體全面的了解下Stream,然后再來討論下這個問題。

          筆者結合在團隊中多年的代碼檢視遇到的情況,結合平時項目編碼實踐經(jīng)驗,對Stream的核心要點與易混淆用法典型使用場景等進行了詳細的梳理總結,希望可以幫助大家對Stream有個更全面的認知,也可以更加高效的應用到項目開發(fā)中去。

          Stream初相識

          概括講,可以將Stream流操作分為3種類型

          • 創(chuàng)建Stream

          • Stream中間處理

          • 終止Steam

          每個Stream管道操作類型都包含若干API方法,先列舉下各個API方法的功能介紹。

          • 開始管道

          主要負責新建一個Stream流,或者基于現(xiàn)有的數(shù)組、List、Set、Map等集合類型對象創(chuàng)建出新的Stream流。

          API功能說明
          stream()創(chuàng)建出一個新的stream串行流對象
          parallelStream()創(chuàng)建出一個可并行執(zhí)行的stream流對象
          Stream.of()通過給定的一系列元素創(chuàng)建一個新的Stream串行流對象
          • 中間管道

          負責對Stream進行處理操作,并返回一個新的Stream對象,中間管道操作可以進行疊加

          API功能說明
          filter()按照條件過濾符合要求的元素, 返回新的stream流
          map()將已有元素轉(zhuǎn)換為另一個對象類型,一對一邏輯,返回新的stream流
          flatMap()將已有元素轉(zhuǎn)換為另一個對象類型,一對多邏輯,即原來一個元素對象可能會轉(zhuǎn)換為1個或者多個新類型的元素,返回新的stream流
          limit()僅保留集合前面指定個數(shù)的元素,返回新的stream流
          skip()跳過集合前面指定個數(shù)的元素,返回新的stream流
          concat()將兩個流的數(shù)據(jù)合并起來為1個新的流,返回新的stream流
          distinct()對Stream中所有元素進行去重,返回新的stream流
          sorted()對stream中所有的元素按照指定規(guī)則進行排序,返回新的stream流
          peek()對stream流中的每個元素進行逐個遍歷處理,返回處理后的stream流
          • 終止管道

          顧名思義,通過終止管道操作之后,Stream流將會結束,最后可能會執(zhí)行某些邏輯處理,或者是按照要求返回某些執(zhí)行后的結果數(shù)據(jù)。

          API功能說明
          count()返回stream處理后最終的元素個數(shù)
          max()返回stream處理后的元素最大值
          min()返回stream處理后的元素最小值
          findFirst()找到第一個符合條件的元素時則終止流處理
          findAny()找到任何一個符合條件的元素時則退出流處理,這個對于串行流時與findFirst相同,對于并行流時比較高效,任何分片中找到都會終止后續(xù)計算邏輯
          anyMatch()返回一個boolean值,類似于isContains(),用于判斷是否有符合條件的元素
          allMatch()返回一個boolean值,用于判斷是否所有元素都符合條件
          noneMatch()安徽一個boolean值, 用于判斷是否所有元素都不符合條件
          collect()將流轉(zhuǎn)換為指定的類型,通過Collectors進行指定
          toArray()將流轉(zhuǎn)換為數(shù)組
          iterator()將流轉(zhuǎn)換為Iterator對象
          foreach()無返回值,對元素進行逐個遍歷,然后執(zhí)行給定的處理邏輯

          Stream方法使用

          map與flatMap

          mapflatMap都是用于轉(zhuǎn)換已有的元素為其它元素,區(qū)別點在于:

          • map 必須是一對一的,即每個元素都只能轉(zhuǎn)換為1個新的元素

          • flatMap 可以是一對多的,即每個元素都可以轉(zhuǎn)換為1個或者多個新的元素

          比如:有一個字符串ID列表,現(xiàn)在需要將其轉(zhuǎn)為User對象列表。可以使用map來實現(xiàn):

          /**
           * 演示map的用途:一對一轉(zhuǎn)換
           */

          public void stringToIntMap() {
              List<String> ids = Arrays.asList("205","105","308","469","627","193","111");
              // 使用流操作
              List<Integer> results = ids.stream()
                      .map(s -> Integer.valueOf(s))
                      .collect(Collectors.toList());
              System.out.println(results);
          }

          執(zhí)行之后,會發(fā)現(xiàn)每一個元素都被轉(zhuǎn)換為對應新的元素,但是前后總元素個數(shù)是一致的:

          [User{id='205'}, 
           User{id='105'},
           User{id='308'}, 
           User{id='469'}, 
           User{id='627'}, 
           User{id='193'}, 
           User{id='111'}]

          再比如:現(xiàn)有一個句子列表,需要將句子中每個單詞都提取出來得到一個所有單詞列表。這種情況用map就搞不定了,需要flatMap上場了:

          public void stringToIntFlatmap() {
              List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao");
              // 使用流操作
              List<String> results = sentences.stream()
                      .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
                      .collect(Collectors.toList());
              System.out.println(results);
          }

          執(zhí)行結果如下,可以看到結果列表中元素個數(shù)是比原始列表元素個數(shù)要多的:

          [hello, world, Jia, Gou, Wu, Dao]

          這里需要補充一句,flatMap操作的時候其實是先每個元素處理并返回一個新的Stream,然后將多個Stream展開合并為了一個完整的新的Stream,如下:

          peek和foreach方法

          peekforeach,都可以用于對元素進行遍歷然后逐個的進行處理。

          但根據(jù)前面的介紹,peek屬于中間方法,而foreach屬于終止方法。這也就意味著peek只能作為管道中途的一個處理步驟,而沒法直接執(zhí)行得到結果,其后面必須還要有其它終止操作的時候才會被執(zhí)行;而foreach作為無返回值的終止方法,則可以直接執(zhí)行相關操作。

          public void testPeekAndforeach() {
              List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao");
              // 演示點1: 僅peek操作,最終不會執(zhí)行
              System.out.println("----before peek----");
              sentences.stream().peek(sentence -> System.out.println(sentence));
              System.out.println("----after peek----");
              // 演示點2: 僅foreach操作,最終會執(zhí)行
              System.out.println("----before foreach----");
              sentences.stream().forEach(sentence -> System.out.println(sentence));
              System.out.println("----after foreach----");
              // 演示點3:peek操作后面增加終止操作,peek會執(zhí)行
              System.out.println("----before peek and count----");
              sentences.stream().peek(sentence -> System.out.println(sentence)).count();
              System.out.println("----after peek and count----");
          }

          輸出結果可以看出,peek獨自調(diào)用時并沒有被執(zhí)行、但peek后面加上終止操作之后便可以被執(zhí)行,而foreach可以直接被執(zhí)行:

          ----before peek----
          ----after peek----
          ----before foreach----
          hello world
          Jia Gou Wu Dao
          ----after foreach----
          ----before peek and count----
          hello world
          Jia Gou Wu Dao
          ----after peek and count----

          filter、sorted、distinct、limit

          這幾個都是常用的Stream的中間操作方法,具體的方法的含義在上面的表格里面有說明。具體使用的時候,可以根據(jù)需要選擇一個或者多個進行組合使用,或者同時使用多個相同方法的組合

          public void testGetTargetUsers() {
              List<String> ids = Arrays.asList("205","10","308","49","627","193","111""193");
              // 使用流操作
              List<Dept> results = ids.stream()
                      .filter(s -> s.length() > 2)
                      .distinct()
                      .map(Integer::valueOf)
                      .sorted(Comparator.comparingInt(o -> o))
                      .limit(3)
                      .map(id -> new Dept(id))
                      .collect(Collectors.toList());
              System.out.println(results);
          }

          上面的代碼片段的處理邏輯很清晰:

          1. 使用filter過濾掉不符合條件的數(shù)據(jù)

          2. 通過distinct對存量元素進行去重操作

          3. 通過map操作將字符串轉(zhuǎn)成整數(shù)類型

          4. 借助sorted指定按照數(shù)字大小正序排列

          5. 使用limit截取排在前3位的元素

          6. 又一次使用map將id轉(zhuǎn)為Dept對象類型

          7. 使用collect終止操作將最終處理后的數(shù)據(jù)收集到list中

          輸出結果:

          [Dept{id=111},  Dept{id=193},  Dept{id=205}]

          簡單結果終止方法

          按照前面介紹的,終止方法里面像countmaxminfindAnyfindFirstanyMatchallMatchnonneMatch等方法,均屬于這里說的簡單結果終止方法。所謂簡單,指的是其結果形式是數(shù)字、布爾值或者Optional對象值等。

          public void testSimpleStopOptions() {
              List<String> ids = Arrays.asList("205""10""308""49""627""193""111""193");
              // 統(tǒng)計stream操作后剩余的元素個數(shù)
              System.out.println(ids.stream().filter(s -> s.length() > 2).count());
              // 判斷是否有元素值等于205
              System.out.println(ids.stream().filter(s -> s.length() > 2).anyMatch("205"::equals));
              // findFirst操作
              ids.stream().filter(s -> s.length() > 2)
                      .findFirst()
                      .ifPresent(s -> System.out.println("findFirst:" + s));
          }

          執(zhí)行后結果為:

          6
          true
          findFirst:205

          避坑提醒

          這里需要補充提醒下,一旦一個Stream被執(zhí)行了終止操作之后,后續(xù)便不可以再讀這個流執(zhí)行其他的操作了,否則會報錯,看下面示例:

          public void testHandleStreamAfterClosed() {
              List<String> ids = Arrays.asList("205""10""308""49""627""193""111""193");
              Stream<String> stream = ids.stream().filter(s -> s.length() > 2);
              // 統(tǒng)計stream操作后剩余的元素個數(shù)
              System.out.println(stream.count());
              System.out.println("-----下面會報錯-----");
              // 判斷是否有元素值等于205
              try {
                  System.out.println(stream.anyMatch("205"::equals));
              } catch (Exception e) {
                  e.printStackTrace();
              }
              System.out.println("-----上面會報錯-----");
          }

          執(zhí)行的時候,結果如下:

          6
          -----下面會報錯-----
          java.lang.IllegalStateExceptionstream has already been operated upon or closed
              at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
              at java.util.stream.ReferencePipeline.anyMatch(ReferencePipeline.java:449)
              at com.veezean.skills.stream.StreamService.testHandleStreamAfterClosed(StreamService.java:153)
              at com.veezean.skills.stream.StreamService.main(StreamService.java:176)
          -----上面會報錯-----

          因為stream已經(jīng)被執(zhí)行count()終止方法了,所以對stream再執(zhí)行anyMatch方法的時候,就會報錯stream has already been operated upon or closed,這一點在使用的時候需要特別注意。

          結果收集終止方法

          因為Stream主要用于對集合數(shù)據(jù)的處理場景,所以除了上面幾種獲取簡單結果的終止方法之外,更多的場景是獲取一個集合類的結果對象,比如List、Set或者HashMap等。

          這里就需要collect方法出場了,它可以支持生成如下類型的結果數(shù)據(jù):

          • 一個集合類,比如List、Set或者HashMap等

          • StringBuilder對象,支持將多個字符串進行拼接處理并輸出拼接后結果

          • 一個可以記錄個數(shù)或者計算總和的對象(數(shù)據(jù)批量運算統(tǒng)計

          生成集合

          應該算是collect最常被使用到的一個場景了:

          public void testCollectStopOptions() {
              List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(23));
              // collect成list
              List<Dept> collectList = ids.stream().filter(dept -> dept.getId() > 20)
                      .collect(Collectors.toList());
              System.out.println("collectList:" + collectList);
              // collect成Set
              Set<Dept> collectSet = ids.stream().filter(dept -> dept.getId() > 20)
                      .collect(Collectors.toSet());
              System.out.println("collectSet:" + collectSet);
              // collect成HashMap,key為id,value為Dept對象
              Map<Integer, Dept> collectMap = ids.stream().filter(dept -> dept.getId() > 20)
                      .collect(Collectors.toMap(Dept::getId, dept -> dept));
              System.out.println("collectMap:" + collectMap);
          }

          結果如下:

          collectList:[Dept{id=22}, Dept{id=23}]
          collectSet:[Dept{id=23}, Dept{id=22}]
          collectMap:{22=Dept{id=22}, 23=Dept{id=23}}

          生成拼接字符串

          將一個List或者數(shù)組中的值拼接到一個字符串里并以逗號分隔開,這個場景相信大家都不陌生吧?

          如果通過for循環(huán)和StringBuilder去循環(huán)拼接,還得考慮下最后一個逗號如何處理的問題,很繁瑣:

          public void testForJoinStrings() {
              List<String> ids = Arrays.asList("205""10""308""49""627""193""111""193");
              StringBuilder builder = new StringBuilder();
              for (String id : ids) {
                  builder.append(id).append(',');
              }
              // 去掉末尾多拼接的逗號
              builder.deleteCharAt(builder.length() - 1);
              System.out.println("拼接后:" + builder.toString());
          }

          但是現(xiàn)在有了Stream,使用collect可以輕而易舉的實現(xiàn):

          public void testCollectJoinStrings() {
              List<String> ids = Arrays.asList("205""10""308""49""627""193""111""193");
              String joinResult = ids.stream().collect(Collectors.joining(","));
              System.out.println("拼接后:" + joinResult);
          }

          兩種方式都可以得到完全相同的結果,但Stream的方式更優(yōu)雅:

          拼接后:205,10,308,49,627,193,111,193

          數(shù)據(jù)批量數(shù)學運算

          還有一種場景,實際使用的時候可能會比較少,就是使用collect生成數(shù)字數(shù)據(jù)的總和信息,也可以了解下實現(xiàn)方式:

          public void testNumberCalculate() {
              List<Integer> ids = Arrays.asList(1020304050);
              // 計算平均值
              Double average = ids.stream().collect(Collectors.averagingInt(value -> value));
              System.out.println("平均值:" + average);
              // 數(shù)據(jù)統(tǒng)計信息
              IntSummaryStatistics summary = ids.stream().collect(Collectors.summarizingInt(value -> value));
              System.out.println("數(shù)據(jù)統(tǒng)計信息: " + summary);
          }

          上面的例子中,使用collect方法來對list中元素值進行數(shù)學運算,結果如下:

          平均值:30.0
          總和:IntSummaryStatistics{count=5, sum=150min=10, average=30.000000max=50}

          并行Stream

          機制說明

          使用并行流,可以有效利用計算機的多CPU硬件,提升邏輯的執(zhí)行速度。并行流通過將一整個stream劃分為多個片段,然后對各個分片流并行執(zhí)行處理邏輯,最后將各個分片流的執(zhí)行結果匯總為一個整體流。

          約束與限制

          并行流類似于多線程在并行處理,所以與多線程場景相關的一些問題同樣會存在,比如死鎖等問題,所以在并行流終止執(zhí)行的函數(shù)邏輯,必須要保證線程安全

          回答最初的問題

          到這里,關于JAVA Stream的相關概念與用法介紹,基本就講完了。我們再把焦點切回本文剛開始時提及的一個問題:

          Stream相較于傳統(tǒng)的foreach的方式處理stream,到底有啥優(yōu)勢

          根據(jù)前面的介紹,我們應該可以得出如下幾點答案:

          • 代碼更簡潔、偏聲明式的編碼風格,更容易體現(xiàn)出代碼的邏輯意圖

          • 邏輯間解耦,一個stream中間處理邏輯,無需關注上游與下游的內(nèi)容,只需要按約定實現(xiàn)自身邏輯即可

          • 并行流場景效率會比迭代器逐個循環(huán)更高

          • 函數(shù)式接口,延遲執(zhí)行的特性,中間管道操作不管有多少步驟都不會立即執(zhí)行,只有遇到終止操作的時候才會開始執(zhí)行,可以避免一些中間不必要的操作消耗

          當然了,Stream也不全是優(yōu)點,在有些方面也有其弊端:

          • 代碼調(diào)測debug不便

          • 程序員從歷史寫法切換到Stream時,需要一定的適應時間

          總結

          好啦,關于JAVA Stream的理解要點與使用技能的闡述就先到這里啦。那通過上面的介紹,各位小伙伴們是否已經(jīng)躍躍欲試了呢?快去項目中使用體驗下吧!當然啦,如果有疑問,也歡迎找我一起探討探討咯。

          瀏覽 35
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲毛片在线 | 精品秘 一区二三区免费 | 亚洲 欧美 另类 综合 偷拍 | 五月天黄网站 | 亚洲成a人v欧美综合天堂 |