深入理解JDK8中Stream使用
概述
Stream 是 Java8 中處理集合的關(guān)鍵抽象概念,它可以指定你希望對(duì)集合進(jìn)行的操作,可以執(zhí)行非常復(fù)雜的查找、過濾和映射數(shù)據(jù)等操作。使用Stream API 對(duì)集合數(shù)據(jù)進(jìn)行操作,就類似于使用 SQL 執(zhí)行的數(shù)據(jù)庫(kù)查詢。也可以使用 Stream API 來并行執(zhí)行操作。簡(jiǎn)而言之,Stream API 提供了一種高效且易于使用的處理數(shù)據(jù)的方式。
特點(diǎn):
不是數(shù)據(jù)結(jié)構(gòu),不會(huì)保存數(shù)據(jù)。
不會(huì)修改原來的數(shù)據(jù)源,它會(huì)將操作后的數(shù)據(jù)保存到另外一個(gè)對(duì)象中。(保留意見:畢竟peek方法可以修改流中元素)
惰性求值,流在中間處理過程中,只是對(duì)操作進(jìn)行了記錄,并不會(huì)立即執(zhí)行,需要等到執(zhí)行終止操作的時(shí)候才會(huì)進(jìn)行實(shí)際的計(jì)算。
現(xiàn)在談及JDK8的新特新,已經(jīng)說不上新了。本篇介紹的就是Stream和Lambda,說的Stream可不是JDK中的IO流,這里的Stream指的是處理集合的抽象概念『像流一樣處理集合數(shù)據(jù)』。
了解Stream前先認(rèn)識(shí)一下Lambda。
函數(shù)式接口和Lambda
先看一組簡(jiǎn)單的對(duì)比
傳統(tǒng)方式使用一個(gè)匿名內(nèi)部類的寫法
new Thread(new Runnable() {
@Override
public void run() {
// ...
}
}).start();
換成Lambda的寫法
new Thread(() -> {
// ...
}).start();
其實(shí)上面的寫法就是簡(jiǎn)寫了函數(shù)式接口的匿名實(shí)現(xiàn)類
配合Lambda,JDK8引入了一個(gè)新的定義叫做:函數(shù)式接口(Functional interfaces)
函數(shù)式接口
從概念上講,有且僅有一個(gè)需要實(shí)現(xiàn)方法的接口稱之為函數(shù)式接口。
看一個(gè)JDK給的一個(gè)函數(shù)式接口的源碼
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看到接口上面有一個(gè)@FunctionalInterface注釋,功能大致和@Override類似
不寫@Override也能重寫父類方法,該方法確實(shí)沒有覆蓋或?qū)崿F(xiàn)了在超類型中聲明的方法時(shí)編譯器就會(huì)報(bào)錯(cuò),主要是為了編譯器可以驗(yàn)證識(shí)別代碼編寫的正確性。
同樣@FunctionalInterface也是這樣,寫到一個(gè)不是函數(shù)式接口的接口上面就會(huì)報(bào)錯(cuò),即使不寫@FunctionalInterface注釋,編譯器也會(huì)將滿足函數(shù)式接口定義的任何接口視為函數(shù)式接口。
寫一個(gè)函數(shù)式接口加不加@FunctionalInterface注釋,下面的接口都是函數(shù)式接口
interface MyFunc {
String show(Integer i);
}
Lambda表達(dá)式
Lambda表達(dá)式就是為了簡(jiǎn)寫函數(shù)式接口
構(gòu)成
看一下Lambda的構(gòu)成
- 括號(hào)里面的參數(shù)
- 箭頭
-> - 然后是身體
它可以是單個(gè)表達(dá)式或java代碼塊。
整體表現(xiàn)為 (...參數(shù)) -> {代碼塊}
簡(jiǎn)寫
下面就是函數(shù)式接口的實(shí)現(xiàn)簡(jiǎn)寫為Lambda的例子
無參 - 無返回
interface MyFunc1 {
void func();
}
// 空實(shí)現(xiàn)
MyFunc1 f11 = () -> { };
// 只有一行語句
MyFunc1 f12 = () -> {
System.out.println(1);
System.out.println(2);
};
// 只有一行語句
MyFunc1 f13 = () -> {
System.out.println(1);
};
// 只有一行語句可以省略 { }
MyFunc1 f14 = () -> System.out.println(1);
有參 - 無返回
interface MyFunc2 {
void func(String str);
}
// 函數(shù)體空實(shí)現(xiàn)
MyFunc2 f21 = (str) -> { };
// 單個(gè)參數(shù)可以省略 () 多個(gè)不可以省略
MyFunc2 f22 = str -> System.out.println(str.length());
無參 - 有返回
interface MyFunc3 {
int func();
}
// 返回值
MyFunc3 f31 = () -> {return 1;};
// 如果只有一個(gè)return 語句時(shí)可以直接寫return 后面的表達(dá)式語句
MyFunc3 f32 = () -> 1;
有參 - 有返回
interface MyFunc4 {
int func(String str);
}
// 這里單個(gè)參數(shù)簡(jiǎn)寫了{(lán)}
MyFunc4 f41 = str -> {
return str.length();
};
// 這里又簡(jiǎn)寫了return
MyFunc4 f42 = str -> str.length();
// 這里直接使用了方法引用進(jìn)行了簡(jiǎn)寫 - 在文章后續(xù)章節(jié)有介紹到
MyFunc4 f43 = String::length;
這里可以總結(jié)出來簡(jiǎn)寫規(guī)則
上面寫的Lambda表達(dá)式中參數(shù)都沒有寫參數(shù)類型(可以寫參數(shù)類型的),so
- 小括號(hào)內(nèi)參數(shù)的類型可以省略;
- 沒有參數(shù)時(shí)小括號(hào)不能省略,小括號(hào)中有且僅有一個(gè)參數(shù)時(shí),不能缺省括號(hào)
- 如果大括號(hào)內(nèi)有且僅有一個(gè)語句,則無論是否有返回值,都可以省略大括號(hào)、return關(guān)鍵字及語句分號(hào)(三者省略都需要一起省略)。
看到這里應(yīng)該認(rèn)識(shí)到了如何用Lambda簡(jiǎn)寫函數(shù)式接口,那現(xiàn)在就進(jìn)一步的認(rèn)識(shí)一下JDK中Stream中對(duì)函數(shù)式接口的幾種大類
常用內(nèi)置函數(shù)式接口
上節(jié)說明了Lambda表達(dá)式就是為了簡(jiǎn)寫函數(shù)式接口,為使用方便,JDK8提供了一些常用的函數(shù)式接口。最具代表性的為Supplier、Function、Consumer、Perdicate,這些函數(shù)式接口都在java.util.function包下。
這些函數(shù)式接口都是泛型類型的,下面的源碼都去除了default方法,只保留真正需要實(shí)現(xiàn)的方法。
Function接口
這是一個(gè)轉(zhuǎn)換的接口。接口有參數(shù)、有返回值,傳入T類型的數(shù)據(jù),經(jīng)過處理后,返回R類型的數(shù)據(jù)?!篢和R都是泛型類型』可以簡(jiǎn)單的理解為這是一個(gè)加工工廠。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
使用實(shí)例:定義一個(gè)轉(zhuǎn)換函數(shù)『將字符串轉(zhuǎn)為數(shù)字,再平方』
// 將字符串轉(zhuǎn)為數(shù)字,再平方
Function<String, Integer> strConvertToIntAndSquareFun = (str) -> {
Integer value = Integer.valueOf(str);
return value * value;
};
Integer result = strConvertToIntAndSquareFun.apply("4");
System.out.println(result); // 16
Supplier接口
這是一個(gè)對(duì)外供給的接口。此接口無需參數(shù),即可返回結(jié)果
@FunctionalInterface
public interface Supplier<T> {
T get();
}
使用實(shí)例:定義一個(gè)函數(shù)返回“Tom”字符串
// 供給接口,調(diào)用一次返回一個(gè) ”tom“ 字符串 Supplier<String> tomFun = () -> "tom"; String tom = tomFun.get(); System.out.println(tom); // tom
Consumer接口
這是一個(gè)消費(fèi)的接口。此接口有參數(shù),但是沒有返回值
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
使用實(shí)例:定義一個(gè)函數(shù)傳入數(shù)字,打印一行相應(yīng)數(shù)量的A
// 重復(fù)打印
Consumer<Integer> printA = (n)->{
for (int i = 0; i < n; i++) {
System.out.print("A");
}
System.out.println();
};
printA.accept(5); // AAAAA
Predicate接口
這是一個(gè)斷言的接口。此接口對(duì)輸入的參數(shù)進(jìn)行一系列的判斷,返回一個(gè)Boolean值。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
使用實(shí)例:定義一個(gè)函數(shù)傳入一個(gè)字符串,判斷是否為A字母開頭且Z字母結(jié)尾
// 判斷是否為`A`字母開頭且`Z`字母結(jié)尾
Predicate<String> strAStartAndZEnd = (str) -> {
return str.startsWith("A") && str.endsWith("Z");
};
System.out.println(strAStartAndZEnd.test("AaaaZ")); // true
System.out.println(strAStartAndZEnd.test("Aaaaa")); // false
System.out.println(strAStartAndZEnd.test("aaaaZ")); // false
System.out.println(strAStartAndZEnd.test("aaaaa")); // false
除Supplier接口外Function、Consumer、Perdicate還有其他一堆默認(rèn)方法可以用,比如Predicate接口包含了多種默認(rèn)方法,用于處理復(fù)雜的判斷邏輯(and, or);
上面的使用方式都是正常簡(jiǎn)單的使用函數(shù)式接口,當(dāng)函數(shù)式接口遇見了方法引用才真正發(fā)揮他的作用。
方法引用
方法引用的唯一存在的意義就是為了簡(jiǎn)寫Lambda表達(dá)式。
方法引用通過方法的名字來指向一個(gè)方法,可以使語言的構(gòu)造更緊湊簡(jiǎn)潔,減少冗余代碼。
比如上面章節(jié)使用的
MyFunc4 f43 = String::length; // 這個(gè)地方就用到了方法引用
方法引用使用一對(duì)冒號(hào) ::
相當(dāng)于將String類的實(shí)例方法length賦給MyFunc4接口
public int length() {
return value.length;
}
interface MyFunc4 {
int func(String str);
}
這里可能有點(diǎn)問題:方法 int length()的返回值和int func(String str)相同,但是方法參數(shù)不同為什么也能正常賦值給MyFunc4。
可以理解為Java實(shí)例方法有一個(gè)隱藏的參數(shù)第一個(gè)參數(shù)this(類型為當(dāng)前類)
public class Student {
public void show() {
// ...
}
public void print(int a) {
// ...
}
}
實(shí)例方法show()和print(int a)相當(dāng)于
public void show(String this); public void print(String this, int a);
這樣解釋的通為什么MyFunc4 f43 = String::length;可以正常賦值。
String::length;
public int length() {
return value.length;
}
// 相當(dāng)于
public int length(String str) {
return str.length();
}
// 這樣看length就和函數(shù)式接口MyFunc4的傳參和返回值就相同了
不只這一種方法引用詳細(xì)分類如下
方法引用分類
| 類型 | 引用寫法 | Lambda表達(dá)式 |
|---|---|---|
| 靜態(tài)方法引用 | ClassName::staticMethod | (args) -> ClassName.staticMethod(args) |
| 對(duì)象方法引用 | ClassName::instanceMethod | (instance, args) -> instance.instanceMethod(args) |
| 實(shí)例方法引用 | instance::instanceMethod | (args) -> instance.instanceMethod(args) |
| 構(gòu)建方法引用 | ClassName::new | (args) -> new ClassName(args) |
對(duì)象方法引用
記住這個(gè)表格,不用刻意去記,使用Stream時(shí)會(huì)經(jīng)常遇到
有幾種比較特殊的方法引用,一般來說原生類型如int不能做泛型類型,但是int[]可以
IntFunction<int[]> arrFun = int[]::new; int[] arr = arrFun.apply(10); // 生成一個(gè)長(zhǎng)度為10的數(shù)組
這節(jié)結(jié)束算是把函數(shù)式接口,Lambda表達(dá)式,方法引用等概念串起來了。
Optional工具
Optional工具是一個(gè)容器對(duì)象,最主要的用途就是為了規(guī)避 NPE(空指針) 異常。構(gòu)造方法是私有的,不能通過new來創(chuàng)建容器。是一個(gè)不可變對(duì)象,具體原理沒什么可以介紹的,容器源碼整個(gè)類沒500行,本章節(jié)主要介紹使用。
構(gòu)造方法
private Optional(T value) {
// 傳 null 會(huì)報(bào)空指針異常
this.value = Objects.requireNonNull(value);
}
創(chuàng)建Optional的方法
empyt返回一個(gè)包含null值的Optional容器
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
of返回一個(gè)不包含null值的Optional容器,傳null值報(bào)空指針異常
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
ofNullable返回一個(gè)可能包含null值的Optional容器
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
可以使用的Optional的方法
ifPresent方法,參數(shù)是一個(gè)Consumer,當(dāng)容器內(nèi)的值不為null是執(zhí)行Consumer
Optional<Integer> opt = Optional.of(123);
opt.ifPresent((x) -> {
System.out.println(opt);
});
// out: 123
get方法,獲取容器值,可能返回空
orElse方法,當(dāng)容器中值為null時(shí),返回orElse方法的入?yún)⒅?/p>
public T orElse(T other) {
return value != null ? value : other;
}
orElseGet方法,當(dāng)容器中值為null時(shí),執(zhí)行入?yún)?code>Supplier并返回值
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
常見用法
// 當(dāng)param為null時(shí) 返回空集合 Optional.ofNullable(param).orElse(Collections.emptyList()); Optional.ofNullable(param).orElseGet(() -> Collections.emptyList());
orElse和orElseGet的區(qū)別,orElseGet算是一個(gè)惰性求值的寫法,當(dāng)容器內(nèi)的值不為null時(shí)Supplier不會(huì)執(zhí)行。
平常工作開發(fā)中,也是經(jīng)常通過 orElse 來規(guī)避 NPE 異常。
這方面不是很困難難主要是后續(xù)Stream有些方法需要會(huì)返回一個(gè)Optional一個(gè)容器對(duì)象。
Stream
Stream可以看作是一個(gè)高級(jí)版的迭代器。增強(qiáng)了Collection的,極大的簡(jiǎn)化了對(duì)集合的處理。
想要使用Stream首先需要?jiǎng)?chuàng)建一個(gè)
創(chuàng)建Stream流的方式
// 方式1,數(shù)組轉(zhuǎn)Stream Arrays.stream(arr); // 方式2,數(shù)組轉(zhuǎn)Stream,看源碼of就是方法1的包裝 Stream.of(arr); // 方式3,調(diào)用Collection接口的stream()方法 List<String> list = new ArrayList<>(); list.stream();
有了Stream自然就少不了操作流
常用Stream流方法
大致可以把對(duì)Stream的操作大致分為兩種類型中間操作和終端操作
中間操作是一個(gè)屬于惰式的操作,也就是不會(huì)立即執(zhí)行,每一次調(diào)用中間操作只會(huì)生成一個(gè)標(biāo)記了新的Stream終端操作會(huì)觸發(fā)實(shí)際計(jì)算,當(dāng)終端操作執(zhí)行時(shí)會(huì)把之前所有中間操作以管道的形式順序執(zhí)行,Stream是一次性的計(jì)算完會(huì)失效
操作Stream會(huì)大量的使用Lambda表達(dá)式,也可以說它就是為函數(shù)式編程而生
先提前認(rèn)識(shí)一個(gè)終端操作forEach對(duì)流中每個(gè)元素執(zhí)行一個(gè)操作,實(shí)現(xiàn)一個(gè)打印的效果
// 打印流中的每一個(gè)元素
Stream.of("jerry", "lisa", "moli", "tom", "Demi").forEach(str -> {
System.out.println(str);
});
forEach的參數(shù)是一個(gè)Consumer可以用方法引用優(yōu)化(靜態(tài)方法引用),優(yōu)化后的結(jié)果為
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.forEach(System.out::println);
有這一個(gè)終端操作就可以向下介紹大量的中間操作了
中間操作
中間操作filter:過濾元素
fileter方法參數(shù)是一個(gè)Predicate接口,表達(dá)式傳入的參數(shù)是元素,返回true保留元素,false過濾掉元素
過濾長(zhǎng)度小于3的字符串,僅保留長(zhǎng)度大于4的字符串
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
// 過濾
.filter(str -> str.length() > 3)
.forEach(System.out::println);
/*
輸出:
jerry
lisa
moli
Demi
*/
中間操作limit:截?cái)嘣?/p>
限制集合長(zhǎng)度不能超過指定大小
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.limit(2)
.forEach(System.out::println);
/*
輸出:
jerry
lisa
*/
中間操作skip:跳過元素(丟棄流的前n元素)
// 丟棄前2個(gè)元素
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.skip(2)
.forEach(System.out::println);
/*
輸出:
moli
tom
Demi
*/
中間操作map:轉(zhuǎn)換元素
map傳入的函數(shù)會(huì)被應(yīng)用到每個(gè)元素上將其映射成一個(gè)新的元素
// 為每一個(gè)元素加上 一個(gè)前綴 "name: "
Stream.of("jerry", "lisa", "moli", "tom", "Demi")
.map(str -> "name: " + str)
.forEach(System.out::println);
/*
輸出:
name: jerry
name: lisa
name: moli
name: tom
name: Demi
*/
中間操作peek:查看元素
peek方法的存在主要是為了支持調(diào)試,方便查看元素流經(jīng)管道中的某個(gè)點(diǎn)時(shí)的情況
下面是一個(gè)JDK源碼中給出的例子
Stream.of("one", "two", "three", "four")
// 第1次查看
.peek(e -> System.out.println("第1次 value: " + e))
// 過濾掉長(zhǎng)度小于3的字符串
.filter(e -> e.length() > 3)
// 第2次查看
.peek(e -> System.out.println("第2次 value: " + e))
// 將流中剩下的字符串轉(zhuǎn)為大寫
.map(String::toUpperCase)
// 第3次查看
.peek(e -> System.out.println("第3次 value: " + e))
// 收集為L(zhǎng)ist
.collect(Collectors.toList());
/*
輸出:
第1次 value: one
第1次 value: two
第1次 value: three
第2次 value: three
第3次 value: THREE
第1次 value: four
第2次 value: four
第3次 value: FOUR
*/
map和peek有點(diǎn)相似,不同的是peek接收一個(gè)Consumer,而map接收一個(gè)Function
當(dāng)然了你非要采用peek修改數(shù)據(jù)也沒人能限制的了
public class User {
public String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
Stream.of(new User("tom"), new User("jerry"))
.peek(e -> {
e.name = "US:" + e.name;
})
.forEach(System.out::println);
/*
輸出:
User{name='US:tom'}
User{name='US:jerry'}
*/
中間操作sorted:排序數(shù)據(jù)
// 排序數(shù)據(jù)
Stream.of(4, 2, 1, 3)
// 默認(rèn)是升序
.sorted()
.forEach(System.out::println);
/*
輸出:
1
2
3
4
*/
逆序排序
// 排序數(shù)據(jù)
Stream.of(4, 2, 1, 3)
// 逆序
.sorted(Comparator.reverseOrder())
.forEach(System.out::println
/*
輸出:
4
3
2
1
*/
如果是對(duì)象如何排序,自定義Comparator,切記不要違反自反性,對(duì)稱性,傳遞性原則
public class User {
public String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
// 名稱長(zhǎng)的排前面
Stream.of(new User("tom"), new User("jerry"))
.sorted((e1, e2) -> {
return e2.name.length() - e1.name.length();
})
.forEach(System.out::println);
/*
輸出:
User{name='US:jerry'}
User{name='US:tom'}
*/
中間操作distinct:去重
注意:必須重寫對(duì)應(yīng)泛型的hashCode()和equals()方法
Stream.of(2, 2, 4, 4, 3, 3, 100)
.distinct()
.forEach(System.out::println);
/*
輸出:
2
4
3
100
*/
中間操作flatMap:平鋪流
返回一個(gè)流,該流由通過將提供的映射函數(shù)(flatMap傳入的參數(shù))應(yīng)用于每個(gè)元素而生成的映射流的內(nèi)容替換此流的每個(gè)元素,通俗易懂就是將原來的Stream中的所有元素都展開組成一個(gè)新的Stream
List<Integer[]> arrList = new ArrayList<>();
arrList.add(arr1);
arrList.add(arr2);
// 未使用
arrList.stream()
.forEach(e -> {
System.out.println(Arrays.toString(e));
});
/*
輸出:
[1, 2]
[3, 4]
*/
// 平鋪后
arrList.stream()
.flatMap(arr -> Stream.of(arr))
.forEach(e -> {
System.out.println(e);
});
/*
輸出:
1
2
3
4
*/
終端操作max,min,count:統(tǒng)計(jì)
// 最大值
Optional<Integer> maxOpt = Stream.of(2, 4, 3, 100)
.max(Comparator.comparing(e -> e));
System.out.println(maxOpt.get()); // 100
// 最小值
Optional<Integer> minOpt = Stream.of(2, 4, 3, 100)
.min(Comparator.comparing(Function.identity()));
System.out.println(minOpt.get()); // 2
// 數(shù)量
long count = Stream.of("one", "two", "three", "four")
.count();
System.out.println(count); // 4
上面例子中有一個(gè)點(diǎn)需要注意一下Function.identity()相當(dāng)于 e -> e
看源碼就可以看出來
static <T> Function<T, T> identity() {
return t -> t;
}
終端操作findAny:返回任意一個(gè)元素
Optional<String> anyOpt = Stream.of("one", "two", "three", "four")
.findAny();
System.out.println(anyOpt.orElse(""));
/*
輸出:
one
*/
終端操作findFirst:返回第一個(gè)元素
Optional<String> firstOpt = Stream.of("one", "two", "three", "four")
.findFirst();
System.out.println(firstOpt.orElse(""));
/*
輸出:
one
*/
返回的Optional容器在上面介紹過了,一般配置orElse使用,原因就在于findAny和findFirst可能返回空空容器,調(diào)用get可能會(huì)拋空指針異常
終端操作allMatch,anyMatch:匹配
// 是否全部為 one 字符串
boolean allIsOne = Stream.of("one", "two", "three", "four")
.allMatch(str -> Objects.equals("one", str));
System.out.println(allIsOne); // false
allIsOne = Stream.of("one", "one", "one", "one")
.allMatch(str -> Objects.equals("one", str));
System.out.println(allIsOne); // true
// 是否包含 one 字符串
boolean hasOne = Stream.of("one", "two", "three", "four")
.anyMatch(str -> Objects.equals("one", str));
System.out.println(hasOne); // true
hasOne = Stream.of("two", "three", "four")
.anyMatch(str -> Objects.equals("one", str));
System.out.println(hasOne); // false
上面僅僅介紹了一個(gè)forEach終端操作,但是業(yè)務(wù)開發(fā)中更多的是對(duì)處理的數(shù)據(jù)進(jìn)行收集起來,如下面的一個(gè)例子將元素收集為一個(gè)List集合
終端操作collect:收集元素到集合
collect高級(jí)使用方法很復(fù)雜,常用的用法使用Collectors工具類
收集成List
List<String> list = Stream.of("one", "two", "three", "four")
.collect(Collectors.toList());
System.out.println(list);
/*
輸出:
[one, two, three, four]
*/
收集成Set『收集后有去除的效果,結(jié)果集亂序』
Set<String> set = Stream.of("one", "one", "two", "three", "four")
.collect(Collectors.toSet());
System.out.println(set);
/*
輸出:
[four, one, two, three]
*/
字符串拼接
String str1 = Stream.of("one", "two", "three", "four")
.collect(Collectors.joining());
System.out.println(str1); // onetwothreefour
String str2 = Stream.of("one", "two", "three", "four")
.collect(Collectors.joining(", "));
System.out.println(str2); // one, two, three, four
收集成Map
// 使用Lombok插件
@Data
@AllArgsConstructor
public class User {
public Integer id;
public String name;
}
Map<Integer, User> map = Stream.of(new User(1, "tom"), new User(2, "jerry"))
.collect(Collectors.toMap(User::getId, Function.identity(), (k1, k2) -> k1));
System.out.println(map);
/*
輸出:
{
1=User(id=1, name=tom),
2=User(id=2, name=jerry)
}
*/
toMap常用的方法簽名
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
/*
keyMapper:Key 的映射函數(shù)
valueMapper:Value 的映射函數(shù)
mergeFunction:當(dāng) Key 沖突時(shí),調(diào)用的合并方法
*/
數(shù)據(jù)分組
@Data
@AllArgsConstructor
class User {
public Integer id;
public String name;
}
Map<String, List<User>> map = Stream.of(
new User(1, "tom"), new User(2, "jerry"),
new User(3, "moli"), new User(4, "lisa")
).collect(Collectors.groupingBy(u -> {
if (u.id % 2 == 0) {
return "奇";
}
return "偶";
}));
System.out.println(map);
/*
輸出:
{
偶=[User(id=1, name=tom), User(id=3, name=moli)],
奇=[User(id=2, name=jerry), User(id=4, name=lisa)]
}
*/
分組后value 是一個(gè)集合,groupingBy分組還有一個(gè)參數(shù)可以指定下級(jí)收集器,后續(xù)例子中有使用到
Steam例
下面例子用到的基礎(chǔ)數(shù)據(jù),如有例子特例會(huì)在例子中單獨(dú)補(bǔ)充
List<Student> studentList = new ArrayList<>(); studentList.add(new Student(1, "tom", 19, "男", "軟工")); studentList.add(new Student(2, "lisa", 15, "女", "軟工")); studentList.add(new Student(3, "Ada", 16, "女", "軟工")); studentList.add(new Student(4, "Dora", 14, "女", "計(jì)科")); studentList.add(new Student(5, "Bob", 20, "男", "軟工")); studentList.add(new Student(6, "Farrah", 15, "女", "計(jì)科")); studentList.add(new Student(7, "Helen", 13, "女", "軟工")); studentList.add(new Student(8, "jerry", 12, "男", "計(jì)科")); studentList.add(new Student(9, "Adam", 20, "男", "計(jì)科"));
例1:封裝一個(gè)分頁(yè)函數(shù)
/**
* 分頁(yè)方法
*
* @param list 要分頁(yè)的數(shù)據(jù)
* @param pageNo 當(dāng)前頁(yè)
* @param pageSize 頁(yè)大小
*/
public static <T> List<T> page(Collection<T> list, long pageNo, long pageSize) {
if (Objects.isNull(list) || list.isEmpty()) {
return Collections.emptyList();
}
return list.stream()
.skip((pageNo - 1) * pageSize)
.limit(pageSize)
.collect(Collectors.toList());
}
List<Student> pageData = page(studentList, 1, 3);
System.out.println(pageData);
/*
輸出:
[
Student(id=1, name=tom, age=19, sex=男, className=軟工),
Student(id=2, name=lisa, age=15, sex=女, className=軟工),
Student(id=3, name=Ada, age=16, sex=女, className=軟工)
]
*/
例2:獲取軟工班全部的人員id
List<Integer> idList = studentList.stream()
.filter(e -> Objects.equals(e.getClassName(), "軟工"))
.map(Student::getId)
.collect(Collectors.toList());
System.out.println(idList);
/*
輸出:
[1, 2, 3, 5, 7]
*/
例3:收集每個(gè)班級(jí)中的人員名稱列表
Map<String, List<String>> map = studentList.stream()
.collect(Collectors.groupingBy(
Student::getClassName,
Collectors.mapping(Student::getName, Collectors.toList())
));
System.out.println(map);
/*
輸出:
{
計(jì)科=[Dora, Farrah, jerry, Adam],
軟工=[tom, lisa, Ada, Bob, Helen]
}
*/
例4:統(tǒng)計(jì)每個(gè)班級(jí)中的人員個(gè)數(shù)
Map<String, Long> map = studentList.stream()
.collect(Collectors.groupingBy(
Student::getClassName,
Collectors.mapping(Function.identity(), Collectors.counting())
));
System.out.println(map);
/*
輸出:
{
計(jì)科=4,
軟工=5
}
*/
例5:獲取全部女生的名稱
List<String> allFemaleNameList = studentList.stream()
.filter(stu -> Objects.equals("女", stu.getSex()))
.map(Student::getName)
.collect(Collectors.toList());
System.out.println(allFemaleNameList);
/*
輸出:
[lisa, Ada, Dora, Farrah, Helen]
*/
例6:依照年齡排序
// 年齡升序排序
List<Student> stuList1 = studentList.stream()
// 升序
.sorted(Comparator.comparingInt(Student::getAge))
.collect(Collectors.toList());
System.out.println(stuList1);
/*
輸出:
[
Student(id=8, name=jerry, age=12, sex=男, className=計(jì)科),
Student(id=7, name=Helen, age=13, sex=女, className=軟工),
Student(id=4, name=Dora, age=14, sex=女, className=計(jì)科),
Student(id=2, name=lisa, age=15, sex=女, className=軟工),
Student(id=6, name=Farrah, age=15, sex=女, className=計(jì)科),
Student(id=3, name=Ada, age=16, sex=女, className=軟工),
Student(id=1, name=tom, age=19, sex=男, className=軟工),
Student(id=5, name=Bob, age=20, sex=男, className=軟工),
Student(id=9, name=Adam, age=20, sex=男, className=計(jì)科)
]
*/
// 年齡降序排序
List<Student> stuList2 = studentList.stream()
// 降序
.sorted(Comparator.comparingInt(Student::getAge).reversed())
.collect(Collectors.toList());
System.out.println(stuList2);
/*
輸出:
[
Student(id=5, name=Bob, age=20, sex=男, className=軟工),
Student(id=9, name=Adam, age=20, sex=男, className=計(jì)科),
Student(id=1, name=tom, age=19, sex=男, className=軟工),
Student(id=3, name=Ada, age=16, sex=女, className=軟工),
Student(id=2, name=lisa, age=15, sex=女, className=軟工),
Student(id=6, name=Farrah, age=15, sex=女, className=計(jì)科),
Student(id=4, name=Dora, age=14, sex=女, className=計(jì)科),
Student(id=7, name=Helen, age=13, sex=女, className=軟工),
Student(id=8, name=jerry, age=12, sex=男, className=計(jì)科)
]
*/
例7:分班級(jí)依照年齡排序
該例中和例3類似的處理,都使用到了downstream下游 - 收集器
Map<String, List<Student>> map = studentList.stream()
.collect(
Collectors.groupingBy(
Student::getClassName,
Collectors.collectingAndThen(Collectors.toList(), arr -> {
return arr.stream()
.sorted(Comparator.comparingInt(Student::getAge))
.collect(Collectors.toList());
})
)
);
/*
輸出:
{
計(jì)科 =[
Student(id = 8, name = jerry, age = 12, sex = 男, className = 計(jì)科),
Student(id = 4, name = Dora, age = 14, sex = 女, className = 計(jì)科),
Student(id = 6, name = Farrah, age = 15, sex = 女, className = 計(jì)科),
Student(id = 9, name = Adam, age = 20, sex = 男, className = 計(jì)科)
],
軟工 =[
Student(id = 7, name = Helen, age = 13, sex = 女, className = 軟工),
Student(id = 2, name = lisa, age = 15, sex = 女, className = 軟工),
Student(id = 3, name = Ada, age = 16, sex = 女, className = 軟工),
Student(id = 1, name = tom, age = 19, sex = 男, className = 軟工),
Student(id = 5, name = Bob, age = 20, sex = 男, className = 軟工)
]
}
*/
本例中使用到的downstream的方式更為通用,可以實(shí)現(xiàn)絕大多數(shù)的功能,例3中的方法JDK提供的簡(jiǎn)寫方式
下面是用collectingAndThen的方式實(shí)現(xiàn)和例3相同的功能
Map<String, Long> map = studentList.stream()
.collect(
Collectors.groupingBy(
Student::getClassName,
Collectors.collectingAndThen(Collectors.toList(), arr -> {
return (long) arr.size();
})
)
);
/*
輸出:
{
計(jì)科=4,
軟工=5
}
*/
例8:將數(shù)據(jù)轉(zhuǎn)為ID和Name對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)Map
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(Student::getId, Student::getName));
System.out.println(map);
/*
輸出:
{
1=tom,
2=lisa,
3=Ada,
4=Dora,
5=Bob,
6=Farrah,
7=Helen,
8=jerry,
9=Adam
}
*/
情況1
上面代碼,在現(xiàn)有的數(shù)據(jù)下正常運(yùn)行,當(dāng)添加多添加一條數(shù)據(jù)
studentList.add(new Student(9, "Adam - 2", 20, "男", "計(jì)科"));
這個(gè)時(shí)候id為9的數(shù)據(jù)有兩條了,這時(shí)候再運(yùn)行上面的代碼就會(huì)出現(xiàn)Duplicate key Adam
也就是說調(diào)用toMap時(shí),假設(shè)其中存在重復(fù)的key,如果不做任何處理,會(huì)拋異常
解決異常就要引入toMap方法的第3個(gè)參數(shù)mergeFunction,函數(shù)式接口方法簽名如下
R apply(T t, U u);
代碼修改后如下
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(Student::getId, Student::getName, (v1, v2) -> {
System.out.println("value1: " + v1);
System.out.println("value2: " + v2);
return v1;
}));
/*
輸出:
value1: Adam
value2: Adam - 2
{1=tom, 2=lisa, 3=Ada, 4=Dora, 5=Bob, 6=Farrah, 7=Helen, 8=jerry, 9=Adam}
*/
可以看出來mergeFunction參數(shù)v1為原值,v2為新值
日常開發(fā)中是必須要考慮第3參數(shù)的mergeFunction,一般采用策略如下
// 參數(shù)意義: o 為原值(old),n 為新值(new)
studentList.stream()
// 保留策略
.collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> o));
studentList.stream()
// 覆蓋策略
.collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> n));
在原有的數(shù)據(jù)下增加一條特殊數(shù)據(jù),這條特殊數(shù)據(jù)的name為null
studentList.add(new Student(10, null, 20, "男", "計(jì)科"));
此時(shí)原始代碼和情況1的代碼都會(huì)出現(xiàn)空指針異常
解決方式就是toMap的第二參數(shù)valueMapper返回值不能為null,下面是解決的方式
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(
Student::getId,
e -> Optional.ofNullable(e.getName()).orElse(""),
(o, n) -> o
));
System.out.println(map);
/*
輸出:
{1=tom, 2=lisa, 3=Ada, 4=Dora, 5=Bob, 6=Farrah, 7=Helen, 8=jerry, 9=Adam, 10=}
*/
// 此時(shí)沒有空指針異常了
還有一種寫法(參考寫法,不用idea工具編寫代碼,這種寫法沒有意義)
public final class Func {
/**
* 當(dāng) func 執(zhí)行結(jié)果為 null 時(shí), 返回 defaultValue
*
* @param func 轉(zhuǎn)換函數(shù)
* @param defaultValue 默認(rèn)值
* @return
*/
public static <T, R> Function<T, R> defaultValue(@NonNull Function<T, R> func, @NonNull R defaultValue) {
Objects.requireNonNull(func, "func不能為null");
Objects.requireNonNull(defaultValue, "defaultValue不能為null");
return t -> Optional.ofNullable(func.apply(t)).orElse(defaultValue);
}
}
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(
Student::getId,
Func.defaultValue(Student::getName, null),
(o, n) -> o
));
System.out.println(map);
這樣寫是為了使用像idea這樣的工具時(shí),Func.defaultValue(Student::getName, null)調(diào)用第二個(gè)參數(shù)傳null會(huì)有一個(gè)告警的標(biāo)識(shí)『不關(guān)閉idea的檢查就會(huì)有warning提示』。
綜上就是toMap的使用注意點(diǎn),
key映射的id有不能重復(fù)的限制,value映射的name也有不能有null,解決方式也在下面有提及
例9:封裝一下關(guān)于Stream的工具類
工作中使用Stream最多的操作都是對(duì)于集合來的,有時(shí)Stream使用就是一個(gè)簡(jiǎn)單的過濾filter或者映射map操作,這樣就出現(xiàn)了大量的.collect(Collectors.toMap(..., ..., ...))和.collect(Collectors.toList()),有時(shí)還要再調(diào)用之前檢測(cè)集合是否為null,下面就是對(duì)Stream的單個(gè)方法進(jìn)行封裝
public final class CollUtils {
/**
* 過濾數(shù)據(jù)集合
*
* @param collection 數(shù)據(jù)集合
* @param filter 過濾函數(shù)
* @return
*/
public static <T> List<T> filter(Collection<T> collection, Predicate<T> filter) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
return collection.stream()
.filter(filter)
.collect(Collectors.toList());
}
/**
* 獲取指定集合中的某個(gè)屬性
*
* @param collection 數(shù)據(jù)集合
* @param attrFunc 屬性映射函數(shù)
* @return
*/
public static <T, R> List<R> attrs(Collection<T> collection, Function<T, R> attrFunc) {
return attrs(collection, attrFunc, true);
}
/**
* 獲取指定集合中的某個(gè)屬性
*
* @param collection 數(shù)據(jù)集合
* @param attrFunc 屬性映射函數(shù)
* @param filterEmpty 是否過濾空值 包括("", null, [])
* @return
*/
public static <T, R> List<R> attrs(Collection<T> collection, Function<T, R> attrFunc, boolean filterEmpty) {
if (isEmpty(collection)) {
return Collections.emptyList();
}
Stream<R> rStream = collection.stream().map(attrFunc);
if (!filterEmpty) {
return rStream.collect(Collectors.toList());
}
return rStream.filter(e -> {
if (Objects.isNull(e)) {
return false;
}
if (e instanceof Collection) {
return !isEmpty((Collection<?>) e);
}
if (e instanceof String) {
return ((String) e).length() > 0;
}
return true;
}).collect(Collectors.toList());
}
/**
* 轉(zhuǎn)換為map, 有重復(fù)key時(shí), 使用第一個(gè)值
*
* @param collection 數(shù)據(jù)集合
* @param keyMapper key映射函數(shù)
* @param valueMapper value映射函數(shù)
* @return
*/
public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<T, K> keyMapper,
Function<T, V> valueMapper) {
if (isEmpty(collection)) {
return Collections.emptyMap();
}
return collection.stream()
.collect(Collectors.toMap(keyMapper, valueMapper, (k1, k2) -> k1));
}
/**
* 判讀集合為空
*
* @param collection 數(shù)據(jù)集合
* @return
*/
public static boolean isEmpty(Collection<?> collection) {
return Objects.isNull(collection) || collection.isEmpty();
}
}
如果單次使用Stream都在一個(gè)函數(shù)中可能出現(xiàn)大量的冗余代碼,如下
// 獲取id集合
List<Integer> idList = studentList.stream()
.map(Student::getId)
.collect(Collectors.toList());
// 獲取id和name對(duì)應(yīng)的map
Map<Integer, String> map = studentList.stream()
.collect(Collectors.toMap(Student::getId, Student::getName, (k1, k2) -> k1));
// 過濾出 軟工 班級(jí)的人員
List<Student> list = studentList.stream()
.filter(e -> Objects.equals(e.getClassName(), "軟工"))
.collect(Collectors.toList());
使用工具類
// 獲取id集合 List<Integer> idList = CollUtils.attrs(studentList, Student::getId); // 獲取id和name對(duì)應(yīng)的map Map<Integer, String> map = CollUtils.toMap(studentList, Student::getId, Student::getName); // 過濾出 軟工 班級(jí)的人員 List<Student> list = CollUtils.filter(studentList, e -> Objects.equals(e.getClassName(), "軟工"));
工具類旨在減少單次使用Stream時(shí)出現(xiàn)的冗余代碼,如toMap和toList,同時(shí)也進(jìn)行了為null判斷
總結(jié)
本篇介紹了函數(shù)式接口,Lambda,Optional,方法引用, Stream等一系列知識(shí)點(diǎn)
也是工作中經(jīng)過長(zhǎng)時(shí)間積累終結(jié)下來的,比如例5中每一個(gè)操作都換一行,這樣不完全是為了格式化好看
List<String> allFemaleNameList = studentList.stream()
.filter(stu -> Objects.equals("女", stu.getSex()))
.map(Student::getName)
.collect(Collectors.toList());
System.out.println(allFemaleNameList);
// 這樣寫 .filter 和 .map 的函數(shù)表達(dá)式中報(bào)錯(cuò)可以看出來是那一行
如果像下面這樣寫,報(bào)錯(cuò)是就會(huì)指示到一行上不能直接看出來是.filter還是.map報(bào)的錯(cuò),并且這樣寫也顯得擁擠
List<String> allFemaleNameList = studentList.stream().filter(stu -> Objects.equals("女", stu.getSex())).map(Student::getName).collect(Collectors.toList());
System.out.println(allFemaleNameList);
Stream的使用遠(yuǎn)遠(yuǎn)不止本篇文章介紹到的,比如一些同類的IntStream,LongStream,DoubleStream都是大同小異,只要把Lambda搞熟其他用法都一樣
學(xué)習(xí)Stream流一定要結(jié)合場(chǎng)景來,同時(shí)也要注意Stream需要規(guī)避的一些風(fēng)險(xiǎn),如toMap的注意點(diǎn)(例8有詳細(xì)介紹)。
還有一些高級(jí)用法downstream下游 - 收集器等(例4,例7)。
以上就是JDK8中Stream使用解析的詳細(xì)內(nèi)容,更多關(guān)于JDK8中Stream使用的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
idea中創(chuàng)建maven的Javaweb工程并進(jìn)行配置(圖文教程)
這篇文章主要介紹了idea中創(chuàng)建maven的Javaweb工程并進(jìn)行配置,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),文中給大家提到了tomcat的運(yùn)行方法,具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02
SpringBoot Event實(shí)現(xiàn)異步消費(fèi)機(jī)制的示例代碼
這篇文章主要介紹了SpringBoot Event實(shí)現(xiàn)異步消費(fèi)機(jī)制,ApplicationEvent以及Listener是Spring為我們提供的一個(gè)事件監(jiān)聽、訂閱的實(shí)現(xiàn),內(nèi)部實(shí)現(xiàn)原理是觀察者設(shè)計(jì)模式,文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下2024-04-04
Java?Spring?Dubbo三種SPI機(jī)制的區(qū)別
這篇文章主要介紹了Java?Spring?Dubbo三種SPI機(jī)制的區(qū)別,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,感興趣的小伙伴可以參考一下2022-08-08
Java簡(jiǎn)單計(jì)算兩個(gè)日期月數(shù)差的方法
這篇文章主要介紹了Java簡(jiǎn)單計(jì)算兩個(gè)日期月數(shù)差的方法,結(jié)合實(shí)例形式分析了java使用Calendar類進(jìn)行日期時(shí)間操作相關(guān)技巧,需要的朋友可以參考下2017-06-06
Java實(shí)現(xiàn)多個(gè)wav文件合成一個(gè)的方法示例
這篇文章主要介紹了Java實(shí)現(xiàn)多個(gè)wav文件合成一個(gè)的方法,涉及java文件流讀寫、編碼轉(zhuǎn)換、解析等相關(guān)操作技巧,需要的朋友可以參考下2019-05-05
java基于包結(jié)構(gòu)的請(qǐng)求路由實(shí)現(xiàn)實(shí)例分享
基于包結(jié)構(gòu)的請(qǐng)求路由簡(jiǎn)單實(shí)現(xiàn)實(shí)例分享,大家參考使用吧2013-12-12
Spring Boot設(shè)置支持跨域請(qǐng)求過程詳解
這篇文章主要介紹了Spring Boot設(shè)置支持跨域請(qǐng)求過程詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
java實(shí)現(xiàn)圖片轉(zhuǎn)base64字符串 java實(shí)現(xiàn)base64字符串轉(zhuǎn)圖片
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)圖片轉(zhuǎn)base64字符串,java實(shí)現(xiàn)base64字符串轉(zhuǎn)圖片,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02

