Stream流的基本介紹以及在工作中的常用操作(去重、排序以及數學運算等)

宋影發表於2022-01-30


平時工作中,我在處理集合的時候,總是會用到各種流操作,但是往往在處理一些較為複雜的集合時,還是會出現無法靈活運用api的場景,這篇文章的目的,主要是為介紹一些工作中使用流時的常用操作,例如去重、排序和數學運算等內容,並不對流的原理和各種高階api做深度剖析,讓我們開始吧~

如果讀者你已經對流有一些基本的瞭解,現在只是有些場景運用到流,不知道如何使用,請劃到文章的最後一個部分-常用操作,希望能夠幫助到你。^^

 

一、流的組成

往往我們使用流的時候,都會經過3步,如下圖所示,首先我們建立一個流,然後對流進行一系列的中間操作,最後執行一個終端操作,這個流就到此結束了。

  1. 建立流:有且建立一次即可。

  2. 中間操作0個,1個及多個均可,可以進行鏈式操作。

  3. 終端操作:一條語句中有且只存在1個,一旦進行該操作,代表該流已結束。

我們需要關注的,實際上是對流的中間操作和終端操作。

二、舉例物件

例子:現在我們多個使用者,抽象成List<User>,該使用者有ID,名稱,年齡,錢以及擁有多個賬戶。

@Data
public class User{
   private Integer id;
   private String name;
   private int age;
   private BigDecimal money;
   private List<Account> accounts;
}

// 操作
List<User> users = new ArrayList<>();

 

三、建立流

3.1 Collection集合

序列流執行緒安全,保證順序;並行流執行緒不安全,不保證順序,但是快。

// 序列流
Stream<User> stream = users.stream();
// 並行流
Stream<User> stream = users.parallelStream();

3.2 陣列

Stream.of()方法底層仍然用得是Arrays.stream()。

String[] userNameArray = {"mary", "jack", "tom"};

// 方法1
Stream<String> stream = Arrays.stream(userNameArray);
// 方法2
Stream<String> stream = Stream.of(userNameArray);

3.3 多個元素

Stream.of()方法可接收可變引數,T... values。

Stream<String> stream = Stream.of("mary", "jack", "tom");

3.4 特殊型別流

處理原始型別int、double、long

IntStream intStream = IntStream.of(1, 2, 3);

 

四、中間操作

4.1 對映和消費

map():可將集合中的元素對映成其他元素。例如 List<User> -> List<String>

flatmap():將對映後的元素放入新的流中,可將集合中元素的某個集合屬性扁平化。例如List<List<Account>> -> List<Account>

peek:對集合中的元素進行一些操作,不對映。例如List<User> -> List<User>

// map 
List<String> userNames = users.stream().map(User::getName).collect(Collectors.toList());
// flatmap
List<Account> accounts = users.stream().map(User::getAccounts).flatMap(Collection::stream).collect(Collectors.toList());
// peek
List<User> newUsers = users.stream().peek(user -> user.setName("Jane")).collect(Collectors.toList());

 

4.2 過濾和去重

filter()保留符合條件的所有元素。

distinct():根據hashCode()和equals方法進行去重。

skip(n):跳過前n個元素。

limit(n):獲取前n個元素

// filter(常用)
List<User> newUsers = users.stream().filter(user -> user.getAge() > 15).collect(Collectors.toList());
// distinct
List<User> newUsers = users.stream().distinct().collect(Collectors.toList());
// limit
List<User> newUsers = users.stream().skip(2).collect(Collectors.toList());
// skip
List<User> newUsers = users.stream().limit(2).collect(Collectors.toList());

 

五、終端操作

5.1 收集

5.1.1 collect()

collect():將流中的元素收整合新的物件,例如List, Set, Map等,這個方法有兩種引數,我們常用的是第一種,利用Collectors工具類來獲取Collector物件,第二種在實際工作中用得少,本文便不介紹,讀者有興趣可去自行了解。:p

  • collect(Collector):(常用)

  • collect(Supplier, BiConsumer, BiConsumer)

收集
// list
List<User> newUsers = users.stream().collect(Collectors.toList());
// set
Set<User> newUsers = users.stream().collect(Collectors.toSet());
// map
// toMap():
// 第一個引數是map的key;
// 第二個引數是map的value(Function.identity()代表取自身的值);
// 第三個引數是key相同時的操作(本行代表key相同時,後面的value覆蓋前面的value)
Map<Integer, User> map = users.stream().collect(Collectors.toMap(User::getId, Function.identity(), (v1, v2) -> v1));
分組
// 根據物件中某個欄位分組
Map<Integer, List<User>> map = users.stream().collect(Collectors.groupingBy(User::getId));
// 根據物件中某個欄位分組後,再根據另外一個欄位分組
Map<Integer, Map<String, List<User>>> map = users.stream().collect(Collectors.groupingBy(User::getId, Collectors.groupingBy(User::getName)));
拼接
// 拼接,比如"hello", "world" -> "hello,world"
String str = users.stream().map(User::getName).collect(Collectors.joining(","));

5.1.2 toArray()

toArray():將List的流收整合陣列Array。

// 可利用String[]::new來指定型別
String[] userNames = users.stream().map(User::getName).toArray(String[]::new);

 

5.2 斷言

allMatch():所有元素符合條件則返回true,否則返回false。 noneMatch():所有元素都不符合條件則返回true,否則返回false。 anyMatch():存在元素符合條件則返回true,否則返回false。

// 是否所有的使用者年齡都大於15
boolean allMatch = users.stream().allMatch(user -> user.getAge() > 15);
// 是否所有的使用者年齡都不大於15
boolean noneMatch = users.stream().noneMatch(user -> user.getAge() > 15);
// 是否存在使用者年齡大於15
boolean anyMatch = users.stream().anyMatch(user -> user.getAge() > 15);

 

5.3 規約

reduce():可以將流的元素組合成一個新的結果。

這個API,我在實際工作中用得很少……可能在計算BigDecimal之和的時候才會用到: BigDecimal sum = users.stream().map(User::getMoney).reduce(Bigdecimal.ZERO, BigDecimal::add);

// 指定初始值:
// 相當於new User(1 + users中所有的ID之和,"1", 0, 0)
User user1 = users.stream().reduce(new User(1, "1", 0, 0), (u1, u2) -> {
   u1.setId(u1.getId() + u2.getId());
   return u1;
});
// 不指定初始值:
// 相當於new User(users中所有的ID之和,"1", 0, 0)
User user2 = users.stream().reduce((u1, u2) -> {
   u1.setId(u1.getId() + u2.getId());
   return u1;
}).orElse(null);

 

5.4 過濾

findAny():返回流中任意一個元素,如果流為空,返回空的Optional。

findFirst():返回流中第一個元素,如果流為空,返回空的Optional。

並行流,findAny會更快,但是可能每次返回結果不一樣。

// findAny()
Optional<User> optional = users.stream().findAny();
// findFirst
Optional<User> optional = users.stream().findFirst();

// 建議先用isPresent判空,再get。
User user = optional.get();

 

六、常用操作

6.1 扁平化

我們想要換取 所有使用者 的 所有賬號 ,比如List<Account>,可以使用flatMap來實現。

兩種方法獲取結果一模一樣。

// 方法1:
List<Account> accounts = users.stream()
      .flatMap(user -> user.getAccounts().stream())
      .collect(Collectors.toList());
// 方法2:
List<Account> accounts = users.stream()
      .map(User::getAccounts)
      .flatMap(Collection::stream)
      .collect(Collectors.toList());

 

6.2 流的邏輯複用

實際工作中,我們可能存在對一個集合多次中間操作後,經過不同的終端操作產生不同的結果這一需求。這個時候,我們就產生想要流能夠複用的想法,但是實際上當一個流呼叫終端操作後,該流就會被關閉,如果關閉後我們再一次呼叫終端操作,則會產生stream has already been operated upon or closed這個Exception,我們無奈之下,只好把相同的邏輯,重複再寫一遍……

如果想使得流邏輯複用,我們可以用Supplier介面把流包裝起來,這樣就可以實現啦。

不過要注意一點,並不是流複用,而是產生流的邏輯複用,其實還是生成了多個流。

比如我們想要15歲以上的:(1)所有使用者集合;(2)根據ID分組後的集合。

// 1. 複用的邏輯
Supplier<Stream<User>> supplier = () -> users.stream().filter(user -> user.getAge() > 15);

// 2.1 所有使用者集合
List<User> list = supplier.get().collect(Collectors.toList());
// 2.2 根據ID分組後的集合
Map<Integer, List<User>> map = supplier.get().collect(Collectors.groupingBy(User::getId));

 

