Java 8 Stream 使用總結
我一直以為,Stream 我接觸的算晚了,可在工作中漸漸發現,儘管很多同事手握 Java8,但仍然遵循著傳統的程式設計模式,並未充分利用 Java 8 新的特性。所以,這篇文章將談談 Stream 實戰,並在實戰中引出少部分概念。
文章通過兩個用例,一個是如何從容器物件構造 Stream 的用例,另一個則是如何使用 Stream 的用例,通過這兩個用例,你可以收穫 Stream 的使用姿勢。
容器物件構造 Stream 用例
// Construct Stream
// 1
Integer[] arrays = {1, 2, 3};
Stream<Integer> integerStream1 = Stream.of(arrays);
// 2
Stream<Integer> integerStream2 = Stream.of(1, 2, 3);
// 3
Stream<Integer> integerStream3 = Stream.<Integer>builder()
.add(1).add(2).add(3).build();
// 4
IntStream intStream = IntStream.range(1, 4);
Stream 的構造非常簡單,不過需要注意的是,除了 Stream 外,還有 IntStream, LongStream, DoubleStream 這類基本型別對應的流,這些流中增加了一些求和,求平均值等操作,並做了一些優化。
// Stream constructed by collection class
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 1
Stream<Integer> listStream = list.stream();
// 2
Stream<Integer> parallelStream =list.parallelStream();
集合介面中新增了 stream
預設方法,呼叫該方法即可返回 Stream 物件。parallelStream
在前文中,我們也介紹過介面的預設方法,並提到它是為了方便這些新增的方法加入原有設計介面,並保持相容的。
使用 Stream 用例
遍歷
list.stream().forEach(System.out::println);
list.stream().forEachOrdered(System.out::println);
list.stream().peek(System.out::println).count();
上述程式碼是對流中的元素進行遍歷。注意,建立的流物件不能重複使用,再次使用需要重新建立。
peek 方法為什麼需要再呼叫一個 count 操作呢?這是因為 peek 方法是一箇中間操作,並不會立馬執行。forEach 和 forEachOrdered 都是終結操作,會立馬執行。所以 peek 方法需要再呼叫一個終結操作的方法來觸發程式碼執行。
peek 方法這樣使用並不推薦,這種使用方式在文件中被描述為 “副作用”,也就是並未合理地使用方法。
流的方法是否為終結操作可以通過文件檢視。不過在日常使用中,我們按照方法的正常的呼叫邏輯來思考即可,比如,使用流對元素進行多種操作,包括後續介紹的過濾等,並不會多次的遍歷流,因為多次遍歷帶來的效能損耗是不能接受的。
計算
統計元素個數:
// 統計元素個數
println(list.stream().count()); //計數
匹配元素,返回 true 或 false
// 流中的所有元素都小於 4 ,則返回 true
println(list.stream().allMatch(e -> e < 4));
// 流中的任意一個元素等於 1 ,則返回 true
println(list.stream().anyMatch(e -> e == 1));
// 流中沒有一個元素等於 1,則返回 true
println(list.stream().noneMatch(e -> e == 1));
查詢元素
// 隨機返回一個元素,如果沒有的話,則返回 -1
println(list.stream().findAny().orElse(-1));
// 返回第一個元素,如果沒有的話,返回 -1
println(list.stream().findFirst().orElse(-1));
// 返回最大值,沒有的話,返回 -1
println(list.stream().max(Comparator
.comparingInt(Integer::intValue)).orElse(-1));
// 返回最小值,沒有的話,返回 -1
println(list.stream().min(Comparator
.comparingInt(Intger::intValue)).orElse(-1));
findAny
方法的行為是不確定的,所以,利用它的隨機性獲取這一特性不太可取,它主要是為了最大化實現並行流操作的效能而設計的。
這類方法返回的都是 Optional
類,將該類用作呼叫獲取實體物件方法的返回值時,可以非常有效的避免 NPE 問題,這是一個非常值得學習的編碼習慣。
注意:不建議將任何的 Optional 型別作為欄位或引數,optional 設計為:有限的機制讓類庫方法返回值清晰的表達 “沒有值”。 optional 是不可被序列化的,如果類是可序列化的就會出問題。
也不建議將其用作獲取集合物件的方法返回,獲取集合物件的方法為了避免 NPE ,建議返回空集合。
數值計算:
這裡的 Student 類以及 studentList 在緊接著的下文給出,不過有著豐富經驗的你,應該能猜到它們指的是什麼。
//數值計算
// 求和
int ageSum1 = studentList.stream().mapToInt(Student::getAge).sum();
int ageSum2 = studentList.stream()
.map(Student::getAge).mapToInt(Integer::intValue).sum();
int ageSum3 = studentList.stream()
.map(Student::getAge).flatMapToInt(IntStream::of).sum();
// 平均值
double averageAge =studentList.stream()
.mapToInt(Student::getAge).averae().orElse(0.0);
sum 以及 average 是在基本型別的流中才有的方法。這裡是將物件流轉換為基本型別的流,即 Stream 轉換為 IntStream。
轉換
普通實體類 Studeng.java:
public static class Student{
private String name;
private Integer age;
public Student(String name, Integer age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
多個 Student 物件通過 Stream 組裝為 List:
// 將多個物件組裝為 list
List<Student> studentList = Stream.of(new Student("老大", 20)
, new Student("老二", 18), new Student("老三", 16))
.collect(Collectors.toList());
過濾並轉換為 List, Set, map:
// 返回 (studentList 中 年齡小於 18) 的 list
List<Student> studentList1 = studentList.stream().filter(student ->
student.getAge() < 18
).collect(Collectors.toList());
// 返回 (studentList 中 年齡小於 18) 的 set
Set<Student> studentSet = studentList.stream().filter(student ->
student.getAge() < 18
).collect(Collectors.toSet());
// 返回 (studentList 中 年齡小於 18 ,且 name 到 age) 的 map
Map<String, Integer> nameAgeMap = studentList.stream().filter(student ->
student.getAge() < 18
).collect(Collectors.toMap(Student::getName, Student::getAge, (u1, u2) -> u2));
// 返回 (studentList 中 年齡小於 18 ,且 name 到 age) 的有序的 map
Map<String, Integer> nameAgeSortedMap = studentList.stream().filter(student ->
student.getAge() < 18
).collect(Collectors.toMap(Student::getName,Student::getAge, (u1, u2) -> u2,LinkedHashMap::new));
轉換為 Map 的過載方法比較多,這是因為它需要考慮在 key 衝突後,如何儲存值,即 (u1, u2) -> u2。自定義該 Lambda 表示式,可以決定當 key 重複時,如何選擇 value 值。
LinkedHashMap::new
方法引用產生的 Map,將決定最後生成的 Map 的實現類。
不過再多的過載方法,都無法逃脫一個限制,那就是 key 或者 value 不能為 null。可在 HashMap 容器中,兩者是可以為 null 的。所以,為了做到這個,我們需要選擇使用原生的 collect 方法,而不是類庫提供給我們的便捷的 Collectors:
Map<String, Integer> nameAgeMap = studentList.stream()
.collect(HashMap::new, (map, ele) -> map.put(ele.getName(), ele.getAge()), HashMap::putAll);
這裡可以得到一個 Map<String, Integer>
物件,可能是和提升的型別推斷有關,後續會有相關文章介紹這個特性。
那麼,如果返回的 Set 介面 也想使用不同的實現類呢?(Collectors.toSet()
最終返回的是 HashSet
)我們可以使用下面的方法:
// 返回 (studentList 中 年齡小於 18) 的 LinkedHashSet
Set<Student> studentLinkedHashSet = studentList.stream().filter(student ->
student.getAge() < 18
).collect(LinkedHashSet::new, Set::add, Set::addAll);
collect 方法除了接收 Colltors 提供的已經封裝好的物件外,還支援自定義。LinkedHashSet::new
代表生成的容器物件,Set::add
代表如何往容器中新增元素,Set::addAll
代表在並行流中,如何合併兩個容器。前文為了解決生成的 HashMap key 不能為 null 的問題,我們已自定義實現過。
轉換為持有不同元素型別的 List:
// 從 Student 集合中返回一個去重且有序的 age 集合
List<Integer> ageSorted = studentList.stream()
.map(Student::getAge).distinct().sorted()
.collect(Colectors.toList());
map 方法接收一個 Lambda 表示式,表示式最終產生的值的型別將更改當前 Stream 的引數化型別。例如,在這裡 studentList.stream() 返回的是一個 Stream ,但經過 map(Student::getAge) 方法後,產生的是一個 Stream ,所以,最終產生的 List 的引數化型別是 Integer。
實現分頁
// 實現分頁: 第一頁開始,每頁 2 條,這裡返回第二頁的資料
List<Student> pagingStudentList = studentList.stream()
.skip(2).limit(2)
.collect(Collectors.toList());
分組
// 相同年齡的 Student,即 age -> Students 的 Map
Map<Integer, List<Student>> ageStudentMap =studentList.stream()
.collect(Collectors.groupingBy(Sudent::getAge, Collectors.toList()));
根據 age 進行分組,返回的 Map 中 value 為 List<Student>
。 Collectors.toList()
也可以替換為 Collectors.mapping(Student::getName, Collectors.toList())
。
// 相同年齡的 Student,即 age -> names 的 Map
Map<Integer, List<String>> ageNamesMap = studentList.stream()
n.collect(Collectors.groupingBy(Student::getAge,
Collectors.mapping(Student::getName, Collectors.toList())));
這時,返回的 value 由 List<Student>
轉換為了 List<String>
。
寫在最後
其實,在日常中,Stream 使用的多了,也就熟練了。不過文章提到的很多小的點,也還是需要多瞭解一二,這樣在使用的時候就完全能夠遊刃有餘啦。
這是 Java 8 系列的第二篇文章,這篇注重實戰,介紹了很多 Stream 的使用方式,我也嘗試著按自己的方式分了類,但難免有紕漏之處,還請見諒。如果你覺得我的文章還不錯,並對後續文章感興趣的話,或者說我們有什麼能夠交流分享的,可以通過掃描下方二維碼來關注我的公眾號!