非常好用的三款Java Stream API擴展庫


前言

在Java8中引入的流API可能仍然是Java在過去幾年中包含的最重要的新特性。我認為每個Java開發人員在其職業生涯中都有機會使用JAVA STRAM API。或者我更願意說,你可能每天都在使用它。但是,如果將函數式編程的內置特性與其他一些語言(例如Kotlin)進行比較,您會很快意識到streamapi提供的方法數量非常有限。因此,社區創建了幾個庫,這些庫僅用於擴展純Java提供的API。今天,我將展示三個流行庫提供的最有趣的Java流API擴展:StreamEx、jOOλ 還有Guava。

依賴

下面是本文中比較的三個庫的當前版本列表。

<dependencies>
   <dependency>
      <groupId>one.util</groupId>
      <artifactId>streamex</artifactId>
      <version>0.7.0</version>
   </dependency>
   <dependency>
      <groupId>org.jooq</groupId>
      <artifactId>jool</artifactId>
      <version>0.9.13</version>
   </dependency>
   <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>28.1-jre</version>
   </dependency>
</dependencies>

使用Java Stream擴展壓縮流

在更高級的應用程序中使用Java Streams時,您通常會處理多個流。而且它們通常可以包含不同的對象。在這種情況下,有用的操作之一就是壓縮。壓縮操作將返回一個流,該流在給定的兩個流中包含一對對應的元素,這意味着它們在這些流中的相同位置。讓我們考慮兩個對象Person和PersonAddress。假設我們有兩個流,第一個流僅包含Person對象,第二個帶PersonAddress對象,並且元素的順序清楚地表明了它們的關聯,我們可以壓縮它們以創建一個新的對象流,其中包含來自Person和的所有字段PersonAddress。這是說明所描述方案的屏幕。

合並壓縮
合並壓縮
  • guava使用

    當前描述的所有三個庫均支持壓縮。讓我們從Guava示例開始。它提供了唯一專用於zip的zip方法-靜態方法,該方法具有三個參數:第一流,第二流和映射功能。

    Stream<Person> s1 = Stream.of(
       new Person(1, "John""Smith"),
       new Person(2, "Tom""Hamilton"),
       new Person(3, "Paul""Walker")
    );
    Stream<PersonAddress> s2 = Stream.of(
       new PersonAddress(1, "London""Street1""100"),
       new PersonAddress(2, "Manchester""Street1""101"),
       new PersonAddress(3, "London""Street2""200")
    );
    Stream<PersonDTO> s3 = Streams.zip(s1, s2, (p, pa) -> PersonDTO.builder()
       .id(p.getId())
       .firstName(p.getFirstName())
       .lastName(p.getLastName())
       .city(pa.getCity())
       .street(pa.getStreet())
       .houseNo(pa.getHouseNo()).build());
    s3.forEach(dto -> {
       Assertions.assertNotNull(dto.getId());
       Assertions.assertNotNull(dto.getFirstName());
       Assertions.assertNotNull(dto.getCity());
    });
  • StreamEx

    StreamEx提供比guava上更多的可能性。我們可以在給定的流上調用某些靜態方法或非靜態方法。讓我們看一下如何使用StreamExzipWith方法執行它。

    StreamEx<Person> s1 = StreamEx.of(
       new Person(1, "John""Smith"),
       new Person(2, "Tom""Hamilton"),
       new Person(3, "Paul""Walker")
    );
    StreamEx<PersonAddress> s2 = StreamEx.of(
       new PersonAddress(1, "London""Street1""100"),
       new PersonAddress(2, "Manchester""Street1""101"),
       new PersonAddress(3, "London""Street2""200")
    );
    StreamEx<PersonDTO> s3 = s1.zipWith(s2, (p, pa) -> PersonDTO.builder()
       .id(p.getId())
       .firstName(p.getFirstName())
       .lastName(p.getLastName())
       .city(pa.getCity())
       .street(pa.getStreet())
       .houseNo(pa.getHouseNo()).build());
    s3.forEach(dto -> {
       Assertions.assertNotNull(dto.getId());
       Assertions.assertNotNull(dto.getFirstName());
       Assertions.assertNotNull(dto.getCity());
    });
  • jOOλ 使用

    這個例子幾乎是相同的。我們有一個zip在給定流上調用的方法。

    Seq<Person> s1 = Seq.of(
    new Person(1, "John""Smith"),
    new Person(2, "Tom""Hamilton"),
    new Person(3, "Paul""Walker"));
    Seq<PersonAddress> s2 = Seq.of(
       new PersonAddress(1, "London""Street1""100"),
       new PersonAddress(2, "Manchester""Street1""101"),
       new PersonAddress(3, "London""Street2""200"));
    Seq<PersonDTO> s3 = s1.zip(s2, (p, pa) -> PersonDTO.builder()
       .id(p.getId())
       .firstName(p.getFirstName())
       .lastName(p.getLastName())
       .city(pa.getCity())
       .street(pa.getStreet())
       .houseNo(pa.getHouseNo()).build());
    s3.forEach(dto -> {
       Assertions.assertNotNull(dto.getId());
       Assertions.assertNotNull(dto.getFirstName());
       Assertions.assertNotNull(dto.getCity());
    });

