創建流
創建流的方式很多,從jdk8
起,很多類中添加了一些方法來創建相應的流,比如:BufferedReader
類的lines()
方法;Pattern
類的splitAsStream
方法。但是開發中使用到Stream
基本上都是對集合的操作,了解如下幾種創建方式即可:
// 集合與數組
List<String> list = new ArrayList<>();
String[] arr = new String[]{};
Stream<String> listStream = list.stream();
Stream<String> arrayStream = Arrays.stream(arr);
// 靜態方法創建指定元素的流
Stream<String> programStream = Stream.of("java", "c++", "c");
// 將多個集合合並為一個流
List<String> sub1 = new ArrayList<>();
List<String> sub2 = new ArrayList<>();
Stream<String> concatStream = Stream.concat(sub1.stream(), sub2.stream());
// 提供一個供給型函數式接口, 源源不斷創造數據, 創建無限流
// 如下創建一個無限的整數流, 源源不斷打印10以內的隨機數
Stream<Integer> generateStream = Stream.generate(() -> RandomUtil.randomInt(10));
generateStream.forEach(System.out::println);
// limit可控制返回流中的前n個元素, 此處僅取前2個隨機數打印
Stream<Integer> limitStream = generateStream.limit(2);
limitStream.forEach(System.out::println);
中間操作
篩選
filter:入參為斷言型接口(Predicate<T>
),即用於篩選出斷言函數返回true
的元素
limit:截斷流,獲取前n個元素的流
skip:跳過n個元素
distinct:通過流中元素的equals
方法比較,去除重復的元素
這四個篩選類方法較為簡單,示例如下:
List<Map<String, Object>> source = ImmutableList.of(
ImmutableMap.of("id", 1),
ImmutableMap.of("id", 2),
ImmutableMap.of("id", 2),
ImmutableMap.of("id", 3),
ImmutableMap.of("id", 4)
);
Stream<Map<String, Object>> target = source.stream()
// 先篩選出id > 1的數據
.filter(item -> Convert.toInt(item.get("id")) > 1)
.distinct() // 去重
.skip(1) // 跳過一個元素
.limit(1); // 只返回第一個元素
target.forEach(System.out::println); // 輸出: {id=3}
映射
map
map
方法入參為函數式接口(Function<T, R>
),即將流中的每個元素映射為另一個元素;
如下,取出source
中的每個map
元素,取id
和age
屬性拼成一句話當做新的元素
List<Map<String, Object>> source = ImmutableList.of(
ImmutableMap.of("id", 1, "age", 10),
ImmutableMap.of("id", 2, "age", 12),
ImmutableMap.of("id", 3, "age", 16)
);
Stream<String> target = source.stream()
.map(item -> item.get("id").toString() + "號嘉賓, 年齡: " + item.get("age"));
target.forEach(System.out::println);
輸出:
1號嘉賓, 年齡: 10
2號嘉賓, 年齡: 12
3號嘉賓, 年齡: 16
flatMap
map
操作是將單個對象轉換為另外一個對象,而flatMap
操作則是將單個對象轉換為多個對象,這多個對象以流的形式返回,最后將所有流合並為一個流返回;
List<List<String>> source = ImmutableList.of(
ImmutableList.of("A_B"),
ImmutableList.of("C_D")
);
Stream<String> target = source.stream().flatMap(item -> {
String data = item.get(0);
// 將數據映射為一個數組, 即:一對多
String[] spilt = data.split("_");
// 由泛型可知, 需要返回流的形式
return Arrays.stream(spilt);
});
target.forEach(System.out::println); // 依次打印A、B、C、D
排序
sorted
對流中的元素進行排序,默認會根據元素的自然順序排序,也可傳入Comparator
接口實現指定排序邏輯
List<Integer> source = ImmutableList.of(
10, 1, 5, 3
);
source.stream()
.sorted()
.forEach(System.out::println); // 打印: 1、3、5、10
source.stream()
.sorted(((o1, o2) -> o2 - o1))
.forEach(System.out::println); // 打印: 10、5、3、1
消費
peek
該方法主要目的是用來調試,接收一個消費者函數,方法注釋中用例如下:
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
實際使用很少,並不需要這么來調試問題
終結操作
查找與匹配
allMatch:檢查是否匹配流中所有元素
anyMatch:檢查是否至少匹配流中的一個元素
noneMatch:檢查是否沒有匹配的元素
findFirst:返回第一個元素
findAny:返回任意一個元素
max/min/count:返回流中最大/最小/總數
List<Integer> source = ImmutableList.of(
10, 1, 5, 3
);
// 元素是否都大於0
System.out.println(source.stream().allMatch(s -> s > 0));
// 是否存在元素大於9
System.out.println(source.stream().anyMatch(s -> s > 9));
// 是否不存在元素大於10
System.out.println(source.stream().noneMatch(s -> s > 10));
// 返回第一個元素, 若不存在則返回0
System.out.println(source.stream().findFirst().orElse(0));
// 任意返回一個元素, 若不存在則返回0
System.out.println(source.stream().findAny().orElse(0));
reduce
該方法可用於聚合求值,有三個重載方法;
reduce(BinaryOperator<T> accumulator)
reduce(T identity, BinaryOperator<T> accumulator)
reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
先看第一種重載形式:
當入參僅為函數式接口BinaryOperator<T>
時,定義了如何將流中的元素聚合在一起,即將流中所有元素組合成一個元素;注意到BinaryOperator<T>
接口繼承自BiFunction<T,T,T>
,從泛型可知接口入參和出參都是同類型的
// 查詢出在不同年齡擁有的財產
List<Map<String, Object>> source = ImmutableList.of(
ImmutableMap.of("age", 10, "money", "21.2"),
ImmutableMap.of("age", 20, "money", "422.14"),
ImmutableMap.of("age", 30, "money", "3312.16")
);
// 計算年齡總和和財產總和
Map<String, Object> res = source.stream().reduce((firstMap, secondMap) -> {
Map<String, Object> tempRes = new HashMap<>();
tempRes.put("age", Integer.parseInt(firstMap.get("age").toString())
+ Integer.parseInt(secondMap.get("age").toString()));
tempRes.put("money", Double.parseDouble(firstMap.get("money").toString())
+ Double.parseDouble(secondMap.get("money").toString()));
// 流中的元素是map, 最終也只能聚合為一個map
return tempRes;
}).orElseGet(HashMap::new);
System.out.println(JSONUtil.toJsonPrettyStr(res));
// 輸出
{
"money": 3755.5,
"age": 60
}
BinaryOperator<T>
函數的兩個入參如何理解呢?如下:
容易看出其含義就是將流中元素整合在一起,但是這種方法的初始值就是流中的第一個元素,能否自定義聚合的初始值呢?
這就是第二種重載形式了,顯然,第一個參數就是指定聚合的初始值;
緊接着上個例子,假設人一出生就擁有100塊錢(100塊都不給我?),如下:
List<Map<String, Object>> source = ImmutableList.of(
ImmutableMap.of("age", 10, "money", "21.2"),
ImmutableMap.of("age", 20, "money", "422.14"),
ImmutableMap.of("age", 30, "money", "3312.16")
);
// 計算年齡總和和財產總和
Map<String, Object> res = source.stream().reduce(ImmutableMap.of("age", 0, "money", "100"),
(firstMap, secondMap) -> {
Map<String, Object> tempRes = new HashMap<>();
tempRes.put("age", Integer.parseInt(firstMap.get("age").toString())
+ Integer.parseInt(secondMap.get("age").toString()));
tempRes.put("money", Double.parseDouble(firstMap.get("money").toString())
+ Double.parseDouble(secondMap.get("money").toString()));
// 流中的元素是map, 最終也只能聚合為一個map
return tempRes;
});
System.out.println(JSONUtil.toJsonPrettyStr(res));
// 輸出
{
"money": 3855.5,
"age": 60
}
注意到第一種形式沒有指定初始值,所以會返回一個Optional
值,而第二種重載形式既定了初始值,也就是不會為空,所以返回值不需要Optional
類包裝了。
如上我們既可以定義初始值,又可以定義聚合的方式了,還缺什么呢?
有一點小缺就是上面兩種返回的結果集的類型,跟原數據流中的類型是一樣的,無法自定義返回的類型,這點從BinaryOperator
參數的泛型可以看出。
所以第三種重載形式出場了,既可自定義返回的數據類型,又支持自定義並行流場景下的多個線程結果集的組合形式;
如下返回Pair
類型:
List<Map<String, Object>> source = ImmutableList.of(
ImmutableMap.of("age", 10, "money", "21.2"),
ImmutableMap.of("age", 20, "money", "422.14"),
ImmutableMap.of("age", 30, "money", "3312.16")
);
// 計算年齡總和和財產總和
Pair<Integer, Double> reduce = source.stream().reduce(Pair.of(0, 100d),
(firstPair, secondMap) -> {
int left = firstPair.getLeft()
+ Integer.parseInt(secondMap.get("age").toString());
double right = firstPair.getRight() +
+Double.parseDouble(secondMap.get("money").toString());
return Pair.of(left, right);
}, (o, n) -> o);
System.out.println(JSONUtil.toJsonPrettyStr(reduce));
其中(o, n) -> o
是隨便寫的,因為在順序流中這段函數不會執行,也就是無效的,只關注前兩個參數:如何定義返回的數據和類型、如何定義聚合的邏輯。
坑點:
- 如果創建並行流,且使用前兩種重載方法,最終得到的結果可能會和上面舉例的有些差別,因為從上面的原理來看,reduce中的每一次運算,下一步的結果總是依賴於上一步的執行結果的,像這樣肯定無法並行執行,所以並行流場景下,會有一些不同的細節問題
- 當創建並行流,且使用了第三種重載方法,得到的結果可能和預期的也不一樣,這需要了解其內部到底是如何聚合多個線程的結果集的
目前開發中沒有詳細使用並行流的經驗,有待研究
collect
collect是個非常有用的操作,可以將流中的元素收集成為另一種形式便於我們使用;該方法需要傳入Collector
類型,但是手動實現此類型還比較麻煩,通常用Collectors
工具類來構建我們想要返回的形式:
構建方法 | 說明 |
---|---|
Collectors.toList() | 將流收集為List形式 |
Collectors.toSet() | 將流收集為Set形式 |
Collectors.toCollection() | 收集為指定的集合形式,如LinkedList...等 |
Collectors.toMap() | 收集為Map |
Collectors.collectingAndThen() | 允許對生成的集合再做一次操作 |
Collectors.joining() | 用來連接流中的元素,比如用逗號分隔元素連接起來 |
Collectors.counting() | 統計元素個數 |
Collectors.summarizingDouble/Long/Int() | 為流中元素生成統計信息,返回的是一個統計類 |
Collectors.averagingDouble/Long/Int() | 對流中元素做平均 |
Collectors.maxBy()/minBy() | 根據指定的Comparator,返回流中最大/最小值 |
Collectors.groupingBy() | 根據某些屬性分組 |
Collectors.partitioningBy() | 根據指定條件 |
如下舉例一些使用:
toList/toSet/toCollection
將元素收集為集合形式
List<Map<String, Object>> source = ImmutableList.of(
ImmutableMap.of("name", "小明", "grade", "a1", "sex", "1"),
ImmutableMap.of("name", "小紅", "grade", "a2", "sex", "2"),
ImmutableMap.of("name", "小白", "grade", "a1", "sex", "1"),
ImmutableMap.of("name", "小黑", "grade", "a3", "sex", "1"),
ImmutableMap.of("name", "小黃", "grade", "a4", "sex", "2")
);
// toSet類似, toCollection指定要返回的集合即可
List<String> toList = source.stream()
.map(map -> map.get("name").toString())
.collect(Collectors.toList());
System.out.println(toList); // [小明, 小紅, 小白, 小黑, 小黃]
collectingAndThen
收集為集合之后再額外做一次操作
List<String> andThen = source.stream()
.map(map -> map.get("name").toString())
.collect(Collectors.collectingAndThen(Collectors.toList(), x -> {
// 將集合翻轉
Collections.reverse(x);
return x;
}));
// 由於上述方法在toList之后, 增加了一個函數操作使集合翻轉, 所以結果跟上個示例是反的
System.out.println(andThen); // [小黃, 小黑, 小白, 小紅, 小明]
toMap
收集為map,使用此方法務必要傳入第三個參數,用於表明當key沖突時如何處理
Map<String, String> toMap = source.stream()
.collect(Collectors.toMap(
// 取班級字段為key
item -> item.get("grade").toString(),
// 取名字為value
item -> item.get("name").toString(),
// 此函數用於決定當key重復時, 新value和舊value如何取舍
(oldV, newV) -> oldV + "_" + newV));
System.out.println(toMap); // {a1=小明_小白, a2=小紅, a3=小黑, a4=小黃}
summarizingDouble/Long/Int
可用於對基本數據類型的數據作一些統計用,使用很少
// 對集合中sex字段統計一些信息
IntSummaryStatistics statistics = source.stream()
.collect(Collectors.summarizingInt(map -> Integer.parseInt(map.get("sex").toString())));
System.out.println("平均值:" + statistics.getAverage()); // 1.4
System.out.println("最大值:" + statistics.getMax()); // 2
System.out.println("最小值:" + statistics.getMin()); // 1
System.out.println("元素數量:" + statistics.getCount()); // 5
System.out.println("總和:" + statistics.getSum()); // 7
groupingBy
分組是用得比較多的一種方法,其有三種重載形式:
groupingBy(Function<? super T, ? extends K> classifier)
groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream)
groupingBy(Function<? super T, ? extends K> classifier, Supplier<M> mapFactory, Collector<? super T, A, D> downstream)
點進源碼中很容易發現,其實前兩種都是第三種的特殊形式;
從第一種重載形式看起:
Function classifier:分類器,是個Function
類型接口,此參數用於指定根據什么值來分組,Function
接口返回的值就是最終返回的Map
中的key
List<Map<String, Object>> source = ImmutableList.of(
ImmutableMap.of("name", "小明", "grade", "a1", "sex", "1"),
ImmutableMap.of("name", "小紅", "grade", "a2", "sex", "2"),
ImmutableMap.of("name", "小白", "grade", "a1", "sex", "1"),
ImmutableMap.of("name", "小黑", "grade", "a3", "sex", "1"),
ImmutableMap.of("name", "小黃", "grade", "a4", "sex", "2")
);
Map<String, List<Map<String, Object>>> grade = source.stream()
.collect(Collectors.groupingBy(item -> item.get("grade") + "-" + item.get("sex")));
System.out.println(JSONUtil.toJsonPrettyStr(grade));
根據班級+性別組合成的字段分組,結果如下(省略了一部分):
{
"a4-2": [
{
"sex": "2",
"grade": "a4",
"name": "小黃"
}
],
"a1-1": [
{
"sex": "1",
"grade": "a1",
"name": "小明"
},
{
"sex": "1",
"grade": "a1",
"name": "小白"
}
],
...
}
可以看到key
是Function
接口中選擇的grade
+sex
字段,value
是原數據集中元素的集合;
既然key
的形式可以控制,value
的形式如何控制呢?這就需要第二種重載形式了
Collector downstream:該參數就是用於控制分組之后想返回什么形式的值,Collector
類型,所以可以用Collectors
工具類的方法來控制結果集形式;根據此特性,可以實現多級分組;
點進第一種重載方法源碼中可以看到,不傳第二個參數時,默認取的是Collectors.toList()
,所以最后返回的Map
中的value
是集合形式,可以指定返回其他的形式。
比如上個例子,分組之后返回每組有多少個元素就行,不需要具體元素的集合
List<Map<String, Object>> source = ImmutableList.of(
ImmutableMap.of("name", "小明", "grade", "a1", "sex", "1"),
ImmutableMap.of("name", "小紅", "grade", "a2", "sex", "2"),
ImmutableMap.of("name", "小白", "grade", "a1", "sex", "1"),
ImmutableMap.of("name", "小黑", "grade", "a3", "sex", "1"),
ImmutableMap.of("name", "小黃", "grade", "a4", "sex", "2")
);
Map<String, Long> collect = source.stream()
.collect(Collectors.groupingBy(item -> item.get("grade") + "-" + item.get("sex"),
Collectors.counting()));
System.out.println(JSONUtil.toJsonPrettyStr(collect));
結果就是返回的Map
中的value
是元素個數了:
{
"a4-2": 1,
"a1-1": 2,
"a3-1": 1,
"a2-2": 1
}
再看最后返回的結果集Map
,其中的key
和value
都可以自由控制形式了,那Map
類型具體到底是哪個實現類呢?這就是第三種重載形式的作用了
Supplier mapFactory:從參數名顯而易見就是制造map
的工廠,Supplier
供給型接口,即指定返回的Map
類型就行
如果沒有使用此參數指定Map
類型,那么默認返回的就是HashMap
實現,這點從方法源碼中很容易看到
緊接着上個例子,返回LinkedHashMap
實現
List<Map<String, Object>> source = ImmutableList.of(
ImmutableMap.of("name", "小明", "grade", "a1", "sex", "1"),
ImmutableMap.of("name", "小紅", "grade", "a2", "sex", "2"),
ImmutableMap.of("name", "小白", "grade", "a1", "sex", "1"),
ImmutableMap.of("name", "小黑", "grade", "a3", "sex", "1"),
ImmutableMap.of("name", "小黃", "grade", "a4", "sex", "2")
);
Map<String, Long> collect = source.stream()
.collect(Collectors.groupingBy(item -> item.get("grade") + "-" + item.get("sex"),
LinkedHashMap::new,
Collectors.counting()));
System.out.println(JSONUtil.toJsonPrettyStr(collect));
System.out.println(collect instanceof LinkedHashMap);
輸出:
{
"a4-2": 1,
"a1-1": 2,
"a2-2": 1,
"a3-1": 1
}
true
partitioningBy
分區也是分組的一種特殊表現形式,分區操作返回的結果集Map
中的key
固定為true/false
兩種,含義就是根據入參的預測型接口將數據分為兩類,類比分組即可;