Java8 新特性之集合的流式程式設計
文章目錄
一、集合流的概述
1.1 集合的流式程式設計簡介
Stream 是 JDK1.8 之後出現的新特性,也是 JDK1.8 新特性中最值得學習的兩種新特性之一。(另外一個是 Lambda 表示式)
Stream 是對集合操作的增強,流不是集合的元素,不是一種資料結構,不負責資料的儲存。流更像是一個迭代器,可以單向的遍歷一個集合中的每個元素,並且不可迴圈。
1.2 集合的流式程式設計的優點
為什麼要用集合的流式程式設計呢?想必它一定有值得人們去關注的優點。
有些時候,對集合的元素進行操作的時候,需要使用到其他操作的結果。在這個過程中,集合的流式程式設計可以大幅度的簡化程式碼的數量。將資料來源中的資料讀取到一個流中,可以對這個流中的資料進行操作(刪除、過濾、對映…)。每次的操作結果也是一個流物件,可以對這個流再進行其他的操作。
1.3 使用流式程式設計的步驟
通常情況下,對集合中的資料使用流式程式設計,需要經過以下三步:
- 獲取資料來源,將資料來源中的資料讀取到流中;
- 對流中的資料進行各種各樣的處理;
- 對流中的資料進行整合處理。
在上述三個過程中:對於過程 2,有若干方法可以對流中的資料進行各種各樣的操作,並且返回流物件本身,這樣的操作,被稱為中間操作。對於過程 3,有若干方法可以對流中的資料進行各種處理,並且關閉流,這樣的操作,被稱為最終操作。
在中間操作和最終操作中,基本上所有的方法引數都是函式式介面,可以使用 Lambda 表示式來實現。使用集合的流式程式設計來簡化程式碼量,需要對 Lambda 表示式做到熟練掌握。
二、資料來源的獲取
2.1 資料來源簡介
資料來源,顧名思義,就是流中的資料的來源。讀取資料來源是流式程式設計的第一步。注意:資料被讀取到流中之後,流中進行處理的資料與資料來源中的資料沒有關係。也就是說,中間操作對流中的資料進行處理、過濾、對映、排序等操作是不會影響到資料來源中的資料的。
2.2 資料來源的獲取
這個過程,實際上是將一個容器中的資料讀取到另一個流中。因此無論什麼容器作為資料來源,讀取到流中的方法的返回值一定是一個 Stream。
如下所示為資料來源的獲取的示例程式碼:
/**
* @Description 將集合作為資料來源,讀取集合中的資料到一個流中
*/
private static void collectionDataSource(){
// 1. 例項化一個集合
ArrayList<Integer> list = new ArrayList<>();
// 2. 填充元素到集合中
Collections.addAll(list, 1, 2, 3, 4, 5);
// 3. 讀取集合中的元素到流中
Stream<Integer> stream = list.stream();
System.out.println(stream);
}
/**
* @Description 將陣列作為資料來源,讀取陣列中的資料到一個流中
*/
private static void arrayDataSource(){
// 1. 例項化一個數組
Integer[] arr = new Integer[]{1, 2, 3, 4, 5};
// 2. 讀取陣列中的資料到流中
Stream<Integer> stream = Arrays.stream(arr);
System.out.println(stream);
}
三、最終操作
3.1 最終操作簡介
最終操作就是將流中的資料整合到一起,比如可以存入到一個集合中,也可以直接對流中的資料進行遍歷、統計等。通過最終操作,可以從流中提取出來我們想要的資訊。
Stream
流的最終操作主要有以下幾種:
【注意事項】
之所以叫最終操作,是因為在最終操作結束之後,會關閉這個流,流中所有的資料都會被銷燬。如果使用一個已經關閉了的流,會出現異常。
3.2 collect
collect()
方法的作用就是將流中的資料收集到一起,最常見的處理就是將流中的資料存入一個集合中。collect
方法的引數是一個 Collector 介面,而且這個介面並不是一個函式式介面,因此我們可以自定義類來實現這個介面。但是在絕大多數情況下,是不需要我們自定義的,我們藉助工具類 Collectors
即可。
【舉例說明】
-
例一:將 List 集合中的資料讀取到流中,然後原封不動將流中資料存入 Set 集合中。
public class StreamDemo_02 { public static void main(String[] args) { Stream<Integer> stream = getDataSource(); Set<Integer> set = stream.collect(Collectors.toSet()); // 將 stream 中資料存入 set 集合中 System.out.println(set); } /** * 獲取資料來源 */ private static Stream<Integer> getDataSource(){ Integer[] list = new Integer[]{1, 2, 3, 4, 5}; return Arrays.stream(list); } }
執行結果如下:
-
例二:將 List 集合中的資料讀取到流中,然後原封不動將流中資料存入 Map 集合中。
public class StreamDemo_02 { public static void main(String[] args) { Stream<Integer> stream = getDataSource(); /** * 將資料存入 map 中,這裡用到了 lambda 表示式 * 這裡的 Collectors.toMap 的函式原型是: * Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, * Function<? super T, ? extends U> valueMapper) * 通過函式原型可以看到,函式的引數都是函式式介面 Function。 * 其中:引數一為 key 的對映,引數二為 value 的對映。 * 因此,可以使用 lambda 表示式,將 key 對映為集合元素的字串形式,將 value 對映為集合元素本身 */ Map<String, Integer> map = stream.collect(Collectors.toMap(x -> x.toString(), x -> x)); // 將stream中資料存入map中 System.out.println(map); } /** * 獲取資料來源 */ private static Stream<Integer> getDataSource(){ Integer[] list = new Integer[]{1, 2, 3, 4, 5}; return Arrays.stream(list); } }
執行結果如下:
3.3 reduce
reduce()
方法的作用是將流中的資料按照一定的規則聚合起來。
【舉例說明】
public static void main(String[] args) {
reduceUsage();
}
/**
* @Description 將流中的資料按照一定的規則聚合起來
*/
private static void reduceUsage(){
Stream<Integer> stream = getDataSource();
Integer optional = stream.reduce((x, y) -> x + y).get();
System.out.println(optional);
}
/**
* @Description 將一個容器中的資料讀取到流中
*/
private static Stream<Integer> getDataSource(){
Integer[] list = new Integer[]{1, 2, 3, 4, 5};
return Arrays.stream(list);
}
執行結果如下:
分析:為什麼結果是 15 呢?我們不難發現,15=1+2+3+4+5。也就是說 reduce
方法中引數 Lambda 表示式方法體為 x+y
的情況下相當於是對 List 集合內的元素進行迭代累加。
3.4 count
count()
方法的作用是統計流中元素的數量。
【舉例說明】
Stream<Integer> stream = getDataSource();
long count = stream.count();
System.out.println(count);
3.5 forEach
foreach()
方法的作用是遍歷流中的資料。
【舉例說明】
private static void forEachUsage(){
Stream<Integer> stream = getDataSource();
// 物件方法的引用:System.out::println <==> x->System.out.println(x)
stream.forEach(System.out::println); // 遍歷流中的資料並列印
}
3.6 max & min
max()
:該方法的作用是按照指定的物件比較的規則進行大小比較,得出流中最大的資料。
min()
:該方法的作用是按照指定的物件比較的規則進行大小比較,得出流中最小的資料。
【舉例說明】
private static void maxAndMinUsage(){
Stream<Integer> stream = getDataSource();
// 獲取流中的最大值
Integer max = stream.max(Integer::compareTo).get(); // 引用特殊方法
System.out.println(max);
// 獲取流中的最小值
// Integer min = stream.min(Integer::compareTo).get();
// System.out.println(min);
}
3.7 Matching
Matching 並不是一個方法,而是一組方法的合集。它包括以下三個方法:
allMatch()
:只有當流中所有的元素都匹配指定的規則時,才會返回 true。
anyMatch()
:只要流中存在一個數據滿足指定的規則,就會返回 true。
noneMatch()
:只有當流中所有的元素都不滿足指定的規則,才會返回 true。
【舉例說明】
/* 流中的元素為:[1, 2, 3, 4, 5] */
/* allMatch */
Stream<Integer> stream = getDataSource();
boolean match = stream.allMatch(x -> x > 0); // 所有元素都大於0,因此將會返回 true
/* anyMatch */
Stream<Integer> stream = getDataSource();
boolean match = stream.anyMatch(x -> x > 4); // 存在一個元素都大於4,因此將會返回 true
/* noneMatch */
Stream<Integer> stream = getDataSource();
boolean match = stream.noneMatch(x -> x > 2); // 存在元素大於2,因此將會返回 false
3.8 Find
同樣,Find 也不是一個單獨的方法,而是一組方法的合集。他包括以下兩個方法:
findFirst()
:從流中獲取一個元素,一般是獲取開頭元素。
findAny()
:從流中獲取一個元素,一般也是獲取開頭元素。
【注意事項】
這兩個方法,在絕大多數情況下是完全一致的,但是在多執行緒的環境下,findAny
和 findFirst
返回的結果可能不一樣。
【舉例說明】
private static void findUsage(){
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5);
/* findFirst */
Integer integer = list.stream().findFirst().get(); // 得到的是第一個元素
System.out.println("findFirst = " + integer);
/* 序列流 findAny */
Integer integer1 = list.stream().findAny().get(); // 得到的是第一個元素
System.out.println("serial findAny = " + integer1);
/* 並行流 findAny */
Integer integer2 = list.parallelStream().findAny().get(); // 得到第幾個元素不確定
System.out.println("parallel findAny = " + integer2);
}
執行結果如下:
3.9 特殊的流
我們上面使用的 Stream
是通用的流,其實 Java 8 還提供了一些特殊的流,包括:
IntStream
LongStream
DoubleStream
從名字可以看出來,這幾個特殊的流是為了針對特定的型別的元素進行操作而誕生的。因此,它們對於自己對應型別的元素可以提供相對於普通流更加便捷、高效、功能更為強大的操作。
關於這幾個特殊流的相關操作,可以參考 jdk1.8 的API。
四、中間操作
4.1 中間操作簡介
中間操作就是資料從資料來源讀取到流中以後,對流中的資料進行的各種各樣的操作。中間操作的每一個操作的返回值都是 Stream
物件,因此中間操作可以是連續操作。
那麼從上面中間操作的定義我們可以看出來,中間操作和最終操作之間的核心區別是:最終操作返回一特定型別的結果,中間操作返回流本身。
Stream
流的中間操作主要包括以下幾種:
4.2 filter
filter()
方法的作用是過濾出滿足指定條件的資料,不滿足條件的資料將會被刪除。
【舉例說明】
過濾出年齡大於 22 的學生。
public static void main(String[] args) {
filterUsage();
}
/**
* @Description 獲取流
*/
public static Stream<Student> getDataSource(){
ArrayList<Student> list = new ArrayList<>();
Collections.addAll(list,
new Student("蠻王", 13),
new Student("李青", 23),
new Student("亞索", 19),
new Student("趙信", 28));
return list.stream();
}
/**
* @Description 中間操作:filter
* 可以將流中的滿足指定條件的資料保留,刪除不滿足指定條件的資料
*/
public static void filterUsage(){
Stream<Student> stream = getDataSource();
stream.filter(stu -> stu.age > 22) // 中間操作:過濾出年齡大於 22 的學生
.forEach(System.out::println); // 最終操作:迭代列印資料
}
執行上面程式碼,結果如下:
可以看到,符合條件(年齡大於 22)的學生都已經被過濾出來並且迭代列印了。
4.3 distinct
distinct()
方法的作用是去除集合中重複的元素。該方法沒有引數,去重的規則與 HashSet
相同。
【注意事項】
HashSet
存入新元素時的去重規則如下圖所示:
- 在儲存元素時,首先呼叫
hashCode()
方法獲取雜湊值; - 如果雜湊值不在雜湊表中,就說明沒有重複元素,直接插入;
- 如果雜湊值在雜湊表中,再呼叫
equals()
方法比較; - 如果
equals()
比較的結果為一致,說明兩個元素相同,則丟棄元素; - 如果
equals()
比較的結果不一致,說明兩個元素不同,則插入元素到hashSet
集合中。
【舉例說明】
去除集合中重複的 Student
物件。此處所定義的重複指的是姓名與年齡的值相同的物件。
public class StreamDemo_03 {
public static void main(String[] args) {
distinctUsage();
}
/**
* @Description 學生類,儲存於集合中的資料型別
*/
public static class Student{
private String name;
private int age;
// 重寫 equals 方法,IDEA 中右鍵選單可直接生成
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
// 重寫 hashCode 方法,IDEA 中右鍵選單可直接生成
@Override
public int hashCode() {
return Objects.hash(name, age);
}
// getter and setter and toString
...
}
/**
* @Description 獲取流
*/
public static Stream<Student> getDataSource(){
ArrayList<Student> list = new ArrayList<>();
Collections.addAll(list,
new Student("蠻王", 13),
new Student("李青", 23),
new Student("亞索", 19),
new Student("亞索", 19), // 重複物件
new Student("趙信", 28));
return list.stream();
}
/**
* @Description 中間操作:distinct
* 去除流中重複的資料,去重規則與 HashSet 相同。
*/
public static void distinctUsage(){
Stream<Student> stream = getDataSource();
// 注:必須要重寫預設的 equals、hashCode 方法,相同姓名、年齡的物件才會被認為相同
stream.distinct()
.forEach(System.out::println);
}
}
執行結果如下:
可以看到,重複的物件(姓名為 “亞索”),已經被去除了一個。
4.4 sorted
sorted()
方法的作用是對流中的資料進行排序。sorted()
方法是一個過載方法,所有方法如下:
sorted()
:根據流中的元素對應的類中實現的Comparable
介面定義的比較規則來排序sorted(Comparator<T> comparator)
:將流中的資料按照引數介面指定的比較規則來排序
【舉例說明】
將流中的元素按照年齡大小從小到大排序輸出。
-
sorted()
方法public class StreamDemo_03 { public static void main(String[] args) { sortedUsage(); } /** * @Description 學生類,儲存於集合中的資料型別 */ public static class Student implements Comparable<Student>{ private String name; private int age; // 實現 Comparable 介面方法,定義比較規則 @Override public int compareTo(Student o) { return this.age - o.getAge(); } // getter and setter and toString ... } /** * @Description 獲取流 */ public static Stream<Student> getDataSource(){ ArrayList<Student> list = new ArrayList<>(); Collections.addAll(list, new Student("蠻王", 13), new Student("李青", 23), new Student("亞索", 19), new Student("趙信", 28)); return list.stream(); } /** * @Description 中間操作:sorted */ public static void sortedUsage(){ Stream<Student> stream = getDataSource(); stream.sorted() .forEach(System.out::println); } }
執行結果如下:
可以看到,所有的元素都按照年齡從小到大排好序了。 -
sorted(Comparator<T> comparator)
方法/* 使用有參的 sorted 方法無需讓 Student 類實現 Comparable 介面。 除了 Student 類和 sorted 方法呼叫部分程式碼外,其他程式碼與上面一致。 */ public static void sortedUsage(){ Stream<Student> stream = getDataSource(); stream.sorted((x,y)->x.getAge()-y.getAge()) // 在引數介面中自定義比較規則 .forEach(System.out::println); }
執行結果與上面無參方法結果一致。
4.5 limit & skip
limit()
:從流中第 1 個元素開始,擷取指定數量的元素。
skip()
:從流中第 1 個元素開始,跳過指定數量的元素,擷取剩餘的部分。
【舉例說明】
-
limit()
方法打印出年齡最小的兩個學生的資訊。
public static void limitUsage(){ Stream<Student> stream = getDataSource(); stream.sorted((o1,o2)->o1.getAge()-o2.getAge()) // 首先將元素按照年齡從小到大排序 .limit(2) // 從第 1 開始,擷取兩個元素 .forEach(System.out::println); }
執行結果如下:
-
skip()
方法打印出年齡第二小和第三小的兩個學生的資訊。
public static void skipUsage(){ Stream<Student> stream = getDataSource(); stream.sorted((o1,o2)->o1.getAge()-o2.getAge()) // 首先將元素按照年齡從小到大排序 .skip(1) // 跳過年齡第一小的學生 .limit(2) // 擷取年齡第二小、第三小的學生 .forEach(System.out::println); }
執行結果如下:
從上圖可以看到,年齡最小的元素被丟棄了,並且截取出了年齡第二、第三小的元素。
4.6 map & flatMap
map()
:根據自定義的對映規則,將流中的每個元素替換成新的元素。
flatMap()
:扁平化元素對映。一般是在 map()
對映完成之後,流中的每個元素可能都是一個容器,而我們需要對容器中的資料進行處理。此時使用扁平化元素對映操作,可以將流中的每個容器中的資料直接讀取到流中。
【舉例說明】
-
map()
方法將流中的每個元素替換成元素對應的學生的名字。
public static void mapusage(){ Stream<Student> stream = getDataSource(); stream.map(Student::getName) // Student::getName <==> o->o.getName() .forEach(System.out::println); }
執行結果如下:
-
flatMap()
方法找出一個字串陣列中出現過的所有字母。
private static void flatUsage() { String[] array = {"hello", "world"}; Stream<String> stream = Arrays.stream(array); stream.map(s->s.split("")) // 每個字串都對映成陣列:{"h","e","l","l","o"},{"w","o","r","l","d"} .flatMap(Arrays::stream) // 等價 arr->Arrays.stream(arr),將每個字串陣列轉換成 stream 後歸併 .distinct() // 去重 .forEach(System.out::println); }
執行結果如下:
五、Collectors 工具類
5.1 Collectors 工具類簡介
Collectors
是一個工具類,裡面封裝了許多方法。通過使用這些方法可以很方便的獲取到一個 Collector
介面的實現類物件,從而可以是使用 collect()
方法對流中的資料進行各種各樣的處理。
5.2 常用方法
Collectors
工具類封裝的常用方法如表格所示:
方法 | 描述 |
---|---|
toList() | 將流中的資料聚合到一個 List 集合中 |
toSet() | 將流中的資料聚合到一個 Set 集合中 |
toMap() | 將流中的資料聚合到一個 Map 集合中 |
maxBy() | 按照指定的規則,找到流中最大的元素 |
minBy() | 按照指定的規則,找到流中最小的元素 |
joining() | 將流中的資料拼接成一個字串(只能操作流中元素為 String 的資料) |
summingInt() | 將流中的資料對映成 int 型別的資料,然後求和 |
averagingInt() | 將流中的資料對映成 int 型別的資料,然後求均值 |
summarizingInt() | 將流中的資料對映成 int 型別的資料,然後獲取描述資訊 |
5.3 使用示例
以 joining()
方法為例,演示將多個字串拼接起來。
String[] array = {"are", "you", "ok"};
Stream<String> stream = Arrays.stream(array);
String collect = stream.collect(Collectors.joining(",", "[", "]")); // 拼接字串,以 , 為分隔,[、] 為前後綴
System.out.println(collect);
執行結果如下:
【參考內容】
千峰大資料——Java8 新特性