平時工作中,我在處理集合的時候,總是會用到各種流操作,但是往往在處理一些較為複雜的集合時,還是會出現無法靈活運用api的場景,這篇文章的目的,主要是為介紹一些工作中使用流時的常用操作,例如去重、排序和數學運算等內容,並不對流的原理和各種高階api做深度剖析,讓我們開始吧~
如果讀者你已經對流有一些基本的瞭解,現在只是有些場景運用到流,不知道如何使用,請劃到文章的最後一個部分-常用操作,希望能夠幫助到你。^^
一、流的組成
往往我們使用流的時候,都會經過3步,如下圖所示,首先我們建立一個流,然後對流進行一系列的中間操作,最後執行一個終端操作,這個流就到此結束了。
-
建立流:有且建立一次即可。
-
中間操作:0個,1個及多個均可,可以進行鏈式操作。
-
終端操作:一條語句中有且只存在1個,一旦進行該操作,代表該流已結束。
我們需要關注的,實際上是對流的中間操作和終端操作。
二、舉例物件
例子:現在我們多個使用者,抽象成List<User>
,該使用者有ID,名稱,年齡,錢以及擁有多個賬戶。
三、建立流
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);
七、結尾
關於流的一些常用操作就介紹完啦~希望大家能有所收穫。我是宋影,第一篇技術類博文就此奉上啦。
參考博文: