Stream流
在Java 8中,得益於Lambda所帶來的函數式編程,引入了一個全新的Stream概念,用於解決已有集合類庫既有的弊端
一、傳統遍歷
1、傳統集合的多步遍歷代碼
幾乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或間接的遍歷操作。而當我們需要對集合中的元素進行操作的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。
例如:
1 import java.util.ArrayList; 2 import java.util.List; 3
4 public class DemoForEach { 5 public static void main(String[] args) { 6 List<String> list = new ArrayList<>(); 7 list.add("張無忌"); 8 list.add("周芷若"); 9 list.add("趙敏"); 10 list.add("張強"); 11 list.add("張三豐"); 12 for (String name : list) { 13 System.out.println(name); 14 } 15 } 16 }
2、循環遍歷的弊端
Java 8的Lambda讓我們可以更加專注於做什么(What),而不是怎么做(How),這點此前已經結合內部類進行了對比說明。現在,我們仔細體會一下上例代碼,可以發現
-
-
- for循環的語法就是“怎么做”;
- for循環的循環體才是“做什么”
- for循環的語法就是“怎么做”;
-
為什么使用循環?因為要進行遍歷。但循環是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而並不是從第一個到最后一個順次處理的循環。前者是目的,后者是方式。
試想一下,如果希望對集合中的元素進行篩選過濾:
① 將集合A根據條件一過濾為子集B;
② 然后再根據條件二過濾為子集C;
Java8 之前的做法:
1 import java.util.ArrayList; 2 import java.util.List; 3 public class DemoNormalFilter { 4 public static void main(String[] args) { 5 List<String> list = new ArrayList<>(); 6 list.add("張無忌"); 7 list.add("周芷若"); 8 list.add("趙敏"); 9 list.add("張強"); 10 list.add("張三豐"); 11 List<String> zhangList = new ArrayList<>(); 12 for (String name : list) { 13 if (name.startsWith("張")) { 14 zhangList.add(name); 15 } 16 } 17 List<String> shortList = new ArrayList<>(); 18 for (String name : zhangList) { 19 if (name.length() == 3) { 20 shortList.add(name); 21 } 22 } 23 for (String name : shortList) { 24 System.out.println(name); 25 } 26 } 27 }
這段代碼中含有三個循環,每一個作用不同:
① 首先篩選出所有姓張的人;
② 然后篩選名字有三個字的人;
③ 最后進行對結果進行打印輸出。
每當我們需要對集合中的元素進行操作的時候,總是需要進行循環、循環、再循環。這是理所當然的么?
不是。循環是做事情的方式,而不是目的。另一方面,使用線性循環就意味着只能遍歷一次。如果希望再次遍歷,只能再使用另一個循環從頭開始。
3、Stream 的更優寫法
使用Java8的Stream API,代碼實現:
1 import java.util.ArrayList; 2 import java.util.List; 3
4 public class DemoStreamFilter { 5 public static void main(String[] args) { 6 List<String> list = new ArrayList<>(); 7 list.add("張無忌"); 8 list.add("周芷若"); 9 list.add("趙敏"); 10 list.add("張強"); 11 list.add("張三豐"); 12 list.stream() 13 .filter(s -> s.startsWith("張")) 14 .filter(s -> s.length() == 3) 15 .forEach(System.out::println); 16 } 17 }
直接閱讀代碼的字面意思即可完美展示無關邏輯方式的語義:獲取流、過濾姓張、過濾長度為3、逐一打印。
代碼中並沒有體現使用線性循環或是其他任何算法進行遍歷,我們真正要做的事情內容被更好地體現在代碼中。
二、流式思想概述
整體來看,流式思想類似於工廠車間的“生產流水線”。
當需要對多個元素進行操作(特別是多步操作)的時候,考慮到性能及便利性,應該首先拼好一個“模型”步驟方案,然后再按照方案去執行它。
這張圖中展示了過濾、映射、跳過、計數等多步操作,這是一種集合元素的處理方案,而方案就是一種“函數模型”。
圖中的每一個方框都是一個“流”,調用指定的方法,可以從一個流模型轉換為另一個流模型。而最右側的數字3是最終結果。
這里的 filter 、 map 、 skip 都是在對函數模型進行操作,集合元素並沒有真正被處理。只有當終結方法 count執行的時候,整個模型才會按照指定策略執行操作。而這得益於Lambda的延遲執行特性 。
注意:“Stream流”其實是一個集合元素的函數模型,它並不是集合,也不是數據結構,其本身並不存儲任何元素(或其地址值)。
Stream(流)是一個來自數據源的元素隊列:
-
- 元素是特定類型的對象,形成一個隊列。 Java中的Stream並不會存儲元素,而是按需計算。
- 數據源 流的來源。 可以是集合,數組 等。
和以前的Collection操作不同, Stream操作還有兩個基礎的特征:
-
- Pipelining: 中間操作都會返回流對象本身。 這樣多個操作可以串聯成一個管道, 如同流式風格(fluentstyle)。 這樣做可以對操作進行優化, 比如延遲執行(laziness)和短路( short-circuiting)。
- 內部迭代: 以前對集合遍歷都是通過Iterator或者增強for的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代。 Stream提供了內部迭代的方式,流可以直接調用遍歷方法。
當使用一個流的時候,通常包括三個基本步驟:
獲取一個數據源(source)→ 數據轉換→執行操作獲取想要的結果,
每次轉換原有 Stream 對象不改變,返回一個新的 Stream 對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道。
三、獲取流
java.util.stream.Stream<T> 是Java 8新加入的最常用的流接口。(這並不是一個函數式接口。)
獲取一個流非常簡單,有以下幾種常用的方式:
-
- 所有的 Collection 集合都可以通過 stream 默認方法獲取流;
- Stream 接口的靜態方法 of 可以獲取數組對應的流
- 所有的 Collection 集合都可以通過 stream 默認方法獲取流;
1、根據 Collection 獲取流
java.util.Collection 接口中加入了default方法 stream 用來獲取流,所以其所有實現類均可獲取流。
Demo:
1 import java.util .*; 2 import java.util.stream.Stream; 3
4 public class DemoGetStream { 5 public static void main(String[] args) { 6 List<String> list = new ArrayList<>(); 7 // ...
8 Stream<String> stream1 = list.stream(); 9 Set<String> set = new HashSet<>(); 10 // ...
11 Stream<String> stream2 = set.stream(); 12 Vector<String> vector = new Vector<>(); 13 // ...
14 Stream<String> stream3 = vector.stream(); 15 } 16 }
2、根據 Map 獲取流
java.util.Map 接口不是 Collection 的子接口,且其K-V數據結構不符合流元素的單一特征,所以獲取對應的流需要分key、value或entry等情況:
Demo:
1 import java.util.HashMap; 2 import java.util.Map; 3 import java.util.stream.Stream; 4
5 public class DemoGetStream { 6 public static void main(String[] args) { 7 Map<String, String> map = new HashMap<>(); 8 // ...
9 Stream<String> keyStream = map.keySet().stream(); 10 Stream<String> valueStream = map.values().stream(); 11 Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream(); 12 } 13 }
3、根據數組獲取流
如果使用的不是集合或映射而是數組,由於數組對象不可能添加默認方法,所以 Stream 接口中提供了靜態方法of ,使用很簡單:
Demo:
1 import java.util.stream.Stream; 2
3 public class DemoGetStream { 4 public static void main(String[] args) { 5 String[] array = {"張無忌", "張翠山", "張三豐", "張一元"}; 6 Stream<String> stream = Stream.of(array); 7 } 8 }
注意: of 方法的參數其實是一個可變參數,所以支持數組。
四、常用方法
流模型的操作很豐富,這里介紹一些常用的API。這些方法可以被分成兩種:
-
- 延遲方法:返回值類型仍然是 Stream 接口自身類型的方法,因此支持鏈式調用。(除了終結方法外,其余方法均為延遲方法。)
- 終結方法:返回值類型不再是 Stream 接口自身類型的方法,因此不再支持類似 StringBuilder 那樣的鏈式調用。本小節中,終結方法包括 count 和 forEach 方法。
1、逐一處理:forEach
雖然方法名字叫 forEach ,但是與for循環中的“for-each”昵稱不同。
void forEach(Consumer<? super T> action);
該方法接收一個 Consumer 接口函數,會將每一個流元素交給該函數進行處理。
Consumer 接口
java.util.function.Consumer<T>接口是一個消費型接口。
Consumer接口中包含抽象方法void accept(T t),意為消費一個指定泛型的數據。
基本使用:
1 import java.util.stream.Stream; 2 public class DemoStreamForEach { 3 public static void main(String[] args) { 4 Stream<String> stream = Stream.of("張無忌", "張三豐", "周芷若"); 5 stream.forEach(name‐> System.out.println(name)); 6 } 7 }
2、過濾:filter
可以通過 filter 方法將一個流轉換成另一個子集流。方法簽名:
Stream<T> filter(Predicate<? super T> predicate);
該接口接收一個 Predicate 函數式接口參數(可以是一個Lambda或方法引用)作為篩選條件。
Predicate 接口:
boolean test(T t);
該方法將會產生一個boolean值結果,代表指定的條件是否滿足。如果結果為true,那么Stream流的 filter 方法將會留用元素;如果結果為false,那么 filter 方法將會舍棄元素。
基本使用:
1 import java.util.stream.Stream; 2 public class DemoStreamFilter { 3 public static void main(String[] args) { 4 Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 5 Stream<String> result = original.filter(s ‐> s.startsWith("張")); 6 } 7 }
在這里通過Lambda表達式來指定了篩選的條件:必須姓張
3、映射:map
如果需要將流中的元素映射到另一個流中,可以使用 map 方法。方法簽名:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
該接口需要一個 Function 函數式接口參數,可以將當前流中的T類型數據轉換為另一種R類型的流。
Function 接口:
R apply(T t);
這可以將一種T類型轉換成為R類型,而這種轉換的動作,就稱為“映射”。
基本使用:
1 import java.util.stream.Stream; 2 public class DemoStreamMap { 3 public static void main(String[] args) { 4 Stream<String> original = Stream.of("10", "12", "18"); 5 Stream<Integer> result = original.map(str‐>Integer.parseInt(str)); 6 } 7 }
這段代碼中, map 方法的參數通過方法引用,將字符串類型轉換成為了int類型(並自動裝箱為 Integer 類對象)。
4、統計個數:count
正如舊集合 Collection 當中的 size 方法一樣,流提供 count 方法來數一數其中的元素個數:
long count();
該方法返回一個long值代表元素個數(不再像舊集合那樣是int值)。
基本使用:
1 import java.util.stream.Stream; 2 public class DemoStreamCount { 3 public static void main(String[] args) { 4 Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 5 Stream<String> result = original.filter(s ‐> s.startsWith("張")); 6 System.out.println(result.count()); // 2
7 } 8 }
5、取用前幾個:limit
limit 方法可以對流進行截取,只取用前n個。方法簽名
Stream<T> limit(long maxSize);
參數是一個long型,如果集合當前長度大於參數則進行截取;否則不進行操作。
基本使用:
1 import java.util.stream.Stream; 2 public class DemoStreamLimit { 3 public static void main(String[] args) { 4 Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 5 Stream<String> result = original.limit(2); 6 System.out.println(result.count()); // 2
7 } 8 }
6、跳過前幾個:skip
如果希望跳過前幾個元素,可以使用 skip 方法獲取一個截取之后的新流:
Stream<T> skip(long n);
如果流的當前長度大於n,則跳過前n個;否則將會得到一個長度為0的空流。
基本使用:
1 import java.util.stream.Stream; 2 public class DemoStreamSkip { 3 public static void main(String[] args) { 4 Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 5 Stream<String> result = original.skip(2); 6 System.out.println(result.count()); // 1
7 } 8 }
7、組合:concat
如果有兩個流,希望合並成為一個流,那么可以使用 Stream 接口的靜態方法 concat :
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
注意:這是一個靜態方法,與 java.lang.String 當中的 concat 方法是不同的。
基本使用:
1 import java.util.stream.Stream; 2 public class DemoStreamConcat { 3 public static void main(String[] args) { 4 Stream<String> streamA = Stream.of("張無忌"); 5 Stream<String> streamB = Stream.of("張翠山"); 6 Stream<String> result = Stream.concat(streamA, streamB); 7 } 8 }
五、案例
題目:現在有兩個 ArrayList 集合存儲隊伍當中的多個成員姓名, 依次進行以下若干操作步驟:
1. 第一個隊伍只要名字為3個字的成員姓名;存儲到一個新集合中。
2. 第一個隊伍篩選之后只要前3個人;存儲到一個新集合中。
3. 第二個隊伍只要姓張的成員姓名;存儲到一個新集合中。
4. 第二個隊伍篩選之后不要前2個人;存儲到一個新集合中。
5. 將兩個隊伍合並為一個隊伍;存儲到一個新集合中。
6. 根據姓名創建 Person 對象;存儲到一個新集合中。
7. 打印整個隊伍的Person對象信息。
兩個隊伍(集合)的代碼如下:
1 import java.util.ArrayList; 2 import java.util.List; 3 public class DemoArrayListNames { 4 public static void main(String[] args) { 5 //第一支隊伍
6 ArrayList<String> one = new ArrayList<>(); 7 one.add("迪麗熱巴"); 8 one.add("宋遠橋"); 9 one.add("蘇星河"); 10 one.add("石破天"); 11 one.add("石中玉"); 12 one.add("老子"); 13 one.add("庄子"); 14 one.add("洪七公"); 15 //第二支隊伍
16 ArrayList<String> two = new ArrayList<>(); 17 two.add("古力娜扎"); 18 two.add("張無忌"); 19 two.add("趙麗穎"); 20 two.add("張三豐"); 21 two.add("尼古拉斯趙四"); 22 two.add("張天愛"); 23 two.add("張二狗"); 24 // ....
25 } 26 }
Person 類的代碼為:
1 public class Person { 2 private String name; 3
4 public Person() { 5 } 6
7 public Person(String name) { 8 this.name = name; 9 } 10
11 @Override 12 public String toString() { 13 return "Person{name='" + name + "'}"; 14 } 15
16 public String getName() { 17 return name; 18 } 19
20 public void setName(String name) { 21 this.name = name; 22 } 23 }
方式一:使用傳統的for循環(或增強for循環)
代碼實現:
1 public class DemoArrayListNames { 2 public static void main(String[] args) { 3 List<String> one = new ArrayList<>(); 4 // ...
5 List<String> two = new ArrayList<>(); 6 // ... 7
8 // 第一個隊伍只要名字為3個字的成員姓名;
9 List<String> oneA = new ArrayList<>(); 10 for (String name : one) { 11 if (name.length() == 3) { 12 oneA.add(name); 13 } 14 } 15
16 // 第一個隊伍篩選之后只要前3個人;
17 List<String> oneB = new ArrayList<>(); 18 for (int i = 0; i < 3; i++) { 19 oneB.add(oneA.get(i)); 20 } 21
22 // 第二個隊伍只要姓張的成員姓名;
23 List<String> twoA = new ArrayList<>(); 24 for (String name : two) { 25 if (name.startsWith("張")) { 26 twoA.add(name); 27 } 28 } 29
30 // 第二個隊伍篩選之后不要前2個人;
31 List<String> twoB = new ArrayList<>(); 32 for (int i = 2; i < twoA.size(); i++) { 33 twoB.add(twoA.get(i)); 34 } 35
36 // 將兩個隊伍合並為一個隊伍;
37 List<String> totalNames = new ArrayList<>(); 38 totalNames.addAll(oneB); 39 totalNames.addAll(twoB); 40 // 根據姓名創建Person對象;
41 List<Person> totalPersonList = new ArrayList<>(); 42 for (String name : totalNames) { 43 totalPersonList.add(new Person(name)); 44 } 45 // 打印整個隊伍的Person對象信息。
46 for (Person person : totalPersonList) { 47 System.out.println(person); 48 } 49 } 50 }
方式二:使用 Stream 流式處理方式。
代碼實現:
1 import java.util.ArrayList; 2 import java.util.List; 3 import java.util.stream.Stream; 4 public class DemoStreamNames { 5 public static void main(String[] args) { 6 List<String> one = new ArrayList<>(); 7 // ...
8 List<String> two = new ArrayList<>(); 9 // ... 10 // 第一個隊伍只要名字為3個字的成員姓名; 11 // 第一個隊伍篩選之后只要前3個人;
12 Stream<String> streamOne = one.stream().filter(s->s.length() == 3).limit(3); 13
14 // 第二個隊伍只要姓張的成員姓名; 15 // 第二個隊伍篩選之后不要前2個人;
16 Stream<String> streamTwo = two.stream().filter(s->s.startsWith("張")).skip(2); 17
18 // 將兩個隊伍合並為一個隊伍; 19 // 根據姓名創建Person對象; 20 // 打印整個隊伍的Person對象信息。
21 Stream.concat(streamOne, streamTwo).map(Person::new).forEach(System.out::println); 22 } 23 }