Join擴展

壓縮操作根據兩個不同流中的元素在這些流中的順序來合並它們。如果我們想根據元素的字段(例如id)而不是流中的順序關聯元素,該怎么辦。兩個實體之間的LEFT JOIN或RIGHT JOIN之類的東西。操作的結果應與上一部分相同–包含來自Person和的所有字段的新對象流PersonAddress。下圖說明了所描述的操作。

當涉及聯接操作時,只有jOOλ提供了一些方法。由於它專用於面向對象的查詢,因此我們可以在許多聯接選項之間進行選擇。例如有innerJoin,leftOuterJoin,rightOuterJoin和crossJoin方法。在下面可見的源代碼中,您可以看到一個示例innerJoin用法。此方法采用兩個參數:要加入的流和謂詞以匹配來自第一個流和加入流的元素。如果要基於innerJoin結果創建新對象,則應另外調用mapoperation。

Seq<Person> s1 = Seq.of(
      new Person(1, "John""Smith"),
      new Person(2, "Tom""Hamilton"),
      new Person(3, "Paul""Walker"));
Seq<PersonAddress> s2 = Seq.of(
      new PersonAddress(2, "London""Street1""100"),
      new PersonAddress(3, "Manchester""Street1""101"),
      new PersonAddress(1, "London""Street2""200"));
Seq<PersonDTO> s3 = s1.innerJoin(s2, (p, pa) -> p.getId().equals(pa.getId())).map(t -> PersonDTO.builder()
      .id(t.v1.getId())
      .firstName(t.v1.getFirstName())
      .lastName(t.v1.getLastName())
      .city(t.v2.getCity())
      .street(t.v2.getStreet())
      .houseNo(t.v2.getHouseNo()).build());
s3.forEach(dto -> {
   Assertions.assertNotNull(dto.getId());
   Assertions.assertNotNull(dto.getFirstName());
   Assertions.assertNotNull(dto.getCity());
});

Stream擴展分組

Java流API僅通過Java.util.Stream.Collectors中的靜態方法groupingBy支持的下一個有用操作是分組(s1.collect(Collectors.groupingBy(PersonDTO::getCity)))。在流上執行這樣一個操作的結果是,您得到一個帶有鍵的映射,這些鍵是將分組函數應用於輸入元素后得到的值,其對應的值是包含輸入元素的列表。這個操作是某種聚合,因此得到java.util.List,結果是沒有java.util.stream.stream。 StreamEx和jOOλ 提供一些分組流的方法。讓我們從StreamEx groupingBy操作示例開始。假設我們有一個PersonDTO對象的輸入流,我們將按個人的家鄉城市對它們進行分組。

  • StreamEx 使用

    StreamEx<PersonDTO> s1 = StreamEx.of(
       PersonDTO.builder().id(1).firstName("John").lastName("Smith").city("London").street("Street1").houseNo("100").build(),
       PersonDTO.builder().id(2).firstName("Tom").lastName("Hamilton").city("Manchester").street("Street1").houseNo("101").build(),
       PersonDTO.builder().id(3).firstName("Paul").lastName("Walker").city("London").street("Street2").houseNo("200").build(),
       PersonDTO.builder().id(4).firstName("Joan").lastName("Collins").city("Manchester").street("Street2").houseNo("201").build()
    );
    Map<String, List<PersonDTO>> m = s1.groupingBy(PersonDTO::getCity);
    Assertions.assertNotNull(m.get("London"));
    Assertions.assertTrue(m.get("London").size() == 2);
    Assertions.assertNotNull(m.get("Manchester"));
    Assertions.assertTrue(m.get("Manchester").size() == 2);
  • jOOλ 使用

    Seq<PersonDTO> s1 = Seq.of(
          PersonDTO.builder().id(1).firstName("John").lastName("Smith").city("London").street("Street1").houseNo("100").build(),
          PersonDTO.builder().id(2).firstName("Tom").lastName("Hamilton").city("Manchester").street("Street1").houseNo("101").build(),
          PersonDTO.builder().id(3).firstName("Paul").lastName("Walker").city("London").street("Street2").houseNo("200").build(),
          PersonDTO.builder().id(4).firstName("Joan").lastName("Collins").city("Manchester").street("Street2").houseNo("201").build()
    );
    Map<String, List<PersonDTO>> m = s1.groupBy(PersonDTO::getCity);
    Assertions.assertNotNull(m.get("London"));
    Assertions.assertTrue(m.get("London").size() == 2);
    Assertions.assertNotNull(m.get("Manchester"));
    Assertions.assertTrue(m.get("Manchester").size() == 2);

