Stream流
說到Stream便容易想到I/O Stream,而實際上,誰規定“流”就一定是“IO流”呢?在Java 8中,得益於Lambda所帶來的函數式編程,引入了一個全新的Stream概念,用於解決已有集合類庫既有的弊端。
傳統集合的多步遍歷代碼幾乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或間接的遍歷操作。而當我們需要對集合中的元素進行操作的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。例如:
import java.util.ArraList; import java.util.List; public class DemoForEach{ public static void main(String[] args) { List<String> list=ArrayList<>(); list.add=("瑪麗雅"); list。add=("艾澤拉"); list.add("小敏"); list.add("張強"); list.add("張三"); for( String name: list){ System.out.println(name); } } }
這是一段非常簡單的集合遍歷操作:對集合中的每一個字符串都進行打印輸出操作。
循環遍歷的弊端Java 8的Lambda讓我們可以更加專注於做什么(What),而不是怎么做(How),這點此前已經結合內部類進行了對比說明。現在,我們仔細體會一下上例代碼,可以發現:
for循環的語法就是“怎么做”
for循環的循環體才是“做什么”
為什么使用循環?因為要進行遍歷。但循環是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而並不是從第一個到最后一個順次處理的循環。前者是目的,后者是方式。
試想一下,如果希望對集合中的元素進行篩選過濾:
1. 將集合A根據條件一過濾為子集B;
2. 然后再根據條件二過濾為子集C。
在Java 8之前的做法可能為:
import java.util.ArrayList; import java.util.List; public class Demo02NormalFilter { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add=("瑪麗雅");
list。add=("艾澤拉");
list.add("小敏");
list.add("張強");
list.add("張三"); List<String> zhangList = new ArrayList<>(); for (String name : list) { if (name.startsWith("張")) { zhangList.add(name); } } List<String> shortList = new ArrayList<>(); for (String name : zhangList) { if (name.length() == 3) { shortList.add(name); } } for (String name : shortList) { System.out.println(name); } } }
這段代碼中含有三個循環,每一個作用不同:
1. 首先篩選所有姓張的人;
2. 然后篩選名字有三個字的人;
3. 最后進行對結果進行打印輸出。
每當我們需要對集合中的元素進行操作的時候,總是需要進行循環、循環、再循環。這是理所當然的么?不是。循環是做事情的方式,而不是目的。另一方面,使用線性循環就意味着只能遍歷一次。如果希望再次遍歷,只能再使用另一個循環從頭開始。
那,Lambda的衍生物Stream能給我們帶來怎樣更加優雅的寫法呢?
Stream的更優寫法
下面來看一下借助Java 8的Stream API,什么才叫優雅:
import java.util.ArrayList; import java.util.List; public class Demo03StreamFilter { public static void main(String[] args) { List<String> list = new ArrayList<>();
list.add=("瑪麗雅");
list。add=("艾澤拉");
list.add("小敏");
list.add("張強");
list.add("張三");
list.stream().filter(s-> s.startsWith("張")).filter(s-> s.lenth()==3 )..forEach(System,out.println); } }
直接閱讀代碼的字面意思即可完美展示無關邏輯方式的語義:獲取流、過濾姓張、過濾長度為3、逐一打印。代碼中並沒有體現使用線性循環或是其他任何算法進行遍歷,我們真正要做的事情內容被更好地體現在代碼中。
流式思想概述
注意:請暫時忘記對傳統IO流的固有印象!
整體來看,流式思想類似於工廠車間的“生產流水線”。
當需要對多個元素進行操作(特別是多步操作)的時候,考慮到性能及便利性,我們應該首先拼好一個“模型”步驟方案,然后再按照方案去執行它。
這張圖中展示了過濾、映射、跳過、計數等多步操作,這是一種集合元素的處理方案,而方案就是一種“函數模型”。圖中的每一個方框都是一個“流”,調用指定的方法,可以從一個流模型轉換為另一個流模型。而最右側的數字3是最終結果。
這里的 filter 、 map 、 skip 都是在對函數模型進行操作,集合元素並沒有真正被處理。只有當終結方法 count執行的時候,整個模型才會按照指定策略執行操作。而這得益於Lambda的延遲執行特性。
“Stream流”其實是一個集合元素的函數模型,它並不是集合,也不是數據結構,其本身並不存儲任何元素(或其地址值)。
獲取流
java.util.stream.Stream<T> 是Java 8新加入的最常用的流接口。(這並不是一個函數式接口。)
獲取一個流非常簡單,有以下幾種常用的方式:
所有的 Collection 集合都可以通過 stream 默認方法獲取流;
Stream 接口的靜態方法 of 可以獲取數組對應的流。
根據Collection獲取流
首先, java.util.Collection 接口中加入了default方法 stream 用來獲取流,所以其所有實現類均可獲取流。
import java.util.*; import java.util.stream.Stream; public class Demo04GetStream { public static void main(String[] args) { List<String> list = new ArrayList<>(); // ... Stream<String> stream1 = list.stream(); Set<String> set = new HashSet<>(); // ... Stream<String> stream2 = set.stream(); Vector<String> vector = new Vector<>(); // ... Stream<String> stream3 = vector.stream(); } }
根據Map獲取流
java.util.Map 接口不是 Collection 的子接口,且其K-V數據結構不符合流元素的單一特征,所以獲取對應的流需要分key、value或entry等情況:
import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; public class Demo05GetStream { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); // ... Stream<String> keyStream = map.keySet().stream(); Stream<String> valueStream = map.values().stream(); Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream(); } }
根據數組獲取流如果使用的不是集合或映射而是數組,由於數組對象不可能添加默認方法,所以 Stream 接口中提供了靜態方法of ,使用很簡單:
import java.util.stream.Stream; public class Demo06GetStream { public static void main(String[] args) { String[] array = { "張一無", "張一山", "張一豐", "張一元" }; Stream<String> stream = Stream.of(array); } }
of 方法的參數其實是一個可變參數,所以支持數組。
常用方法
流模型的操作很豐富,這里介紹一些常用的API。這些方法可以被分成兩種:
終結方法:返回值類型不再是 Stream 接口自身類型的方法,因此不再支持類似 StringBuilder 那樣的鏈式調用。本小節中,終結方法包括 count 和 forEach 方法。
非終結方法:返回值類型仍然是 Stream 接口自身類型的方法,因此支持鏈式調用。(除了終結方法外,其余方法均為非終結方法。)
備注:本小節之外的更多方法,請自行參考API文檔。
過濾:filter
可以通過 filter 方法將一個流轉換成另一個子集流。方法簽名:
Stream<T> filter(Predicate<? super T> predicate);
該接口接收一個 Predicate 函數式接口參數(可以是一個Lambda或方法引用)作為篩選條件。
Predicate接口
此前在 java.util.stream.Predicate 函數式接口,其中唯一的抽象方法為:
boolean test(T t);
該方法將會產生一個boolean值結果,代表指定的條件是否滿足。如果結果為true,那么Stream流的 filter 方法將會留用元素;如果結果為false,那么 filter 方法將會舍棄元素。
基本使用
Stream流中的 filter 方法基本使用的代碼如:
import java.util.stream.Stream; public class Demo07StreamFilter { public static void main(String[] args) { Stream<String> original = Stream.of("張一上", "張三", "王麻子"); Stream<String> result = original.filter(s ‐> s.startsWith("張")); } }
在這里通過Lambda表達式來指定了篩選的條件:必須姓張。
統計個數:count
正如舊集合 Collection 當中的 size 方法一樣,流提供 count 方法來數一數其中的元素個數:
long count();
該方法返回一個long值代表元素個數(不再像舊集合那樣是int值)。基本使用:
import java.util.stream.Stream; public class Demo09StreamCount { public static void main(String[] args) { Stream<String> original = Stream.of("張一商", "張三", "王麻子"); Stream<String> result = original.filter(s ‐> s.startsWith("張")); System.out.println(result.count()); // 2 } }
取用前幾個:limit
limit 方法可以對流進行截取,只取用前n個。方法簽名:
Stream<T> limit(long maxSize);
參數是一個long型,如果集合當前長度大於參數則進行截取;否則不進行操作。基本使用:
import java.util.stream.Stream; public class Demo10StreamLimit { public static void main(String[] args) { Stream<String> original = Stream.of("張一上", "張三", "李四"); Stream<String> result = original.limit(2); System.out.println(result.count()); // 2 } }
跳過前幾個:skip
如果希望跳過前幾個元素,可以使用 skip 方法獲取一個截取之后的新流:
Stream<T> skip(long n);
如果流的當前長度大於n,則跳過前n個;否則將會得到一個長度為0的空流。基本使用:
import java.util.stream.Stream; public class Demo11StreamSkip { public static void main(String[] args) { Stream<String> original = Stream.of("張一上", "張三", "薛蠻子"); Stream<String> result = original.skip(2); System.out.println(result.count()); // 1 } }
映射:map
如果需要將流中的元素映射到另一個流中,可以使用 map 方法。方法簽名:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
該接口需要一個 Function 函數式接口參數,可以將當前流中的T類型數據轉換為另一種R類型的流。
Function接口
此前我們已經學習過 java.util.stream.Function 函數式接口,其中唯一的抽象方法為:
R apply(T t);
這可以將一種T類型轉換成為R類型,而這種轉換的動作,就稱為“映射”。
基本使用
Stream流中的 map 方法基本使用的代碼如:
import java.util.stream.Stream; public class Demo08StreamMap { public static void main(String[] args) { Stream<String> original = Stream.of("10", "12", "18"); Stream<Integer> result = original.map(Integer::parseInt); } }
這段代碼中, map 方法的參數通過方法引用,將字符串類型轉換成為了int類型(並自動裝箱為 Integer 類對象)。
組合:concat
如果有兩個流,希望合並成為一個流,那么可以使用 Stream 接口的靜態方法 concat :
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
備注:這是一個靜態方法,與 java.lang.String 當中的 concat 方法是不同的。
該方法的基本使用代碼如:
import java.util.stream.Stream; public class Demo12StreamConcat { public static void main(String[] args) { Stream<String> streamA = Stream.of("張三"); Stream<String> streamB = Stream.of("張一山"); Stream<String> result = Stream.concat(streamA, streamB); } }
逐一處理:forEach
雖然方法名字叫 forEach ,但是與for循環中的“for-each”昵稱不同,該方法並不保證元素的逐一消費動作在流中是被有序執行的。
void forEach(Consumer<? super T> action);
該方法接收一個 Consumer 接口函數,會將每一個流元素交給該函數進行處理。例如:
import java.util.stream.Stream; public class Demo12StreamForEach { public static void main(String[] args) { Stream<String> stream = Stream.of("張一", "張三", "小敏"); stream.forEach(System.out::println); } }
在這里,方法引用 System.out::println 就是一個 Consumer 函數式接口的示例。
現在有兩個 ArrayList 集合存儲隊伍當中的多個成員姓名,要求使用傳統的for循環(或增強for循環)依次進行以
下若干操作步驟:
1. 第一個隊伍只要名字為3個字的成員姓名;
2. 第一個隊伍篩選之后只要前3個人;
3. 第二個隊伍只要姓張的成員姓名;
4. 第二個隊伍篩選之后不要前2個人;
5. 將兩個隊伍合並為一個隊伍;
6. 根據姓名創建 Person 對象;
7. 打印整個隊伍的Person對象信息。
兩個隊伍(集合)的代碼如下:
import java.util.ArrayList; import java.util.List; public class DemoArrayListNames { public static void main(String[] args) { List<String> one = new ArrayList<>(); one.add("迪麗熱巴"); one.add("宋遠橋"); one.add("蘇星河"); one.add("老子"); one.add("庄子"); one.add("孫子"); one.add("洪七公"); List<String> two = new ArrayList<>(); two.add("古力娜扎"); two.add("張無忌"); two.add("張三豐"); two.add("趙麗穎"); two.add("張二狗"); two.add("張天愛"); two.add("張三"); // .... } } //Person 類的代碼為: public class Person { private String name; public Person() {} public Person(String name) { this.name = name; } @Override public String toString() { return "Person{name='" + name + "'}"; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
解答
既然使用傳統的for循環寫法,那么:
public class DemoArrayListNames { public static void main(String[] args) { List<String> one = new ArrayList<>(); // ... List<String> two = new ArrayList<>(); // ... // 第一個隊伍只要名字為3個字的成員姓名; List<String> oneA = new ArrayList<>(); for (String name : one) { if (name.length() == 3) { oneA.add(name); } } // 第一個隊伍篩選之后只要前3個人; List<String> oneB = new ArrayList<>(); for (int i = 0; i < 3; i++) { oneB.add(oneA.get(i)); } // 第二個隊伍只要姓張的成員姓名; List<String> twoA = new ArrayList<>(); for (String name : two) { if (name.startsWith("張")) { twoA.add(name); } } // 第二個隊伍篩選之后不要前2個人; List<String> twoB = new ArrayList<>(); for (int i = 2; i < twoA.size(); i++) { twoB.add(twoA.get(i)); } // 將兩個隊伍合並為一個隊伍; List<String> totalNames = new ArrayList<>(); totalNames.addAll(oneB); totalNames.addAll(twoB); // 根據姓名創建Person對象; List<Person> totalPersonList = new ArrayList<>(); for (String name : totalNames) { totalPersonList.add(new Person(name)); } // 打印整個隊伍的Person對象信息。 for (Person person : totalPersonList) { System.out.println(person); } } } 運行結果為: Person{name='宋遠橋'} Person{name='蘇星河'} Person{name='洪七公'} Person{name='張二狗'} Person{name='張天愛'} Person{name='張三'}
集合元素處理(Stream方式)
將上一題當中的傳統for循環寫法更換為Stream流式處理方式。兩個集合的初始內容不變, Person 類的定義也不變。
解答
等效的Stream流式處理代碼為:
import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; public class DemoStreamNames { public static void main(String[] args) { List<String> one = new ArrayList<>(); // ... List<String> two = new ArrayList<>(); // ... // 第一個隊伍只要名字為3個字的成員姓名; // 第一個隊伍篩選之后只要前3個人; Stream<String> streamOne = one.stream().filter(s ‐> s.length() == 3).limit(3); // 第二個隊伍只要姓張的成員姓名; // 第二個隊伍篩選之后不要前2個人; Stream<String> streamTwo = two.stream().filter(s ‐> s.startsWith("張")).skip(2); // 將兩個隊伍合並為一個隊伍; // 根據姓名創建Person對象; // 打印整個隊伍的Person對象信息。 Stream.concat(streamOne, streamTwo).map(Person::new).forEach(System.out::println); } } 運行效果完全一樣: Person{name='宋遠橋'} Person{name='蘇星河'} Person{name='洪七公'} Person{name='張二狗'} Person{name='張天愛'} Person{name='張三'}
總結:函數拼接與終結方法
在上述介紹的各種方法中,凡是返回值仍然為 Stream 接口的為函數拼接方法,它們支持鏈式調用;而返回值不再為 Stream 接口的為終結方法,不再支持鏈式調用。如下表所示:
並發流
當需要對存在於集合或數組中的若干元素進行並發操作時,簡直就是噩夢!我們需要仔細考慮多線程環境下的原子性、競爭甚至鎖問題,即便是 java.util.concurrent.ConcurrentMap<K, V> 接口也必須謹慎地正確使用。
而對於Stream流來說,這很簡單。
轉換為並發流
Stream 的父接口 java.util.stream.BaseStream 中定義了一個 parallel 方法:
S parallel();
只需要在流上調用一下無參數的 parallel 方法,那么當前流即可變身成為支持並發操作的流,返回值仍然為
Stream 類型。例如:
import java.util.stream.Stream; public class Demo13StreamParallel { public static void main(String[] args) { Stream<Integer> stream = Stream.of(10, 20, 30, 40, 50).parallel(); } }
直接獲取並發流
在通過集合獲取流時,也可以直接調用 parallelStream 方法來直接獲取支持並發操作的流。方法定義為:
default Stream<E> parallelStream() {...}
應用代碼為:
import java.util.ArrayList; import java.util.Collection; import java.util.stream.Stream; public class Demo13StreamParallel { public static void main(String[] args) { Collection<String> coll = new ArrayList<>(); Stream<String> stream = coll.parallelStream(); } }
使用並發流多次執行下面這段代碼,結果的順序在很大概率上是不一定的:
import java.util.stream.Stream; public class Demo13StreamParallel { public static void main(String[] args) { Stream.of(10, 20, 30, 40, 50, 60, 70, 80, 90, 100) .parallel().forEach(System.out::println); } }
收集Stream結果
對流操作完成之后,如果需要將其結果進行收集,例如獲取對應的集合、數組等,如何操作?
收集到集合中
Stream流提供 collect 方法,其參數需要一個 java.util.stream.Collector<T,A, R> 接口對象來指定收集到哪種集合中。幸運的是, java.util.stream.Collectors 類提供一些方法,可以作為 Collector 接口的實例:
public static <T> Collector<T, ?, List<T>> toList() :轉換為 List 集合。
public static <T> Collector<T, ?, Set<T>> toSet() :轉換為 Set 集合。
下面是這兩個方法的基本使用代碼:
import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; public class Demo15StreamCollect { public static void main(String[] args) { Stream<String> stream = Stream.of("10", "20", "30", "40", "50"); List<String> list = stream.collect(Collectors.toList()); Set<String> set = stream.collect(Collectors.toSet()); } }
收集到數組中
Stream提供 toArray 方法來將結果放到一個數組中,由於泛型擦除的原因,返回值類型是Object[]的:
Object[] toArray();
其使用場景如:
import java.util.stream.Stream; public class Demo16StreamArray { public static void main(String[] args) { Stream<String> stream = Stream.of("10", "20", "30", "40", "50"); Object[] objArray = stream.toArray(); } }
解決泛型數組問題
有了Lambda和方法引用之后,可以使用 toArray 方法的另一種重載形式傳遞一個 IntFunction<A[]> 的函數,繼
而從外面指定泛型參數。方法簽名:
<A> A[] toArray(IntFunction<A[]> generator);
有了它,上例代碼中不再局限於 Object[] 結果,而可以得到 String[] 結果:
import java.util.stream.Stream; public class Demo17StreamArray { public static void main(String[] args) { Stream<String> stream = Stream.of("10", "20", "30", "40", "50"); String[] strArray = stream.toArray(String[]::new); } }
既然數組也是有構造器的,那么傳遞一個數組的構造器引用即可。
Java仍然沒有泛型數組,原因同樣是泛型擦除。
將數組元素加到集合中
請通過Stream流的方式,將下面數組當中的元素添加(收集)到 List 集合當中:
public class DemoCollect { public static void main(String[] args) { int[] array = { 10, 20, 30, 40, 50 }; } } 解答 首先需要將數組轉換成為流,然后再通過 collect 方法收集到 List 集合中: import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; public class DemoCollect { public static void main(String[] args) { String[] array = { "Java", "Groovy", "Scala", "Kotlin" }; List<String> list = Stream.of(array).collect(Collectors.toList()); } }