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