多重串聯

這是一個非常簡單的場景。javastreamapi提供了一種用於連接的靜態方法,但只適用於兩個流。有時在一個步驟中濃縮多個流是很方便的。guava跟jOOλ為此提供專用方法。

  • 下面是用jOOλ調用concat方法的示例

    Seq<Integer> s1 = Seq.of(1, 2, 3);
    Seq<Integer> s2 = Seq.of(4, 5, 6);
    Seq<Integer> s3 = Seq.of(7, 8, 9);
    Seq<Integer> s4 = Seq.concat(s1, s2, s3);
    Assertions.assertEquals(9, s4.count());
  • guava方法的示例

    Stream<Integer> s1 = Stream.of(1, 2, 3);
    Stream<Integer> s2 = Stream.of(4, 5, 6);
    Stream<Integer> s3 = Stream.of(7, 8, 9);
    Stream<Integer> s4 = Streams.concat(s1, s2, s3);
    Assertions.assertEquals(9, s4.count());

分區擴展

分區操作與分組非常相似,但將輸入流分為兩個列表或流,其中第一個列表中的元素滿足給定的謂詞,而第二個列表中的元素則不滿足。

  • StreamEx partitioning by方法將返回地圖中的兩個列表對象。

    StreamEx<PersonDTO> s1 = StreamEx.of(
          PersonDTO.builder().id(1).firstName("John").lastName("Smith").city("London").street("Street1").houseNo("100").build(),
          PersonDTO.builder().id(2).firstName("Tom").lastName("Hamilton").city("Manchester").street("Street1").houseNo("101").build(),
          PersonDTO.builder().id(3).firstName("Paul").lastName("Walker").city("London").street("Street2").houseNo("200").build(),
          PersonDTO.builder().id(4).firstName("Joan").lastName("Collins").city("Manchester").street("Street2").houseNo("201").build()
    );
    Map<Boolean, List<PersonDTO>> m = s1.partitioningBy(dto -> dto.getStreet().equals("Street1"));
    Assertions.assertTrue(m.get(true).size() == 2);
    Assertions.assertTrue(m.get(false).size() == 2);
  • 與StreamEx相反,jOOλ 在Tuple2對象內返回兩個流(Seq)。與StreamEx相比,這種方法有一個很大的優勢—您仍然可以在不進行任何轉換的情況下對結果調用流操作。

     Seq<PersonDTO> s1 = Seq.of(
         PersonDTO.builder().id(1).firstName("John").lastName("Smith").city("London").street("Street1").houseNo("100").build(),
         PersonDTO.builder().id(2).firstName("Tom").lastName("Hamilton").city("Manchester").street("Street1").houseNo("101").build(),
         PersonDTO.builder().id(3).firstName("Paul").lastName("Walker").city("London").street("Street2").houseNo("200").build(),
         PersonDTO.builder().id(4).firstName("Joan").lastName("Collins").city("Manchester").street("Street2").houseNo("201").build());
     Tuple2<Seq<PersonDTO>, Seq<PersonDTO>> t = s1.partition(dto -> dto.getStreet().equals("Street1"));
     Assertions.assertTrue(t.v1.count() == 2);
     Assertions.assertTrue(t.v2.count() == 2);

匯總擴展

只有jOOλ提供一些流聚合方法。例如,我們可以計算總和、平均值或中位數。自從喬λ是jOOQ的一部分,它的目標是用於面向對象的查詢,實際上,它提供了許多與sqlselect子句相對應的操作。

下面可見的源代碼片段以所有人的年齡為例,說明了我們如何輕松地計算對象流中選定字段的總和。

Seq<Person> s1 = Seq.of(
   new Person(1, "John""Smith", 35),
   new Person(2, "Tom""Hamilton", 45),
   new Person(3, "Paul""Walker", 20)
);
Optional<Integer> sum = s1.sum(Person::getAge);
Assertions.assertEquals(100, sum.get());

配對擴展

StreamEx允許您處理流中的相鄰對象對,並對它們應用給定的函數。它可以通過使用pairMap函數來實現。在下面可見的代碼片段中,我計算流中每對相鄰數字的和。

StreamEx<Integer> s1 = StreamEx.of(1, 2, 1, 2, 1);
StreamEx<Integer> s2 = s1.pairMap(Integer::sum);
s2.forEach(i -> Assertions.assertEquals(3, i));

END

歡迎關注公眾號! 公眾號回復:入群 ,掃碼加入我們交流群! 掃碼關注公眾號獲取更多學習資料

閱讀更多文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM