<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      研發(fā)軟件的郭

      一步一步,研發(fā)之路

      從頭學(xué)Java17-Stream API(一)

      Stream API

      Stream API 是按照map/filter/reduce方法處理內(nèi)存中數(shù)據(jù)的最佳工具。
      本系列中的教程包含從基本概念一直到collector設(shè)計(jì)和并行流。

      logo2

      在流上添加中繼操作

      將一個(gè)流map為另一個(gè)流

      mapping流就是使用函數(shù)轉(zhuǎn)換其元素。此轉(zhuǎn)換可能會更改該流處理的元素的類型。

      您可以使用 map() 方法將一個(gè)流map為另一個(gè)流,該方法用Function作為參數(shù)。mapping一個(gè)流意味著該流的所有元素都將使用該函數(shù)進(jìn)行轉(zhuǎn)換。

      代碼模式如下:

      List<String> strings = List.of("one", "two", "three", "four");
      Function<String, Integer> toLength = String::length;
      Stream<Integer> ints = strings.stream()
                                    .map(toLength);
      

      此代碼粘貼到 IDE 運(yùn)行時(shí),你不會看到任何東西,你可能想知道為什么。

      答案其實(shí)很簡單:該流上沒有定義末端操作。這段代碼沒有做任何事情。它不處理任何數(shù)據(jù)。

      讓我們添加一個(gè)非常有用的末端操作collect(Collectors.toList()),它將處理后的元素放在一個(gè)列表中。如果您不確定此代碼的真正作用,請不要擔(dān)心;我們將在本教程的后面部分介紹這一點(diǎn)。代碼將變?yōu)橐韵聝?nèi)容。

      List<String> strings = List.of("one", "two", "three", "four");
      List<Integer> lengths = strings.stream()
                                     .map(String::length)
                                     .collect(Collectors.toList());
      System.out.println("lengths = " + lengths);
      

      運(yùn)行此代碼將打印以下內(nèi)容:

      lengths = [3, 3, 5, 4]
      

      您可以看到此模式創(chuàng)建了一個(gè) Stream<Integer>,由 map(String::length) 返回。你也可以通過調(diào)用mapToInt()來使其成為一個(gè)專門的IntStream。這個(gè)mapToInt()方法以ToIntFuction作參數(shù)。在上一示例中.map(String::length)更改為.mapToInt(String::length) 不會創(chuàng)建編譯器錯(cuò)誤。String::length方法引用可以是兩種類型:Function<String、Integer>ToIntFunction<String>

      專用流沒有 collect() 方法將Collector作參數(shù)。因此,如果用 mapToInt(),則無法再在列表中收集結(jié)果。讓我們獲取有關(guān)該流的一些統(tǒng)計(jì)信息。這個(gè) summaryStatistics() 方法非常方便,并且僅在專門的原始類型流上可用。

      List<String> strings = List.of("one", "two", "three", "four");
      IntSummaryStatistics stats = strings.stream()
                                          .mapToInt(String::length)
                                          .summaryStatistics();
      System.out.println("stats = " + stats);
      

      結(jié)果如下:

      stats = IntSummaryStatistics{count=4, sum=15, min=3, average=3,750000, max=5}
      

      Stream 轉(zhuǎn)為原始類型流有三種方法:mapToInt()、mapToLong()mapToDouble()。

      filter流

      filtering就是在流處理中使用Predicate丟棄某些元素。此方法可用于對象流和原始類型流。

      假設(shè)您需要計(jì)算長度為 3 的字符串。您可以編寫以下代碼來執(zhí)行此操作:

      List<String> strings = List.of("one", "two", "three", "four");
      long count = strings.stream()
                          .map(String::length)
                          .filter(length -> length == 3)
                          .count();
      System.out.println("count = " + count);
      

      運(yùn)行此代碼將生成以下內(nèi)容:

      count = 2
      

      請注意,您剛剛使用了 Stream API 的另一個(gè)末端操作 count(),它只計(jì)算已處理元素的數(shù)量。此方法返回long ,您可以使用它計(jì)算很多元素。比 ArrayList 里面的更多。

      flatmap流以處理 1:p 關(guān)系

      讓我們在一個(gè)示例中查看 flatMap 操作。假設(shè)您有兩個(gè)實(shí)體:StateCity。一個(gè)state實(shí)例包含多個(gè)city實(shí)例,存儲在一個(gè)列表中。

      這是City類的代碼。

      public class City {
          
          private String name;
          private int population;
      
          // constructors, getters
          // toString, equals and hashCode
      }
      

      這是State類的代碼,以及與City類的關(guān)系。

      public class State {
          
          private String name;
          private List<City> cities;
      
          // constructors, getters
          // toString, equals and hashCode
      }
      

      假設(shè)您的代碼正在處理狀態(tài)列表,并且在某些時(shí)候您需要計(jì)算所有城市的人口。

      您可以編寫以下代碼:

      List<State> states = ...;
      
      int totalPopulation = 0;
      for (State state: states) {
          for (City city: state.getCities()) {
              totalPopulation += city.getPopulation();
          }
      }
      
      System.out.println("Total population = " + totalPopulation);
      

      此代碼的內(nèi)部循環(huán)是 map-reduce 的一種形式,也可以使用流編寫:

      totalPopulation += state.getCities().stream().mapToInt(City::getPopulation).sum();
      

      外層和內(nèi)層有點(diǎn)不匹配,將流放入循環(huán)中不是一個(gè)很好的代碼模式。

      這正是flatmap的作用。此運(yùn)算符在對象之間打開一對多關(guān)系,并基于這些關(guān)系創(chuàng)建流。flatMap() 方法將一個(gè)特殊函數(shù)作為參數(shù),這個(gè)函數(shù)返回 Stream 對象。類與類之間的關(guān)系由此函數(shù)定義。

      在我們的示例中,此函數(shù)很簡單,因?yàn)?code>State類中有一個(gè)List<City>。所以你可以按以下方式編寫它。

      //根據(jù)state和city的關(guān)系,生成city流
      Function<State, Stream<City>> stateToCity = state -> state.getCities().stream();
      

      List類型不是強(qiáng)制的。假設(shè)您有一個(gè)包含 Map <String,Country>Continent類,其中鍵是國家/地區(qū)的代碼(CAN 表示加拿大,MEX 表示墨西哥,F(xiàn)RA 表示法國等)。假設(shè)該類有一個(gè)返回此map的方法getCountries()。

      這種情況下,可以通過這種方式編寫此函數(shù)。

      Function<Continent, Stream<Country>> continentToCountry = 
          continent -> continent.getCountries().values().stream();
      

      flatMap() 方法的處理分兩個(gè)步驟。

      • 第一步,使用此函數(shù)mapping流的所有元素。從Stream<State>創(chuàng)建一個(gè) Stream<Stream<City>> ,每個(gè)州都map為城市流。
      • 第二步,展平產(chǎn)生的流。您最終會得到一個(gè)單一的流,其中包含所有州的所有城市。

      因此,使用flatmap,之前的嵌套 for 編寫的代碼可以改寫為:

      List<State> states = ...;
      
      int totalPopulation = 
              states.stream()
                    .flatMap(state -> state.getCities().stream())//對每個(gè)state,都轉(zhuǎn)換為city流,最后合并
                    .mapToInt(City::getPopulation)
                    .sum();
      
      System.out.println("Total population = " + totalPopulation);
      

      使用flatmap和 MapMulti 驗(yàn)證元素轉(zhuǎn)換

      flatMap 可用于流元素的轉(zhuǎn)換中的驗(yàn)證。

      假設(shè)您有一個(gè)表示整數(shù)的字符串流。您需要用 Integer.parseInt() 將它們轉(zhuǎn)為整數(shù)。不幸的是,其中一些字符串有問題:也許有些字符串為空,null,或者末尾有額外的空白字符。這些都會使解析失敗,并出現(xiàn) NumberFormatException。當(dāng)然,您可以嘗試filter此流,用Predicate刪除錯(cuò)誤的字符串,但最安全的方法是使用 try-catch 模式。

      如下所示。

      Predicate<String> isANumber = s -> {
          try {
              int i = Integer.parseInt(s);
              return true;
          } catch (NumberFormatException e) {
              return false;
          }
      };
      

      第一個(gè)缺陷是您需要實(shí)際進(jìn)行轉(zhuǎn)換以查看它是否有效。然后,您不得不在mapping函數(shù)中再次執(zhí)行此操作:不要這樣做!第二個(gè)缺陷是,從catch塊return,絕不是一個(gè)好主意。

      您真正需要做的是,當(dāng)此字符串中有一個(gè)正確的整數(shù)時(shí)返回一個(gè)整數(shù),如果有問題,則什么都不返回。這是flatmap的工作。如果可以解析整數(shù),則可以返回包含結(jié)果的流。另一種情況下,您可以返回空流。

      然后,可以編寫以下函數(shù)。

      Function<String, Stream<Integer>> flatParser = s -> {//根據(jù)String與Integer的關(guān)系,生成Integer流
          try {
              return Stream.of(Integer.parseInt(s));
          } catch (NumberFormatException e) {
          }
          return Stream.empty();
      };
      
      List<String> strings = List.of("1", " ", "2", "3 ", "", "3");
      List<Integer> ints = 
          strings.stream()
                 .flatMap(flatParser)//對每個(gè)String,都轉(zhuǎn)為Integer流,最后合并 flatmap會跳過空流
                 .collect(Collectors.toList());
      System.out.println("ints = " + ints);
      

      運(yùn)行此代碼將生成以下結(jié)果。所有有問題的字符串都已靜默刪除。

      ints = [1, 2, 3]
      

      這種flatmap代碼的使用效果很好,但它有一個(gè)開銷:為流的每個(gè)元素都會創(chuàng)建一個(gè)流。從 Java SE 16 開始,Stream API 中添加了一個(gè)方法:當(dāng)您創(chuàng)建零個(gè)或一個(gè)對象的多個(gè)流時(shí)。此方法稱為mapMulti(),并將BiConsumer作為參數(shù)。

      BiConsumer 使用兩個(gè)參數(shù):

      • 需要mapping的流元素
      • 對mapping結(jié)果調(diào)用的Consumer

      調(diào)用Consumer會將該元素添加到生成的流中。如果mapping無法完成,則biconsumer不會調(diào)用此消費(fèi)者,并且不會添加任何元素。

      讓我們用這個(gè) mapMulti() 方法重寫你的模式。

      List<Integer> ints =
              strings.stream()
                     .<Integer>mapMulti((string, consumer) -> {//方法前面聲明Integer
                          try {
                              consumer.accept(Integer.parseInt(string));//直接說明跟Integer的關(guān)系,生成最終Integer流
                          } catch (NumberFormatException ignored) {
                          }
                     })
                     .collect(Collectors.toList());
      System.out.println("ints = " + ints);
      

      運(yùn)行此代碼會產(chǎn)生與以前相同的結(jié)果。所有有問題的字符串都已被靜默刪除,但這一次,沒有創(chuàng)建其他流。

      ints = [1, 2, 3]
      

      使用此方法,需要告訴編譯器 Consumer 的類型。通過這種特殊語法,在 mapMulti() 前聲明此類型。它不是您在 Java 代碼中經(jīng)常看到的語法。您可以在靜態(tài)和非靜態(tài)上下文中使用它。

      刪除重復(fù)項(xiàng)并對流進(jìn)行排序

      Stream API 有兩個(gè)方法,distinct()sorted(),去重和排序。distinct() 方法使用 hashCode() 和 equals() 方法來發(fā)現(xiàn)重復(fù)項(xiàng)。sorted() 方法有一個(gè)重載,需要一個(gè)comparator,用于比較和排序。如果未提供,則假定流元素具有可比性。否則,則會引發(fā) ClassCastException。

      您可能還記得本教程的前一部分,流應(yīng)該是不存儲任何數(shù)據(jù)的空對象。此規(guī)則也有例外,這兩個(gè)方法就是。

      事實(shí)上,為了發(fā)現(xiàn)重復(fù)項(xiàng),distinct() 方法需要存儲流元素。當(dāng)處理一個(gè)元素時(shí),首先檢查該元素是否見到過。

      sorted() 也是如此。此方法需要存儲所有元素,然后在內(nèi)部緩沖區(qū)中對它們進(jìn)行排序,再發(fā)送到管道的下一步。

      distinct() 可以用于非綁定(無限)流,而 sorted() 不能。

      限制和跳過流的元素

      Stream API 提供了兩種選擇流元素的方法:基于索引或使用Predicate。

      第一種方法,使用 skip() 和 limit() 方法,兩者都將 long 作為參數(shù)。使用這些方法時(shí),需要避免一個(gè)小陷阱。您需要記住,每次在流中調(diào)用中繼方法時(shí),都會創(chuàng)建一個(gè)新流。因此,如果您在 skip() 之后調(diào)用 limit(),請不要忘記從該新流開始計(jì)算。

      假設(shè)您有一個(gè)包含所有整數(shù)的流,從 1 開始。您需要選擇 3 到 8 之間的整數(shù)。正確的代碼如下。

      List<Integer> ints = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
      
      List<Integer> result = 
          ints.stream()
              .skip(2)//產(chǎn)生了新流
              .limit(5)//不是limit(8)
              .collect(Collectors.toList());
      
      System.out.println("result = " + result);
      

      此代碼打印以下內(nèi)容。

      result = [3, 4, 5, 6, 7]
      

      Java SE 9 又引入了兩種方法。它不是根據(jù)元素在流中的索引跳過和限制元素,而是根據(jù)Predicate。

      • dropWhile(Predicate)如果Predicate為 true,一直跳過元素,直到Predicate為 false。此時(shí),該流后面所有元素都將傳輸?shù)较乱粋€(gè)流。
      • takeWhile(Predicate)做相反的事情:如果Predicate為 true,它一直將元素傳輸?shù)较乱粋€(gè)流,直到Predicate為false,后面都跳過。這個(gè)是短路的

      請注意,這些方法的工作方式類似于門。一旦 dropWhile() 打開了門讓處理后的元素流動,它就不會關(guān)閉它。一旦 takeWhile() 關(guān)閉了門,它就不能重新打開它,沒有更多的元素將被發(fā)送到下一個(gè)操作。

      串聯(lián)流

      Stream API 提供了多種模式,可將多個(gè)流連接成一個(gè)。最明顯的方法是使用 Stream 接口中定義的工廠方法:concat()。

      此方法接收兩個(gè)流并生成一個(gè)流,其中包含第一個(gè)流的元素,然后是第二個(gè)流的元素。

      您可能想知道為什么此方法不用 vararg 來連接任意數(shù)量的流。如果你有兩個(gè)以上,JavaDoc API文檔建議你使用另一種模式,基于flatmap。

      讓我們在一個(gè)例子上看看這是如何工作的。

      List<Integer> list0 = List.of(1, 2, 3);
      List<Integer> list1 = List.of(4, 5, 6);
      List<Integer> list2 = List.of(7, 8, 9);
      
      // 1st pattern: concat
      List<Integer> concat = 
          Stream.concat(list0.stream(), list1.stream())
                .collect(Collectors.toList());
      
      // 2nd pattern: flatMap
      List<Integer> flatMap =
          Stream.of(list0.stream(), list1.stream(), list2.stream())//類似city的外層組成的流
                .flatMap(Function.identity())//變成了city流,每個(gè)Stream<Integer>要變成Stream<Integer>,原樣返回即可
                .collect(Collectors.toList());
      
      System.out.println("concat  = " + concat);
      System.out.println("flatMap = " + flatMap);
      

      運(yùn)行此代碼將產(chǎn)生以下結(jié)果:

      concat  = [1, 2, 3, 4, 5, 6]
      flatMap = [1, 2, 3, 4, 5, 6, 7, 8, 9]
      

      建議使用 flatMap() 方式的原因是 concat() 在連接期間會創(chuàng)建中繼流,來連接兩個(gè)流。如果需要連接多個(gè),則最終每次串聯(lián)都需要一個(gè)很快就會被丟棄的流。

      使用flatmap模式,您只需創(chuàng)建一個(gè)流來保存所有流并執(zhí)行flatmap。開銷要低得多。

      您可能想知道為什么添加了這兩種模式??雌饋?concat() 并不是很有用。事實(shí)上,如果連接的兩個(gè)流的源的大小已知,則生成的流的大小也是已知的,只是兩個(gè)串聯(lián)流的總和。

      如果連接的兩個(gè)流的源的大小已知,則生成的流的大小也是已知的。實(shí)際上,它只是兩個(gè)串聯(lián)流的總和。

      在流上使用flatmap可能會創(chuàng)建未知數(shù)量的元素,以便在生成的流中進(jìn)行處理。Stream API 會丟失對元素數(shù)量的跟蹤。

      換句話說:concat 產(chǎn)生一個(gè) SIZED 流,而flatmap不會。此 SIZED 屬性是流可能具有的一種屬性,本教程稍后將介紹。

      調(diào)試流

      有時(shí),在運(yùn)行時(shí)能檢查流處理的元素可能很方便。Stream API 有一個(gè)方法:peek() 方法。此方法用于調(diào)試數(shù)據(jù)處理管道。不應(yīng)在生產(chǎn)代碼中使用此方法。

      絕對不要使用此方法在應(yīng)用程序中執(zhí)行一些副作用。

      此方法將Consumer作為參數(shù),將每個(gè)元素上調(diào)用。讓我們實(shí)際效果。

      List<String> strings = List.of("one", "two", "three", "four");
      List<String> result =
              strings.stream()
                      .peek(s -> System.out.println("Starting with = " + s))
                      .filter(s -> s.startsWith("t"))
                      .peek(s -> System.out.println("Filtered = " + s))
                      .map(String::toUpperCase)
                      .peek(s -> System.out.println("Mapped = " + s))
                      .collect(Collectors.toList());
      System.out.println("result = " + result);
      

      如果運(yùn)行此代碼,您將在控制臺上看到以下內(nèi)容。

      Starting with = one
      Starting with = two
      Filtered = two
      Mapped = TWO
      Starting with = three
      Filtered = three
      Mapped = THREE
      Starting with = four
      result = [TWO, THREE]
      

      讓我們分析一下這個(gè)輸出。

      1. 要處理的第一個(gè)元素是one。你可以看到它被filter掉了。
      2. 第二個(gè)是two。此元素通過filter,然后map為大寫。然后將其添加到結(jié)果列表中。
      3. 第三個(gè)是three,它也通過filter。
      4. 最后一個(gè)是four,被filtering步驟拒絕。

      有一點(diǎn)你在本教程前面看到,現(xiàn)在很明顯:流確實(shí)一一處理了它必須處理的所有元素,從流的開始到結(jié)束。這在之前已經(jīng)提到過,現(xiàn)在你可以看到它的實(shí)際效果。

      您可以看到,此peek(System.out::println)模式對于逐個(gè)跟蹤流處理的元素非常有用,無需調(diào)試代碼。調(diào)試流很困難,需要小心放置斷點(diǎn)的位置。大多數(shù)情況下,在流處理上放置斷點(diǎn)會跳轉(zhuǎn)到Stream接口的實(shí)現(xiàn)。這不是你需要的。您需要將這些斷點(diǎn)放在 lambda 表達(dá)式的代碼中。

      創(chuàng)建流

      創(chuàng)建流

      在本教程中,您已經(jīng)創(chuàng)建了許多流,所有這些都是通過調(diào)用 Collection 接口的 stream() 方法創(chuàng)建的。此方法非常方便:只需要兩行簡單的代碼,您可以使用此流來試驗(yàn)Stream API 的幾乎任何功能。

      如您所見,還有許多其他方法。了解這些方法后,您可以在應(yīng)用程序中的許多位置利用 Stream API,并編寫更具可讀性和可維護(hù)性的代碼。

      讓我們快速瀏覽您將在本教程中看到的內(nèi)容,然后再深入研究它們中的每一個(gè)。

      第一組模式使用 Stream 接口中的工廠方法。使用它們,您可以從以下元素創(chuàng)建流:

      • vararg 參數(shù);
      • supplier;
      • unary operator,從前一個(gè)元素生成下一個(gè)元素;
      • builder。

      您甚至可以創(chuàng)建空流,這在某些情況下可能很方便。

      您已經(jīng)看到可以在集合上創(chuàng)建流。如果您擁有的只是一個(gè)iterator,而不是一個(gè)成熟的集合,那么您可以在iterator上創(chuàng)建流。如果你有一個(gè)數(shù)組,那么還有一個(gè)模式可以在數(shù)組的元素上創(chuàng)建一個(gè)流。

      它并不止于此。JDK 中的許多模式也已添加到眾所周知的對象中。然后,您可以從以下元素創(chuàng)建流:

      • 字符串的字符;
      • 文本文件的行;
      • 通過使用正則表達(dá)式拆分字符串來創(chuàng)建的元素;
      • 一個(gè)隨機(jī)變量,可以創(chuàng)建隨機(jī)數(shù)流。

      您還可以使用builder模式創(chuàng)建流。

      從集合或iterator創(chuàng)建流

      您已經(jīng)知道Collection接口中有一個(gè)可用的 stream() 。這可能是創(chuàng)建流的最經(jīng)典方法。

      在某些情況下,您可能需要基于map的內(nèi)容創(chuàng)建流。Map 接口中沒有stream()方法,因此無法直接創(chuàng)建。但是,您可以通過三個(gè)集合訪問map的內(nèi)容:

      Stream API 提供了一種從簡單iterator創(chuàng)建流的模式,它可能是在非標(biāo)準(zhǔn)數(shù)據(jù)源上創(chuàng)建流的非常方便的方法。模式如下。

      Iterator<String> iterator = ...;
      
      long estimateSize = 10L;
      int characteristics = 0;
      Spliterator<String> spliterator = Spliterators.spliterator(iterator, estimateSize, characteristics);
      
      boolean parallel = false;
      Stream<String> stream = StreamSupport.stream(spliterator, parallel);
      

      此模式包含幾個(gè)神奇元素,本教程稍后將介紹。讓我們快速瀏覽它們。

      estimateSize是您認(rèn)為此流將消費(fèi)的元素?cái)?shù)。在某些情況下,此信息很容易獲得:例如,如果要在數(shù)組或集合上創(chuàng)建流。但某些情況下是未知的。

      本教程稍后將介紹characteristics參數(shù)。它用于優(yōu)化數(shù)據(jù)的處理。

      parallel參數(shù)告知 API 要?jiǎng)?chuàng)建的流是否為并行流。本教程稍后將介紹。

      創(chuàng)建空流

      讓我們從最簡單的開始:創(chuàng)建一個(gè)空流。Stream接口中有一個(gè)工廠方法。您可以通過以下方式使用它。

      Stream<String> empty = Stream.empty();
      List<String> strings = empty.collect(Collectors.toList());
      
      System.out.println("strings = " + strings);
      

      運(yùn)行此代碼會在主機(jī)上顯示以下內(nèi)容。

      strings = []
      

      在某些情況下,創(chuàng)建空流可能非常方便。事實(shí)上,您在本教程的前一部分看到了一個(gè),使用空流和flatmap從流中刪除無效元素。從 Java SE 16 開始,此模式已被 mapMulti() 模式所取代。

      從 vararg 或數(shù)組創(chuàng)建流

      兩種模式非常相似。第一個(gè)在 Stream 接口中使用 of() 工廠方法。第二個(gè)使用 Arrays 工廠類的 stream() 工廠方法。事實(shí)上,如果你檢查 Stream.of() 方法的源代碼,你會看到它調(diào)用了 Arrays.stream()。

      這是第一個(gè)實(shí)際模式。

      Stream<Integer> intStream = Stream.of(1, 2, 3);
      List<Integer> ints = intStream.collect(Collectors.toList());
      
      System.out.println("ints = " + ints);
      

      運(yùn)行第一個(gè)示例將提供以下內(nèi)容:

      ints = [1, 2, 3]
      

      這是第二個(gè)。

      String[] stringArray = {"one", "two", "three"};
      Stream<String> stringStream = Arrays.stream(stringArray);
      List<String> strings = stringStream.collect(Collectors.toList());
      
      System.out.println("strings = " + strings);
      

      運(yùn)行第二個(gè)示例將提供以下內(nèi)容:

      strings = [one, two, three]
      

      從supplier創(chuàng)建流

      Stream 接口上有兩種工廠方法。

      第一個(gè)是 generate(),以supplier為參數(shù)。每次需要新元素時(shí),都會調(diào)用該supplier。

      您可以使用以下代碼創(chuàng)建這樣的流,但不要這樣做!

      Stream<String> generated = Stream.generate(() -> "+");
      List<String> strings = generated.collect(Collectors.toList());
      

      如果你運(yùn)行這段代碼,你會發(fā)現(xiàn)它永遠(yuǎn)不會停止。如果您這樣做并且有足夠的耐心,您可能會看到 OutOfMemoryError。如果沒有,最好通過 IDE 終止應(yīng)用程序。它真的產(chǎn)生了無限的流。

      我們還沒有介紹這一點(diǎn),但擁有這樣的流是完全合法的!您可能想知道它們有什么用?事實(shí)上有很多。要使用它們,您需要在某個(gè)時(shí)候截?cái)啻肆?,而Stream API 為您提供了幾種方法來執(zhí)行此操作。

      你已經(jīng)看到了一個(gè),是調(diào)用該流上的 limit()。讓我們重寫前面的示例,并修復(fù)它。

      Stream<String> generated = Stream.generate(() -> "+");
      List<String> strings = 
              generated
                 .limit(10L)
                 .collect(Collectors.toList());
      
      System.out.println("strings = " + strings);
      

      運(yùn)行此代碼將打印以下內(nèi)容。

      strings = [+, +, +, +, +, +, +, +, +, +]
      

      limit() 方法稱為短路方法:它可以停止流元素的消費(fèi)。

      從unary operator和種子創(chuàng)建流

      如果您需要生成常量的流,使用supplier非常有用。如果你需要一個(gè)具有不同值的無限流,那么你可以使用 iterate() 模式。

      此模式適用于種子,種子是第一個(gè)生成的元素。然后,它使用 UnaryOperator 轉(zhuǎn)換前一個(gè)元素來生成流的下一個(gè)元素。

      Stream<String> iterated = Stream.iterate("+", s -> s + "+");//根據(jù)前后關(guān)系函數(shù),挨個(gè)生成無限流
      iterated.limit(5L).forEach(System.out::println);
      

      您應(yīng)該看到以下結(jié)果。

      +
      ++
      +++
      ++++
      +++++
      

      使用此模式時(shí),不要忘記限制元素?cái)?shù)。

      從 Java SE 9 開始,此模式具有重載,它將Predicate作為參數(shù)。當(dāng)此Predicate變?yōu)?false 時(shí),iterate() 方法將停止生成元素。前面的代碼可以通過以下方式使用此模式。

      Stream<String> iterated = Stream.iterate("+", s -> s.length() <= 5, s -> s + "+");
      iterated.forEach(System.out::println);
      

      運(yùn)行此代碼會得到與上一個(gè)代碼相同的結(jié)果。

      從一系列數(shù)字創(chuàng)建流

      使用以前的模式可以創(chuàng)建一系列數(shù)字。但是,使用專門的數(shù)字流及其 range() 工廠方法會更容易。

      range() 接收初始值和范圍的上限(不包含)為參數(shù)。也可以在 rangeClosed() 方法中包含上限。調(diào)用 LongStream.range(0L, 10L) 將簡單地生成一個(gè)流,其中所有l(wèi)ong都在 0 到 9 之間。

      這個(gè) range() 方法也可以用來遍歷數(shù)組的元素。這是您可以做到這一點(diǎn)的方法。

      String[] letters = {"A", "B", "C", "D"};
      List<String> listLetters =
          IntStream.range(0, 10)
                   .mapToObj(index -> letters[index % letters.length])//實(shí)現(xiàn)了數(shù)組的遍歷
                   .collect(Collectors.toList());
      System.out.println("listLetters = " + listLeters);
      

      結(jié)果如下。

      listLetters = [A, B, C, D, A, B, C, D, A, B]
      

      基于此模式,您可以做很多事情。請注意,由于 IntStream.range() 創(chuàng)建了一個(gè) IntStream(原始類型流),因此您需要使用 mapToObj() 方法將其轉(zhuǎn)換為對象流。

      創(chuàng)建隨機(jī)數(shù)流

      Random類用于創(chuàng)建隨機(jī)數(shù)字序列。從 Java SE 8 開始,已向此類添加了幾個(gè)方法來創(chuàng)建不同類型的隨機(jī)數(shù)流int,long,double

      您可以創(chuàng)建提供種子參數(shù)的Random實(shí)例。此種子是一個(gè)long。隨機(jī)數(shù)取決于該種子。對于給定的種子,您將始終獲得相同的數(shù)字序列。這在許多情況下可能很方便,包括編寫測試。這種情況下,數(shù)字序列可以預(yù)先知道。

      有三種方法可以生成這樣的流,它們都在 Random 類中定義:ints()、longs() doubles()。

      所有這些方法都有幾個(gè)重載可用,它們接受以下參數(shù):

      • 此流將生成的元素?cái)?shù);
      • 生成的隨機(jī)數(shù)的上限和下限。

      下面是生成 10 個(gè)介于 1 和 5 之間的隨機(jī)整數(shù)的第一種代碼模式。

      Random random = new Random(314L);
      List<Integer> randomInts = 
          random.ints(10, 1, 5)
                .boxed()//裝箱
                .collect(Collectors.toList());
      System.out.println("randomInts = " + randomInts);
      

      如果您使用的種子與此示例中使用的種子相同,則控制臺中將具有以下內(nèi)容。

      randomInts = [4, 4, 3, 1, 1, 1, 2, 2, 4, 2]
      

      請注意,我們在專用數(shù)字流中使用了 boxed() 方法,它只是將此流map為等效的包裝器類型流。因此,通過此方法將 IntStream 轉(zhuǎn)換為 Stream<Integer>。

      這是第二種模式。該流的任何元素都是true,概率為 80%。

      Random random = new Random(314L);
      List<Boolean> booleans =
          random.doubles(1_000, 0d, 1d)
                .mapToObj(rand -> rand <= 0.8) // you can tune the probability here
                .collect(Collectors.toList());
      
      // Let us count the number of true in this list
      long numberOfTrue =
          booleans.stream()
                  .filter(b -> b)//b本身就是boolean
                  .count();
      System.out.println("numberOfTrue = " + numberOfTrue);
      

      如果您使用的種子與我們在本示例中使用的種子相同,您將看到以下結(jié)果。

      numberOfTrue = 773
      

      您可以調(diào)整此模式以生成具有所需概率的任何類型的對象。下面是另一個(gè)示例,它生成帶有字母 A、B、C 和 D 的流。每個(gè)字母的概率如下:

      • A的50%;
      • B的30%;
      • C的10%;
      • D的10%。
      Random random = new Random(314L);
      List<String> letters =
          random.doubles(1_000, 0d, 1d)
                .mapToObj(rand ->
                          rand < 0.5 ? "A" : // 50% of A
                          rand < 0.8 ? "B" : // 30% of B
                          rand < 0.9 ? "C" : // 10% of C
                                       "D")  // 10% of D
                .collect(Collectors.toList());
      
      Map<String, Long> map =
          letters.stream()
                  .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));//每個(gè)出現(xiàn)的次數(shù)
      
      map.forEach((letter, number) -> System.out.println(letter + " :: " + number));
      

      使用相同的種子,您將獲得以下結(jié)果。

      A :: 470
      B :: 303
      C :: 117
      D :: 110
      

      此時(shí),使用此 groupingBy() 構(gòu)建map可能看起來不明白。不用擔(dān)心,本教程稍后將介紹。

      從字符串的字符創(chuàng)建流

      String 類在 Java SE 8 中添加了一個(gè) chars() 方法。此方法返回一個(gè) IntStream,該 IntStream 為您提供字符。

      每個(gè)字符都作為一個(gè)整數(shù)給出, ASCII 碼。在某些情況下,您可能需要轉(zhuǎn)換為字符串。

      您有兩種模式可以執(zhí)行此操作,具體取決于您使用的 JDK 版本。

      在 Java SE 10 之前,您可以使用以下代碼。

      String sentence = "Hello Duke";
      List<String> letters =
          sentence.chars()
                  .mapToObj(codePoint -> (char)codePoint)//int => char
                  .map(Object::toString)// char =>String
                  .collect(Collectors.toList());
      System.out.println("letters = " + letters);
      

      在 Java SE 11 的 Character 類中添加了一個(gè) toString() 工廠方法,您可以使用它來簡化此代碼。

      String sentence = "Hello Duke";
      List<String> letters =
          sentence.chars()
                  .mapToObj(Character::toString)//int =>String
                  .collect(Collectors.toList());
      System.out.println("letters = " + letters);
      

      兩個(gè)代碼都打印出以下內(nèi)容。

      letters = [H, e, l, l, o,  , D, u, k, e]
      

      從文本文件的行創(chuàng)建流

      能夠在文本文件上打開流是一種非常強(qiáng)大的模式。

      Java I/O API 有一個(gè)模式,能從文本文件中讀取一行:BufferedReader.readLine()。您可以循環(huán)調(diào)用此方法,逐行讀取整個(gè)文本。

      使用 Stream API 能為你提供更具可讀性和更易于維護(hù)的代碼。

      有幾種模式可以創(chuàng)建這樣的流。

      如果需要基于buffered reader重構(gòu)現(xiàn)有代碼,則可以使用在此對象上定義的lines()方法。如果要編寫新代碼,可以使用工廠方法 Files.lines()。最后一種方法將 Path 作為參數(shù),并具有一個(gè)重載方法,添加 CharSet為參數(shù),以防文件不是以 UTF-8 編碼。

      您可能知道,文件資源與任何 I/O 資源一樣,當(dāng)不再需要時(shí),應(yīng)將其關(guān)閉。

      好消息是Stream接口實(shí)現(xiàn)了AutoCloseable。流本身就是一個(gè)資源,您可以在需要時(shí)關(guān)閉它。上面您看到的所有示例都運(yùn)行在內(nèi)存中,并不需要,但下面情況下肯定是必需的。

      下面是計(jì)算日志文件中警告數(shù)量的示例。

      Path log = Path.of("/tmp/debug.log"); // adjust to fit your installation
      try (Stream<String> lines = Files.lines(log)) {
          
          long warnings = 
              lines.filter(line -> line.contains("WARNING"))
                   .count();
          System.out.println("Number of warnings = " + warnings);
          
      } catch (IOException e) {
          // do something with the exception
      }
      

      try-with-resources模式將調(diào)用流的 close() 方法,該方法將正確關(guān)閉已解析的文本文件。

      從正則表達(dá)式創(chuàng)建流

      這一系列模式的最后一個(gè)示例是添加到 Pattern 類的方法,用于在將正則表達(dá)式應(yīng)用于字符串生成的元素上創(chuàng)建流。

      假設(shè)您需要用給定的分隔符拆分字符串。您有兩種模式來執(zhí)行此操作。

      這兩種模式都為您提供了一個(gè)字符串?dāng)?shù)組,其中包含拆分后的結(jié)果元素。

      上面您看到了從此數(shù)組創(chuàng)建流的模式。讓我們編寫此代碼。

      String sentence = "For there is good news yet to hear and fine things to be seen";
      
      String[] elements = sentence.split(" ");
      Stream<String> stream = Arrays.stream(elements);
      

      Pattern類也有一個(gè)適合你的方法。你可以調(diào)用 Pattern.compile().splitAsStream()。下面是可以使用此方法編寫的代碼。

      String sentence = "For there is good news yet to hear and fine things to be seen";
      
      Pattern pattern = Pattern.compile(" ");
      Stream<String> stream = pattern.splitAsStream(sentence);//
      List<String> words = stream.collect(Collectors.toList());
      
      System.out.println("words = " + words);
      

      運(yùn)行此代碼將生成以下結(jié)果。

      words = [For, there, is, good, news, yet, to, hear, and, fine, things, to, be, seen]
      

      您可能想知道這兩種模式中哪一種最好。要回答這個(gè)問題,您需要仔細(xì)查看下。第一種模式首先,創(chuàng)建一個(gè)數(shù)組來存儲拆分的結(jié)果,然后在此數(shù)組上創(chuàng)建一個(gè)流。

      在第二種模式中沒有創(chuàng)建數(shù)組,因此開銷更少。

      您已經(jīng)看到某些流可能使用短路操作(本教程稍后將詳細(xì)介紹這一點(diǎn))。如果您有這樣的流,拆分整個(gè)字符串并創(chuàng)建生成的數(shù)組可能是一個(gè)重要但無用的開銷。流管道不一定非要使用其所有元素才能生成結(jié)果。

      即使您的流需要使用所有元素,將所有這些元素存儲在數(shù)組中仍然是不必要的。

      因此,使用 splitAsStream() 模式更好。在內(nèi)存和 CPU 方面更好。

      使用builder模式創(chuàng)建流

      使用此模式創(chuàng)建流的過程分為兩個(gè)步驟。首先,在builder中添加流將使用的元素。然后,從此builder創(chuàng)建流。使用builder創(chuàng)建流后,您將無法向其添加更多元素,也無法再次使用它來構(gòu)建另一個(gè)流。如果你這樣做,你會得到一個(gè)IllegalStateException。

      模式如下。

      Stream.Builder<String> builder = Stream.<String>builder();
      
      builder.add("one")
             .add("two")
             .add("three")
             .add("four");
      
      Stream<String> stream = builder.build();//無法再次更改
      
      List<String> list = stream.collect(Collectors.toList());
      System.out.println("list = " + list);
      

      運(yùn)行此代碼將打印以下內(nèi)容。

      list = [one, two, three, four]
      

      在 HTTP 源上創(chuàng)建流

      我們在本教程中介紹的最后一個(gè)模式是關(guān)于分析 HTTP 響應(yīng)的主體。您看到您可以在文本文件的行上創(chuàng)建流,也可以在 HTTP 響應(yīng)的正文上執(zhí)行相同的操作。此模式由添加到 JDK 11 的 HTTP Client API 提供。

      這是它的工作原理。我們將在在線提供的文本中使用它:查爾斯狄更斯的《雙城記》,由古騰堡項(xiàng)目在線提供:https://www.gutenberg.org/files/98/98-0.txt

      文本文件的開頭提供有關(guān)文本本身的信息。這本書的開頭是“A TALE OF TWO CITIES”。文件的末尾是分發(fā)此文件的許可證。

      我們只需要本書的文本,并希望刪除此分布式文件的頁眉和頁腳。

      // The URI of the file
      URI uri = URI.create("https://www.gutenberg.org/files/98/98-0.txt");
      
      // The code to open create an HTTP request
      HttpClient client = HttpClient.newHttpClient();
      HttpRequest request = HttpRequest.newBuilder(uri).build();
      
      
      // The sending of the request
      HttpResponse<Stream<String>> response = client.send(request, HttpResponse.BodyHandlers.ofLines());
      List<String> lines;
      try (Stream<String> stream = response.body()) {
          lines = stream
              .dropWhile(line -> !line.equals("A TALE OF TWO CITIES"))
              .takeWhile(line -> !line.equals("*** END OF THE PROJECT GUTENBERG EBOOK A TALE OF TWO CITIES ***"))
              .collect(Collectors.toList());
      }
      System.out.println("# lines = " + lines.size());
      

      運(yùn)行此代碼將打印出以下內(nèi)容。

      # lines = 15904
      

      流由您提供的body handler創(chuàng)建,作為 send() 方法的參數(shù)。HTTP Client API 為您提供了多個(gè)body handler。上面是由工廠方法 HttpResponse.BodyHandlers.ofLines() 創(chuàng)建的。這種消費(fèi)響應(yīng)主體的方式非常節(jié)省內(nèi)存。如果仔細(xì)編寫流,響應(yīng)的正文將永遠(yuǎn)不會存儲在內(nèi)存中。

      我們這里將所有文本行放在一個(gè)列表中,但是,您不一定需要這樣做。實(shí)際上,大多數(shù)情況下,將此數(shù)據(jù)存儲在內(nèi)存中可能是一個(gè)壞主意。

      reduce流

      reduce流

      到目前為止,您在本教程中了解到,reduce流包括以類似于 SQL 語言中的方式聚合該流的元素。在您運(yùn)行的示例中,您還使用collect(Collectors.toList())模式在列表中收集了您構(gòu)建的流的元素。所有這些操作在Stream API 中稱為末端操作,包括reduce流。

      在流上調(diào)用末端操作時(shí),需要記住兩件事。

      1. 沒有末端操作的流不會處理任何數(shù)據(jù)。如果您在應(yīng)用程序中發(fā)現(xiàn)這樣的流,則很可能是一個(gè)錯(cuò)誤。
      2. 一個(gè)流同時(shí)只能有一個(gè)中繼或末端操作調(diào)用。您不能重復(fù)使用流;如果你嘗試這樣做,你會得到一個(gè)IllegalStateException。

      使用binary operator來reduce流

      在 Stream 接口中定義的 reduce() 方法有三個(gè)重載。它們都接收 BinaryOperator 對象作為參數(shù)。讓我們看看如何使用這個(gè)binary operator。

      讓我們舉個(gè)例子。假設(shè)您有一個(gè)整數(shù)列表,您需要計(jì)算這些整數(shù)的總和。您可以使用經(jīng)典的 for 循環(huán)模式編寫以下代碼來計(jì)算此總和。

      List<Integer> ints = List.of(3, 6, 2, 1);
      
      int sum = ints.get(0);
      for (int index = 1; index < ints.size(); index++) {
          sum += ints.get(index);
      }
      System.out.println("sum = " + sum);
      

      運(yùn)行它會打印出以下結(jié)果。

      sum = 12
      

      此代碼的作用如下。

      1. 將列表中的前兩個(gè)元素相加。
      2. 然后取下一個(gè)元素并將其求和到您計(jì)算的部分總和。
      3. 重復(fù)該過程,直到到達(dá)列表末尾。

      如果仔細(xì)檢查此代碼,可以使用binary operator對 SUM 運(yùn)算符進(jìn)行建模,以獲得相同的結(jié)果。然后,代碼將變?yōu)橐韵聝?nèi)容。

      List<Integer> ints = List.of(3, 6, 2, 1);
      BinaryOperator<Integer> sum = (a, b) -> a + b;//把操作邏輯提取為lambda
      
      int result = ints.get(0);
      for (int index = 1; index < ints.size(); index++) {
          result = sum.apply(result, ints.get(index));
      }
      System.out.println("sum = " + result);
      

      現(xiàn)在您可以看到此代碼僅依賴于binary operator本身。假設(shè)您需要計(jì)算一個(gè) MAX。您需要做的就是為此提供正確的binary operator。

      List<Integer> ints = List.of(3, 6, 2, 1);
      BinaryOperator<Integer> max = (a, b) -> a > b ? a: b;//提供具體函數(shù)即可
      
      int result = ints.get(0);
      for (int index = 1; index < ints.size(); index++) {
          result = max.apply(result, ints.get(index));
      }
      System.out.println("max = " + result);
      

      結(jié)論是,您確實(shí)可以僅提供binary operator來計(jì)算reduce。這就是 reduce() 方法在 Stream API 中的工作方式。

      選擇可以并行使用的binary operator

      不過,您需要了解兩個(gè)注意事項(xiàng)。讓我們在這里介紹第一個(gè)。

      第一個(gè)是可以并行計(jì)算的流。本教程稍后將更詳細(xì)地介紹這一點(diǎn),但現(xiàn)在需要討論它,因?yàn)樗鼘@個(gè)binary operator有影響。數(shù)據(jù)源分為兩部分,每部分單獨(dú)處理。每個(gè)進(jìn)程都與您剛剛看到的進(jìn)程相同,它使用binary operator。然后,在處理每個(gè)部分時(shí),兩個(gè)部分結(jié)果將使用相同的binary operator合并。

      并行處理這個(gè)數(shù)據(jù)流非常簡單:只需在給定流上調(diào)用 parallel() 即可。

      讓我們來看看事情是如何工作的,為此,您可以編寫以下代碼。您只是在模擬如何并行執(zhí)行計(jì)算。當(dāng)然,這是并行流的過度簡化版本,只是為了解釋事情是如何工作的。

      讓我們創(chuàng)建一個(gè) reduce() 方法,該方法接收binary operator并使用它來reduce整數(shù)列表。代碼如下。

      int reduce(List<Integer> ints, BinaryOperator<Integer> sum) {
          int result = ints.get(0);
          for (int index = 1; index < ints.size(); index++) {
              result = sum.apply(result, ints.get(index));
          }
          return result;
      }
      

      下面是使用此方法的主要代碼。

      List<Integer> ints = List.of(3, 6, 2, 1);
      BinaryOperator<Integer> sum = (a, b) -> a + b;//兩個(gè)Integer的具體操作
      
      int result1 = reduce(ints.subList(0, 2), sum);
      int result2 = reduce(ints.subList(2, 4), sum);
      
      int result = sum.apply(result1, result2);
      System.out.println("sum = " + result);
      

      為了讓過程更明顯,我們將您的數(shù)據(jù)源分為兩部分,并將它們分別reduce為兩個(gè)整數(shù):reduce1reduce2 。然后,我們使用相同的binary operator合并了這些結(jié)果。這基本上就是并行流的工作方式。

      這段代碼非常簡化,它只是為了顯示你的binary operator應(yīng)該具有的一個(gè)非常特殊的屬性。拆分流的方式不應(yīng)影響計(jì)算結(jié)果。以下所有拆分都應(yīng)提供相同的結(jié)果:

      • 3 + (6 + 2 + 1)
      • (3 + 6) + (2 + 1)
      • (3 + 6 + 2) + 1

      這表明您的binary operator應(yīng)該具有一個(gè)稱為結(jié)合性 associativity的已知屬性。傳遞給 reduce() 方法的binary operator應(yīng)該是可結(jié)合的。

      Stream API 的 reduce() 重載版本, JavaDoc API 文檔指出,您作為參數(shù)提供的binary operator必須是可結(jié)合的。

      如果不是這樣,會發(fā)生什么?嗯,這正是問題所在:編譯器和 Java 運(yùn)行時(shí)都不會檢測到它。因此,您的數(shù)據(jù)將被處理,沒有明顯的錯(cuò)誤。你可能有正確的結(jié)果,也可能沒有;這取決于內(nèi)部處理數(shù)據(jù)的方式。事實(shí)上,如果你多次運(yùn)行代碼,你最終可能會得到不同的結(jié)果。這是您需要注意的非常重要的一點(diǎn)。

      如何測試binary operator是否可結(jié)合?在某些情況下,這可能非常簡單:SUM,MINMAX是眾所周知的可結(jié)合運(yùn)算符。在其他一些情況下,這可能要困難得多。檢查的一種方法,可以是在隨機(jī)數(shù)據(jù)上運(yùn)行binary operator,并驗(yàn)證是否始終獲得相同的結(jié)果。

      管理具有幺元的binary operator

      第二個(gè)是binary operator這種結(jié)合性產(chǎn)生的結(jié)果。

      此結(jié)合性屬性是由以下事實(shí)保證的:數(shù)據(jù)的拆分方式不應(yīng)影響計(jì)算結(jié)果。如果將集合 A 拆分為兩個(gè)子集 B 和 C,則reduce A 應(yīng)該得到與reduce (B 的reduce和 C 的reduce)相同的結(jié)果。

      可以將前面的屬性寫入更通用的表達(dá)式:

      A = B ? C ? Red(A) = Red(RedB), Red(C))

      事實(shí)證明,這導(dǎo)致了另一個(gè)問題。假設(shè)事情出了意外,B實(shí)際上是空的。這種情況下,C = A。前面的表達(dá)式變?yōu)橐韵聝?nèi)容:

      Red(A) = Red(Red(?), Red(A)) //必須成立才行

      當(dāng)且僅當(dāng)空集 (?) 的reduce是reduce操作的幺元identity element時(shí),才是正確的。

      這是數(shù)據(jù)處理中的一種屬性:空集的reduce是reduce操作的幺元。

      在數(shù)據(jù)處理中這確實(shí)是一個(gè)問題,尤其是在并行處理中,一些非常經(jīng)典的binary operator并沒有幺元,比如 MINMAX??占淖钚≡貨]有定義,因?yàn)?MIN 操作沒有幺元

      此問題必須在Stream API 中解決,因?yàn)槟赡鼙仨毺幚砜樟鳌D吹搅藙?chuàng)建空流的模式,很容易看出 filter() 可以filter掉所有數(shù)據(jù),從而返回空流。

      Stream API 是這樣處理的。幺元未知(不存在或未提供)的reduce將返回 Optional 類的實(shí)例。我們將在本教程后面更詳細(xì)地介紹此類。此時(shí)您需要知道的是,此 Optional 類是一個(gè)可以為空的包裝類。每次對沒有已知幺元的流調(diào)用末端操作時(shí),Stream API 都會將結(jié)果包裝在該對象中。如果處理的流為空,則此Optional對象也將為空,下一步如何處理將由您和您的應(yīng)用程序決定。

      探索Stream API 的reduce方法

      正如我們前面提到的,Stream API 有三個(gè)重載的 reduce() 方法,我們現(xiàn)在可以詳細(xì)介紹這些重載。

      使用幺元進(jìn)行reduce

      第一個(gè)接收幺元和 BinaryOperator 的實(shí)例。由于您提供的第一個(gè)參數(shù)是binary operator的已知幺元,因此具體實(shí)現(xiàn)可能會使用它來簡化計(jì)算。從這個(gè)幺元開始,啟動進(jìn)程,而不是選兩個(gè)元素。使用的算法具有以下形式。

      List<Integer> ints = List.of(3, 6, 2, 1);
      BinaryOperator<Integer> sum = (a, b) -> a + b;
      int identity = 0;
      
      int result = identity;//人為設(shè)定幺元,初始值
      for (int i: ints) {
          result = sum.apply(result, i);
      }
      
      System.out.println("sum = " + result);
      

      你可以注意到,即使你需要處理的列表是空的,這種編寫方式也能很好地工作。這種情況下,它將返回幺元,這是您需要的。

      API 不會檢查您提供的元素確實(shí)是binary operator的幺元這一事實(shí)。提供不對的元素將返回錯(cuò)誤的結(jié)果。

      您可以在以下示例中看到這一點(diǎn)。

      Stream<Integer> ints = Stream.of(0, 0, 0, 0);
      
      int sum = ints.reduce(10, (a, b) -> a + b);//初始值為10
      System.out.println("sum = " + sum);
      

      您希望此代碼在控制臺上打印值 0。因?yàn)?reduce() 方法調(diào)用的第一個(gè)參數(shù)不是binary operator的幺元,所以結(jié)果實(shí)際上是錯(cuò)誤的。運(yùn)行此代碼將在主機(jī)上打印以下內(nèi)容。

      sum = 10
      

      這是您應(yīng)該使用的正確代碼。

      Stream<Integer> ints = Stream.of(0, 0, 0, 0);
      
      int sum = ints.reduce(0, (a, b) -> a + b);//初始值為0
      System.out.println("sum = " + sum);
      

      此示例說明在編譯或運(yùn)行代碼時(shí)傳遞錯(cuò)誤的幺元不會觸發(fā)任何錯(cuò)誤或異常。具體取決于您。

      此屬性的測試方式可以跟測試結(jié)合性相同。將候選幺元與盡可能多的值組合在一起。如果您找到一個(gè)因組合而改變的值,那么您的幺元就不是合適的候選。反之并不成立,如果您找不到任何錯(cuò)誤的組合,并不一定意味著您的候選就是正確的。

      不使用幺元進(jìn)行reduce

      reduce() 方法的第二個(gè)重載接收沒有幺元的 BinaryOperator 實(shí)例作為參數(shù)。正如預(yù)期的那樣,它返回一個(gè) Optional 對象,包裝reduce的結(jié)果。使用Optional做的最簡單的事情,就是打開并查看其中是否有任何東西。

      讓我們舉一個(gè)沒有幺元的reduce示例。

      Stream<Integer> ints = Stream.of(2, 8, 1, 5, 3);
      Optional<Integer> optional = ints.reduce((i1, i2) -> i1 > i2 ? i1: i2);//大于空集沒有意義,可能為null
      
      if (optional.isPresent()) {
          System.out.println("result = " + optional.orElseThrow());
      } else {
          System.out.println("No result could be computed");
      }
      

      運(yùn)行此代碼將產(chǎn)生以下結(jié)果。

      result = 8
      

      請注意,此代碼使用 orElseThrow() 方法打開可選代碼,該方法現(xiàn)在是執(zhí)行此操作的首選方法。此模式已在 Java SE 10 中添加,以取代最初在 Java SE 8 中引入的更傳統(tǒng)的 get() 方法。

      get()方法的問題在于,如果Optional為空,可能會拋出一個(gè)NoSuchElementException。此方法的命名 orElseThrow() 比 get() 更直觀,它提醒您,打開空的Optional您將收到異常。

      使用Optional可以完成更多操作,您將在本教程后面了解。

      在一種方法中組合map和reduce

      第三個(gè)稍微復(fù)雜一些。它使用多個(gè)參數(shù)組合combine了內(nèi)部mapping和reduce。

      讓我們檢查一下此方法的簽名。

      <U> U reduce(U identity,//幺元
                   BiFunction<U, ? super T, U> accumulator,
                   BinaryOperator<U> combiner);//兩個(gè)U類型的具體操作
      

      類型U在本地定義并由binary operator使用。binary operator的工作方式與 reduce() 剛才那個(gè)重載相同,只是它不應(yīng)用于流的元素,而僅應(yīng)用于它們mapping后的版本。

      這種mapping和reduce本身實(shí)際上組合為一個(gè)操作:累加器accumulator。請記住,在本部分的開頭,您看到reduce是逐步進(jìn)行的,并且一次消費(fèi)一個(gè)元素。在每一步,reduce操作的第一個(gè)參數(shù)是到目前為止消費(fèi)的所有元素的reduce部分。

      幺元是combiner的幺元。的確是這樣。

      假設(shè)您有一個(gè) String 實(shí)例流,您需要對所有字符串的長度求和。

      combiner組合了兩個(gè)部分:到目前處理的字符串長度的部分總和,兩個(gè)整數(shù)。

      accumulator從流中獲取一個(gè)元素,將其map為一個(gè)整數(shù)(該字符串的長度),并將其添加到到目前為止計(jì)算的總和中。

      以下是該算法的工作原理。

      Fusing Reduction and Mapping

      相應(yīng)的代碼如下。

      Stream<String> strings = Stream.of("one", "two", "three", "four");
      
      BinaryOperator<Integer> combiner = (length1, length2) -> length1 + length2;//兩個(gè)Integer部分總和,具體操作
      
      //累加mapping操作:部分總和Integer,跟新元素String作運(yùn)算,返回新的部分總和Integer
      BiFunction<Integer, String, Integer> accumulator =
              (partialReduction, element) -> partialReduction + element.length();
      
      int result = strings.reduce(0, accumulator, combiner);//combiner的初始值為0
      System.out.println("sum = " + result);
      

      運(yùn)行此代碼將生成以下結(jié)果。

      sum = 15
      

      在上面的示例中,mapping過程實(shí)際為以下函數(shù)。

      Function<String, Integer> mapper = String::length;
      

      因此,您可以將accumulator重寫為以下模式。這種寫法清楚地顯示了mapping的組合過程。

      Function<String, Integer> mapper = String::length;//mapping
      BinaryOperator<Integer> combiner = (length1, length2) -> length1 + length2;
      
      BiFunction<Integer, String, Integer> accumulator =
              (partialReduction, element) -> partialReduction + mapper.apply(element);
      

      在流上添加末端操作

      避免使用reduce方法

      如果流不以末端操作結(jié)束,則不會處理任何數(shù)據(jù)。我們已經(jīng)介紹了末端操作 reduce(),您在其他示例中看到了幾個(gè)末端操作?,F(xiàn)在讓我們介紹其他幾個(gè)。

      使用 reduce() 方法并不是reduce流的最簡單方法。您需要確保您提供的binary operator是可結(jié)合的,然后您需要知道它是否具有幺元。您需要檢查許多點(diǎn),以確保您的代碼正確并產(chǎn)生您期望的結(jié)果。如果你可以避免使用 reduce() 方法,那么你絕對應(yīng)該這樣做,因?yàn)樗苋菀壮鲥e(cuò)。

      幸運(yùn)的是,Stream API 為您提供了許多其他reduce流的方法:我們在介紹專門的數(shù)字流時(shí)介紹的 sum()、min() 和 max() 是您可以使用的便捷方法。事實(shí)上,你只能吧 reduce() 方法作為最后的手段,只有當(dāng)你沒有其他解決方案時(shí)。

      計(jì)算元素?cái)?shù)量

      count() 方法存在于所有流接口中,包括專用流和對象流。它用long返回該流處理的元素?cái)?shù)。這個(gè)數(shù)字可能很大,實(shí)際上大于 Integer.MAX_VALUE

      您可能想知道為什么需要如此多的數(shù)字。實(shí)際上,您可以從許多源創(chuàng)建流,包括可以生成大量元素的源,大于 Integer.MAX_VALUE。即使不是這種情況,也很容易創(chuàng)建一個(gè)中繼操作,將流處理的元素?cái)?shù)量成倍增加。我們在本教程前面介紹的 flatMap() 方法可以做到這一點(diǎn)。有很多方法可以讓你最終超過 Integer.MAX_VALUE 。這就是 Stream API 支持它的原因。

      下面是 count() 方法的一個(gè)示例。

      Collection<String> strings =
              List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");
      
      long count =
              strings.stream()
                      .filter(s -> s.length() == 3)
                      .count();
      System.out.println("count = " + count);
      

      運(yùn)行此代碼將生成以下結(jié)果。

      count = 4
      

      逐個(gè)消費(fèi)元素

      Stream API 的 forEach() 方法允許您將流的每個(gè)元素傳遞給Consumer接口的實(shí)例。此方法對于打印流處理的元素非常方便。這就是以下代碼的作用。

      Stream<String> strings = Stream.of("one", "two", "three", "four");
      strings.filter(s -> s.length() == 3)
             .map(String::toUpperCase)
             .forEach(System.out::println);
      

      運(yùn)行此代碼將打印以下內(nèi)容。

      ONE
      TWO
      

      這種方法非常簡單,但您可能會用錯(cuò)。

      請記住,您編寫的 lambda 表達(dá)式應(yīng)避免改變其外部作用域。有時(shí),在狀態(tài)外發(fā)生突變稱為傳導(dǎo)副作用。剛才的Consumer很特殊,因?yàn)闆]有什么特別的副作用。實(shí)際上也有,調(diào)用 System.out.println() 會對應(yīng)用程序的控制臺產(chǎn)生副作用。

      讓我們考慮以下示例。

      Stream<String> strings = Stream.of("one", "two", "three", "four");
      List<String> result = new ArrayList<>();
      
      strings.filter(s -> s.length() == 3)
             .map(String::toUpperCase)
             .forEach(result::add);
      
      System.out.println("result = " + result);
      

      運(yùn)行前面的代碼會打印出以下內(nèi)容。

      result = [ONE, TWO]
      

      因此,您可能會想使用此代碼,因?yàn)樗芎唵危夷堋罢9ぷ鳌薄:冒桑@段代碼正在做一些錯(cuò)誤的事情。讓我們來看看它們。

      從流中調(diào)用result::add,將該流處理的所有元素添加到外部result列表中。此Consumer正在對流本身范圍之外的變量產(chǎn)生副作用。

      訪問此類變量會使您的 lambda 表達(dá)式成為捕獲式 lambda 表達(dá)式。創(chuàng)建這樣的 lambda 表達(dá)式雖然完全合法,但會降低性能。如果性能是應(yīng)用程序中的重要問題,則應(yīng)避免編寫捕獲式 lambda。

      此外,這種方式也會阻止此流的并行。實(shí)際上,如果您嘗試使此流并行,您將有多個(gè)線程并行訪問您的result列表。而 ArrayList 并不是并發(fā)安全的類。

      有兩種變通模式收集到列表。下面的示例演示使用集合對象。

      Stream<String> strings = Stream.of("one", "two", "three", "four");
      
      List<String> result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .collect(Collectors.toList());
      

      這段代碼同樣創(chuàng)建 ArrayList 的實(shí)例,并將流處理的元素添加到其中。不會產(chǎn)生任何副作用,因此不會對性能造成影響。

      并行性和并發(fā)性由Collector API 本身處理,因此您可以安全地使此流并行。

      從Java SE 16開始,您有第二種更簡單的模式,使用collector對象。

      Stream<String> strings = Stream.of("one", "two", "three", "four");
      
      List<String> result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .toList();
      

      此模式生成 List 的特殊不可變實(shí)例。如果你需要一個(gè)可變列表,你應(yīng)該使用上一種。另外,它還比在 ArrayList 中收集流的性能更好。這一點(diǎn)將在下一段介紹。

      收集到集合或數(shù)組中

      Stream API 提供了多種將流元素收集到集合中的方法。在上一節(jié)中,您初步了解了其中兩種。讓我們看看其他的。

      在選擇所需的模式之前,您需要問自己幾個(gè)問題。

      • 是否需要構(gòu)建不可變列表?
      • 你對 ArrayList 的實(shí)例感到滿意嗎?或者你更喜歡LinkedList
      • 您是否確切地知道您的流將處理多少個(gè)元素?
      • 您是否需要在精確的、可能是第三方或自制的 List 中收集您的元素?

      Stream API 可以處理所有這些情況。

      在ArrayList中收集

      您已經(jīng)在前面的示例中使用了此模式。它是您可以使用的最簡單的方法,并返回 ArrayList 實(shí)例中的元素。

      下面是這種模式的實(shí)際示例。

      Stream<String> strings = Stream.of("one", "two", "three", "four");
      
      List<String> result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .collect(Collectors.toList());
      

      此模式創(chuàng)建 ArrayList 的簡單實(shí)例,并在其中累積流的元素。如果有太多元素, ArrayList 的內(nèi)部數(shù)組無法存儲它們,則當(dāng)前數(shù)組將被復(fù)制到一個(gè)更大的數(shù)組中,并由GC回收。

      如果你想避免這種情況,并且知道你的流將產(chǎn)生的元素?cái)?shù)量,那么你可以使用 Collectors.toCollection() ,它以supplier作為參數(shù)來創(chuàng)建集合,你將在其中收集處理的元素。以下代碼使用此模式創(chuàng)建初始容量為 10,000 的 ArrayList 實(shí)例。

      Stream<String> strings = ...;
      
      List<String> result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .collect(Collectors.toCollection(() -> new ArrayList<>(10_000)));
      

      在不可變List中收集

      在某些情況下,您需要在不可變列表中累積元素。這聽起來可能自相矛盾,因?yàn)槭占馕吨鴮⒃靥砑拥奖仨毧勺兊娜萜髦?。?shí)際上,這就是Collector API 的工作方式,本教程后面將詳細(xì)介紹。在此累加操作結(jié)束時(shí),Collector API 可以繼續(xù)執(zhí)行最后一個(gè)可選操作,本例中,在返回之前密封這個(gè)列表。

      為此,您只需使用以下模式。

      Stream<String> strings = ...;
      
      List<String> result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .collect(Collectors.toUnmodifiableList()));
      

      在此示例中,result是一個(gè)不可變列表。

      從 Java SE 16 開始,有一種更好的方法可以在不可變列表中收集數(shù)據(jù),這在某些情況下可能更有效。模式如下。

      Stream<String> strings = ...;
      
      List<String> result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .toList();
      

      如何提高效率的?第一種模式是建立在使用collector的基礎(chǔ)上的,首先在普通 ArrayList 中收集元素,然后將其密封,使其在處理完成后不可變。您的代碼看到的只是從此 ArrayList 構(gòu)建的不可變列表。

      如您所知,ArrayList 的實(shí)例是在具有固定大小的內(nèi)部數(shù)組上構(gòu)建的。此列表可能已滿。這種情況下,ArrayList 實(shí)現(xiàn)會檢測到并將其復(fù)制到更大的數(shù)組中。此機(jī)制對使用者是透明的,但會帶來開銷:復(fù)制此數(shù)組需要一些時(shí)間。

      在某些情況下,在消費(fèi)所有流之前,Stream API 可以跟蹤要處理的元素?cái)?shù)量。這種情況下,創(chuàng)建大小合適的內(nèi)部數(shù)組更有效,因?yàn)樗苊饬藢⑿?shù)組到較大數(shù)組的開銷。

      Stream.toList() 方法已添加到 Java SE 16 中。如果您需要的是不可變的列表,那么您應(yīng)該使用此模式。

      在自制List中收集

      如果您需要在自己的列表或 JDK 之外的第三方List中收集數(shù)據(jù),則可以使用 Collectors.toCollection() 模式。用于調(diào)整 ArrayList 初始大小的supplier也可用于構(gòu)建 Collection 的任何實(shí)現(xiàn),包括不屬于 JDK 的實(shí)現(xiàn)。您所需要的只是一個(gè)supplier。在以下示例中,我們提供了一個(gè)supplier來創(chuàng)建 LinkedList 的實(shí)例。

      Stream<String> strings = ...;
      
      List<String> result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .collect(Collectors.toCollection(LinkedList::new));
      

      在Set中收集

      由于 Set 接口是 Collection 接口的擴(kuò)展,因此可以使用 Collectors.toCollection(HashSet::new)Set 實(shí)例中收集數(shù)據(jù)。這很好,但 Collector API 仍然為您提供了一個(gè)更簡潔的模式:Collectors.toSet()。

      Stream<String> strings = ...;
      
      Set<String> result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .collect(Collectors.toSet());
      

      您可能想知道這兩種模式之間是否有任何區(qū)別。答案是肯定的,存在細(xì)微的區(qū)別,您將在本教程后面看到。

      如果你需要的是一個(gè)不可變的集合,Collector API 還有另一種模式:Collectors.toUnmodifiableSet()。

      Stream<String> strings = ...;
      
      Set<String> result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .collect(Collectors.toUnmodifiableSet());
      

      在數(shù)組中收集

      Stream API 也有自己的一組 toArray() 方法重載。其中有兩個(gè)。

      第一個(gè)是普通的 toArray() 方法,它返回Object[] .使用此模式會丟失元素的確切類型。

      第二個(gè)參數(shù)接收 IntFunction<A[]> 類型的參數(shù),按照size返回一個(gè)數(shù)組。乍一看可能很嚇人,但編寫此函數(shù)的實(shí)現(xiàn)實(shí)際上非常容易。如果需要構(gòu)建一個(gè)字符串?dāng)?shù)組,則此函數(shù)的實(shí)現(xiàn)為 String[]::new

      Stream<String> strings = ...;
      
      String[] result = 
          strings.filter(s -> s.length() == 3)
                 .map(String::toUpperCase)
                 .toArray(String[]::new);
      
      System.out.println("result = " + Arrays.toString(result));
      

      運(yùn)行此代碼將生成以下結(jié)果。

      result = [ONE, TWO]
      

      提取流的最大值和最小值

      Stream API 為此提供了幾種方法,具體取決于您當(dāng)前正在使用的流。

      我們已經(jīng)介紹了來自專用數(shù)字流的 max() 和 min() 方法:IntStream、LongStreamDoubleStream。您知道這些操作沒有幺元,因此所有都將返回Optional。

      順便說一下,同樣來自數(shù)字流的 average() 方法也返回一個(gè)Optional對象,因?yàn)?average 操作也沒有幺元。

      Stream 接口也有兩個(gè)方法 max()min(),它們也返回一個(gè)Optional對象。與數(shù)字流的區(qū)別在于,Stream的元素實(shí)際上可以是任何類型的。為了能夠計(jì)算最大值或最小值,實(shí)現(xiàn)需要比較這些對象。這就是您需要為這些方法提供comparator的原因。

      這是 max() 方法的實(shí)際應(yīng)用。

      Stream<String> strings = Stream.of("one", "two", "three", "four");
      String longest =
           strings.max(Comparator.comparing(String::length))//對象Stream
                  .orElseThrow();
      System.out.println("longest = " + longest);
      

      它將打印以下內(nèi)容。

      longest = three
      

      請記住,嘗試打開空的Optional對象會拋出 NoSuchElementException,這是您不希望在應(yīng)用程序中看到的內(nèi)容。僅當(dāng)您的流沒有任何要處理的數(shù)據(jù)時(shí),才會這樣。在這個(gè)簡單的示例中,你有一個(gè)流,它處理多個(gè)字符串,沒有filter操作。此流不會為空,因此您可以安全地打開。

      在流中查找元素

      Stream API 為您提供了兩個(gè)末端操作來查找元素:findFirst()findAny()。這兩個(gè)方法不接受任何參數(shù),并返回流的單個(gè)元素。為了正確處理空流的情況,此元素包裝在Optional對象中。如果流為空,則此Optional也為空。

      了解返回哪個(gè)元素需要您了解流可能是順序的。順序流只是一種流,其中元素的順序很重要,并由Stream API 保存。默認(rèn)情況下,在任何順序源(例如 List 接口的實(shí)現(xiàn))上創(chuàng)建的流本身都是順序的。

      在這樣的流上,稱呼第一個(gè)、第二個(gè)或第三個(gè)元素是有意義的。找到這樣一個(gè)流的第一個(gè)元素也是完全有意義的。

      如果您的流無序,或者如果順序在流處理中丟失了,則查找第一個(gè)元素是無法定義的,并且調(diào)用 findFirst() 實(shí)際上會返回流的任何元素。您將在本教程后面看到有關(guān)順序流的更多詳細(xì)信息。

      請注意,調(diào)用 findFirst() 會在流實(shí)現(xiàn)中觸發(fā)一些檢查,以確保在對該流進(jìn)行排序時(shí)獲得該流的第一個(gè)元素。如果您的流是并行流,這可能代價(jià)很高。在許多情況下,獲取的是不是第一個(gè)元素并無所謂,比如流僅處理單個(gè)元素的情況。在所有這些情況下,您應(yīng)該使用 findAny() 而不是 findFirst()。

      讓我們看看 findFirst() 的實(shí)際效果。

      Collection<String> strings =
              List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");
      
      String first =
          strings.stream()
                 // .unordered()
                 // .parallel()
                 .filter(s -> s.length() == 3)
                 .findFirst()
                 .orElseThrow();
      
      System.out.println("first = " + first);
      

      此流是在 List 的實(shí)例上創(chuàng)建的,這使它成為順序流。請注意,在第一個(gè)版本中注釋了 unordered() 和 parallel() 兩行。

      多次運(yùn)行此代碼將始終得到相同的結(jié)果。

      first = one
      

      unordered() 中繼方法調(diào)用使順序流成為無序流。這種情況下,它沒有任何區(qū)別,因?yàn)槟牧魇前错樞蛱幚淼?。您的?shù)據(jù)是從始終以相同順序遍歷其元素的列表中提取的。出于同樣的原因,將 findFirst() 方法調(diào)用替換為 findAny() 方法調(diào)用也沒有任何區(qū)別。

      可以對此代碼進(jìn)行的第一個(gè)修改是取消注釋 parallel() 方法調(diào)用?,F(xiàn)在,您有一個(gè)并行處理的順序流。多次運(yùn)行此代碼將始終得到相同的結(jié)果:one。這是因?yàn)槟牧魇?em>順序的,因此無論您的流是如何處理的,第一個(gè)元素都是確定的。

      要使此流無,您可以取消注釋 unordered() 方法調(diào)用,或者將(List.of)替換為 Set.of()。在這兩種情況下,使用 findFirst() 終止流將從該并行流返回一個(gè)隨機(jī)元素。并行流的處理方式使其如此。

      您可以在此代碼中進(jìn)行的第二個(gè)修改是將 List.of() 替換為 Set.of()。現(xiàn)在不再是順序的。此外,Set.of() 返回的實(shí)現(xiàn),使得集合元素的遍歷以隨機(jī)順序發(fā)生。多次運(yùn)行此代碼會顯示 findFirst() 和 findAny() 都返回一個(gè)隨機(jī)字符串,即使 unordered() 和 parallel() 都注釋掉。查找無序源的第一個(gè)元素?zé)o法定義,結(jié)果是隨機(jī)的。

      從這些示例中,您可以推斷出在并行流的實(shí)現(xiàn)中采取了一些預(yù)防措施來跟蹤哪個(gè)元素是第一個(gè)。這造成了開銷,因此,只有在確實(shí)需要時(shí)才應(yīng)調(diào)用 findFirst()。

      檢查流的元素是否與Predicate匹配

      在某些情況下,在流中查找元素或未能在流中找到元素可能是您真正需要的。您查找的元素不一定與您的應(yīng)用程序有關(guān);但是否存在非常重要。

      以下代碼將用于檢查給定元素是否存在。

      boolean exists =
          strings.stream()
                 .filter(s -> s.length() == 3)
                 .findFirst()
                 .isPresent();
      

      實(shí)際上,此代碼檢查返回的Optional是否為空。

      上面的模式工作正常,但Stream API 提供了一種更有效的方法。實(shí)際上,構(gòu)建此Optional對象是一種開銷,如果您使用以下三種方法之一,則無需支付該開銷。這三種方法將Predicate作為參數(shù)。

      讓我們看看這些方法的實(shí)際應(yīng)用。

      Collection<String> strings =
          List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");
      
      boolean noBlank  = 
              strings.stream()
                     .allMatch(Predicate.not(String::isBlank));
      boolean oneGT3   = 
              strings.stream()
                     .anyMatch(s -> s.length() == 3);
      boolean allLT10  = 
              strings.stream()
                     .noneMatch(s -> s.length() > 10);
              
      System.out.println("noBlank = " + noBlank);
      System.out.println("oneGT3  = " + oneGT3);
      System.out.println("allLT10 = " + allLT10);
      

      運(yùn)行此代碼將生成以下結(jié)果。

      noBlank = true
      oneGT3  = true
      allLT10 = true
      

      短路流的處理

      您可能已經(jīng)注意到我們在此處介紹的不同末端操作之間的重要差異。

      其中一些需要處理流消費(fèi)的所有數(shù)據(jù)。COUNT、MAX、MIN、AVERAGE操作以及forEach()、toList()或toArray()方法調(diào)用就是這種情況。

      我們介紹的最后一個(gè)末端操作并非如此。一旦找到元素,findFirst()findAny() 方法就會停止處理您的數(shù)據(jù),無論還有多少元素需要處理。anyMatch()、allMatch() 和 noneMatch() 也是如此:它們可能會中斷流的處理并得到結(jié)果,而不必消費(fèi)源所有元素。

      這些方法在Stream API 中稱為短路方法,因?yàn)樗鼈兛梢园肼飞山Y(jié)果,而無需處理流的所有元素。

      在某些情況下,這些最后的方法仍然可能處理所有元素:

      查找流的特征

      流的特征

      Stream API 依賴于一個(gè)特殊的對象,即 Spliterator 接口的實(shí)例。此接口的名稱來源于這樣一個(gè)事實(shí),即Stream API 中spliterator的角色類似于iterator在集合 API 中的角色。此外,由于Stream API 支持并行處理,因此spliterator對象還控制流在處理并行化時(shí),不同 CPU 之間如何拆分其元素。名稱是splititerator的組合。

      詳細(xì)介紹此spliterator對象超出了本教程的范圍。您需要知道的是,此spliterator對象具有流的特征。這些特征不是您經(jīng)常使用到的,但了解它們是什么將幫助您在某些情況下編寫更好、更高效的管道。

      流的特征如下。

      特征 評論
      ORDERED 順序的,處理流元素的順序很重要。
      DISTINCT 去重的,該流處理的元素中沒有重復(fù)出現(xiàn)。
      NONNULL 該流中沒有空元素。
      SORTED 排序的,對該流的元素已經(jīng)進(jìn)行排序。
      SIZED 有數(shù)量的,此流處理的元素?cái)?shù)是已知的。
      SUBSIZED 拆分此流會產(chǎn)生兩個(gè) SIZED 流。

      有兩個(gè)特征,不可變 IMMUTABLE并發(fā)的 CONCURRENT,本教程未介紹。

      每個(gè)流在創(chuàng)建時(shí)都設(shè)置或取消設(shè)置了所有這些特征。

      請記住,可以通過兩種方式創(chuàng)建流。

      1. 您可以從數(shù)據(jù)源創(chuàng)建流,我們介紹了幾種不同的模式。
      2. 每次對現(xiàn)有流調(diào)用中繼操作時(shí),都會創(chuàng)建一個(gè)新流。

      給定流的特征取決于創(chuàng)建它的源,或者創(chuàng)建它的流的特征,以及創(chuàng)建的操作。如果您的流是使用源創(chuàng)建的,則其特征取決于該源,如果您使用另一個(gè)流創(chuàng)建它,則它們將取決于該其他流以及您正在使用的操作類型。

      讓我們更詳細(xì)地介紹每個(gè)特征。

      ORDERED流

      順序流是使用順序數(shù)據(jù)源創(chuàng)建的。可能想到的第一個(gè)示例是 List 接口的任何實(shí)例。還有其他的:Files.lines(pathPattern.splitAsStream(string) 也生成 ORDERED 流。

      跟蹤流元素的順序可能會導(dǎo)致并行流的開銷。如果不需要此特性,則可以通過在現(xiàn)有流上調(diào)用 unordered() 中繼方法來刪除它。這將返回沒有此特征的新流。你為什么要這樣做?在某些情況下,保持流 ORDERED 可能會很昂貴,例如,當(dāng)您使用并行流時(shí)。

      SORTED流

      SORTED的流是已排序的流??梢詮囊雅判虻脑矗ㄈ?TreeSet 實(shí)例)或通過調(diào)用 sorted() 方法創(chuàng)建此流。知道流已被排序可能會被流的某些實(shí)現(xiàn)拿來用,以避免再次進(jìn)行排序。但排序后的順序可能會變,因?yàn)?SORTED 流可能會使用與第一次不同的comparator再次排序。

      有一些中繼操作可以清除 SORTED 特征。在下面的代碼中,您可以看到strings,filteredStream兩者都是 SORTED 流,而lengths不是。

      Collection<String> stringCollection = List.of("one", "two", "two", "three", "four", "five");
      
      Stream<String> strings = stringCollection.stream().sorted();
      Stream<String> filteredStrings = strings.filtered(s -> s.length() < 5);
      Stream<Integer> lengths = filteredStrings.map(String::length);
      

      mapping或flatmapping SORTED 流會從生成的流中刪除此特征。

      DISTINCT流

      DISTINCT 流是它正在處理的元素之間沒有重復(fù)項(xiàng)的流。例如,當(dāng)從 HashSet 構(gòu)建流時(shí),或者從對 distinct() 中繼方法調(diào)用的調(diào)用中構(gòu)建流時(shí),可以獲得這樣的特征。

      DISTINCT 特征在filtering流時(shí)保留,但在mapping或flatmapping流時(shí)丟失。

      讓我們檢查以下示例。

      Collection<String> stringCollection = List.of("one", "two", "two", "three", "four", "five");
      
      Stream<String> strings = stringCollection.stream().distinct();
      Stream<String> filteredStrings = strings.filtered(s -> s.length() < 5);
      Stream<Integer> lengths = filteredStrings.map(String::length);
      
      • stringCollection.stream() 不是 DISTINCT 的,因?yàn)樗菑?List 的實(shí)例構(gòu)建的。
      • stringsDISTINCT 的,因?yàn)榇肆魇峭ㄟ^調(diào)用 distinct() 中繼方法創(chuàng)建的。
      • filteredStrings仍然是 DISTINCT:從流中刪除元素不會創(chuàng)造重復(fù)項(xiàng)。
      • length已被map,因此 DISTINCT 特征丟失。

      NONNULL 流

      非空流是不包含null值的流。集合框架中的一些結(jié)構(gòu)不接受空值,包括 ArrayDeque 和并發(fā)結(jié)構(gòu),如 ArrayBlockingQueue、ConcurrentSkipListSet 和調(diào)用 ConcurrentHashMap.newKeySet() 返回的并發(fā)Set。使用 Files.lines(pathPattern.splitAsStream(line) 創(chuàng)建的流也是非流。

      至于前面的特征,一些中繼操作可以產(chǎn)生具有不同特征的流。

      • filtering或排序非空流將返回非流。
      • 在 NONNULL 流上調(diào)用 distinct() 也會返回一個(gè) NONNULL 流。
      • mapping或flatmapping NONNULL 流將返回沒有此特征的流。

      SIZED和SUBSIZED流

      SIZED流

      當(dāng)您想要使用并行流時(shí),最后一個(gè)特征非常重要。本教程稍后將更詳細(xì)地介紹并行流。

      SIZED 流是知道它將處理多少個(gè)元素的流。從 Collection 的任何實(shí)例創(chuàng)建的流都是這樣的流,因?yàn)?Collection 接口具有 size() 方法,因此獲取此數(shù)字很容易。

      另一方面,在某些情況下,您知道流的元素是有限數(shù)的,但除非您處理流本身,否則您無法知道此數(shù)量。

      對于使用 Files.lines(path) 模式創(chuàng)建的流,情況就是如此。您可以獲取文本文件的大?。ㄒ宰止?jié)為單位),但此信息不會告訴您此文本文件有多少行。您需要分析文件以獲取此信息。

      Pattern.splitAsStream(line) 模式也是。知道您正在分析的字符串中的字符數(shù)并不能給出任何關(guān)于此模式將產(chǎn)生多少元素的提示。

      SUBSIZED流

      SUBSIZED 特征,與并行流的拆分方式有關(guān)。簡單說,并行化機(jī)制將流分成兩部分,并在 CPU 正在執(zhí)行的不同可用內(nèi)核之間分配計(jì)算。此拆分由流使用的 Spliterator 實(shí)例實(shí)現(xiàn)。具體實(shí)現(xiàn)取決于您使用的數(shù)據(jù)源。

      假設(shè)您需要在 ArrayList 上打開一個(gè)流。此列表的所有數(shù)據(jù)都保存在 ArrayList 實(shí)例的內(nèi)部數(shù)組中。也許您還記得 ArrayList 對象上的內(nèi)部數(shù)組是一個(gè)緊湊數(shù)組,每當(dāng)數(shù)組中刪除元素時(shí),所有后續(xù)元素都會向左移動一個(gè)單元格,不會留下任何空位。

      這使得拆分 ArrayList 變得簡單明了。要拆分 ArrayList 的實(shí)例,您可以將此內(nèi)部數(shù)組拆分為兩部分,兩部分中的元素?cái)?shù)量相同。這使得在 ArrayList 實(shí)例上創(chuàng)建的流具有 SUBSIZED特性:您甚至可以設(shè)定拆分后每個(gè)部分中將保留多少個(gè)元素。

      假設(shè)現(xiàn)在您需要在 HashSet 實(shí)例上打開一個(gè)流。HashSet 將其元素存儲在數(shù)組中,但此數(shù)組的使用方式與 ArrayList 使用的數(shù)組不同。實(shí)際上,多個(gè)元素可以存儲在此數(shù)組的一個(gè)單元格中。拆分這個(gè)數(shù)組沒有問題,但是如果不計(jì)算一下,就無法提前知道每個(gè)部分中將保留多少個(gè)元素。即使你把這個(gè)數(shù)組從中間分開,也無法保證兩半的元素?cái)?shù)量就是相同。這就是為什么在 HashSet 實(shí)例上創(chuàng)建的流是 SIZED而不是 SUBSIZED

      map流可能會更改返回流的 SIZEDSUBSIZED 特征。

      • mapping和排序流會保留 SIZEDSUBSIZED特征。
      • flatmapping、filtering和調(diào)用 distinct() 會擦除這些特征。

      最好用有 SIZEDSUBSIZED 的流進(jìn)行并行計(jì)算。

      posted on 2023-07-02 17:20  研發(fā)軟件的郭  閱讀(334)  評論(0)    收藏  舉報(bào)

      導(dǎo)航

      主站蜘蛛池模板: 国产精品天天看天天狠| 十八禁午夜福利免费网站| 亚洲人成网网址在线看| 福利一区二区不卡国产| 中文字幕国产精品专区| 国产成人精品午夜福利在线观看| 日韩亚洲精品国产第二页| 绯色蜜臀av一区二区不卡| 丁香花成人电影| 亚洲一区二区乱码精品| 亚洲码和欧洲码一二三四| 国产福利在线观看免费第一福利| 蓬安县| 久久自己只精产国品| 久久久WWW成人免费精品| 四虎影视一区二区精品| 亚洲宅男精品一区在线观看| 国产精品一区二区三区性色| 久热这里只有精品12| 亚洲狠狠婷婷综合久久久久图片 | 狠狠色丁香婷婷综合久久来来去| 男女激情一区二区三区| 91久久夜色精品国产网站| 亚洲成亚洲成网| 国产成人8X人网站视频| 特黄aaaaaaaaa毛片免费视频| 亚洲综合精品香蕉久久网| 亚洲高清日韩专区精品| 一本色道久久综合亚洲精品| 久久av中文字幕资源网| 中文字幕亚洲综合第一页| 最新午夜男女福利片视频| 免费现黄频在线观看国产| 久久综合老鸭窝色综合久久| 一区二区三区av天堂| 少妇做爰免费视看片| 国产不卡的一区二区三区| 草裙社区精品视频播放| 亚洲中文字幕日产无码成人片| 国产国产成人精品久久蜜| 大伊香蕉精品一区二区|