6.3 排序

根據基礎型別和String型別排序:

比如List<Integer>List<String>集合,可使用sorted()排序, 預設升序。

注意:例如"123",字串型別的數字不可直接比較,因為它是根據ASCII碼值來比較排序的。

// 升序 {3, 2, 4} -> {2, 3, 4}
List<Integer> newList = list.stream().sorted().collect(Collectors.toList());
// 降序 {3, 2, 4} -> {4, 2, 3}
List<Integer> newList = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());

根據物件中某個欄位排序:

根據ID進行排序。

// 升序
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId)).collect(Collectors.toList());
// 降序
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId).reversed()).collect(Collectors.toList());
// 先根據ID排序,再根據age排序
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId).thenComparing(User::getAge)).collect(Collectors.toList());

其中User可能為null,User中的ID也可能為null。

  • 方法1:先過濾,再排序

  • 方法2:可使用nullFirst或者nullLast

// 2.1 如果User可能為null
List<User> newUsers = users.stream().sorted(Comparator.nullsLast(Comparator.comparing(User::getId))).collect(Collectors.toList());
// 2.2 如果User中的ID可能為null
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId, Comparator.nullsLast(Comparator.naturalOrder()))).collect(Collectors.toList());

 

6.4 去重

根據基礎型別和String型別去重:

比如List<Integer>List<String>集合,可使用distinct()去重。

List<Integer> newList = list.stream().distinct().collect(Collectors.toList());

根據物件中某個或多個欄位去重:

ID有可能相同,根據ID進行去重。

// 方法一:使用TreeSet去重,但是這個方法有副作用,會根據ID排序(TreeSet特性)
List<User> newUsers = users.stream().collect(Collectors.collectingAndThen(
             Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getId))), ArrayList::new));

// 方法二:使用Map的key不可重複的特性,進行去重
List<User> newUsers = users.stream().collect(Collectors.toMap(User::getId, b -> b, (b1, b2) -> b2))
              .values().stream().collect(Collectors.toList());

// 方法三:自定義方法去重
List<User> newUsers = users.stream().filter(distinctByKey(User::getId)).collect(Collectors.toList());
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
       Map<Object,Boolean> seen = new ConcurrentHashMap<>();
       return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

根據ID和Age兩個欄位進行去重。

List<User> newUsers = users.stream().filter(distinctByKey(User::getId, User::getAge)).collect(Collectors.toList());
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor1, Function<? super T, ?> keyExtractor2) {
   Map<Object,Boolean> seen = new ConcurrentHashMap<>();
   return t -> seen.putIfAbsent(keyExtractor1.apply(t).toString() + keyExtractor2.apply(t).toString(), Boolean.TRUE) == null;
}

其中User可能為null,User中的ID也可能為null(參考排序)。

// 如果User中的ID可能為null:可使用nullFirst或者nullLast
List<User> newUsers = users.stream().collect(Collectors.collectingAndThen(
               Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getId,
                       Comparator.nullsFirst(Comparator.naturalOrder())))), ArrayList::new));

 

6.5 數學運算

計算平均值:

// 方法1:mapToInt會將當前流轉換成IntStream
double average = users.stream().mapToInt(User::getAge).average().getAsDouble()
double average = users.stream().mapToInt(User::getAge).summaryStatistics().getAverage();
// 方法2:Collectors實現的平均數
double average = users.stream().collect(Collectors.averagingInt(User::getAge));

計算總和:

// BigDecimal
BigDecimal sum = users.stream().map(User::getMoney).reduce(Bigdecimal.ZERO, BigDecimal::add);
// int、double、long:
int sum = users.stream.mapToInt(User::getNum).sum;

計算最大值:

找到年齡最大的使用者。

int age = users.stream().max(Comparator.comparing(User::getAge)).orElse(null);

計算最小值:

找到年齡最小的使用者。

int age = users.stream().min(Comparator.comparing(User::getAge)).orElse(null);

 

七、結尾

關於流的一些常用操作就介紹完啦~希望大家能有所收穫。我是宋影,第一篇技術類博文就此奉上啦。

參考博文:

  1. https://juejin.cn/post/6844903830254010381#heading-9

  2. https://blog.csdn.net/sinat_36184075/article/details/111767670

  3. https://colobu.com/2016/03/02/Java-Stream/

  4. http://www.itwanger.com/life/2020/04/01/java-stream.html

相關文章