說到Stream便容易想到I/O Stream,而實際上,誰規定“流”就一定是“IO流”呢?在Java 8中,得益於Lambda所帶來的函數式編程,引入了一個全新的Stream概念,用於解決已有集合類庫既有的弊端。
傳統集合的多步遍歷代碼
幾乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或間接的遍歷操作。而當我們需要對集合中的元素進行操作的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。例如:
import java.util.ArrayList;
import java.util.List;
public class Demo01ForEach {
public static void main(String[] args) {
List<String> list = new 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循環的循環體纔是“做什麼”
爲什麼使用循環?因爲要進行遍歷。但循環是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而並不是從第一個到最後一個順次處理的循環。前者是目的,後者是方式。
試想一下,如果希望對集合中的元素進行篩選過濾:
- 將集合A根據條件一過濾爲子集B;
- 然後再根據條件二過濾爲子集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);
}
}
}
這段代碼中含有三個循環,每一個作用不同:
- 首先篩選所有姓張的人;
- 然後篩選名字有三個字的人;
- 最後進行對結果進行打印輸出
每當我們需要對集合中的元素進行操作的時候,總是需要進行循環、循環、再循環。這是理所當然的麼?不是。循環是做事情的方式,而不是目的。另一方面,使用線性循環就意味着只能遍歷一次。如果希望再次遍歷,只能再使用另一個循環從頭開始。
那,Lambda的衍生物Stream能給我們帶來怎樣更加優雅的寫法呢?
Stream的更優寫法
package com.demo.java.study8;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三丰");
list.add("張三sasa");
list.stream().filter(s -> s.startsWith("張"))
.filter(s -> s.length()==3)
.forEach(System.out::println);
}
}
直接閱讀代碼的字面意思即可完美展示無關邏輯方式的語義:獲取流、過濾姓張、過濾長度爲3、逐一打印。代碼中並沒有體現使用線性循環或是其他任何算法進行遍歷,我們真正要做的事情內容被更好地體現在代碼中。
整體來看,流式思想類似於工廠車間的“生產流水線”。
什麼是 Stream?
Stream是操作集合的一種計算數據的工具。集合就是數據,Stream是計算集合中的數據的工具
和以前的Collection操作不同, Stream操作還有兩個基礎的特徵:
- Pipelining: 中間操作都會返回流對象本身。 這樣多個操作可以串聯成一個管道, 如同流式風格(flfluentstyle)。 這樣做可以對操作進行優化, 比如延遲執行(laziness)和短路( short-circuiting)。
- 內部迭代: 以前對集合遍歷都是通過Iterator或者增強for的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代。 Stream提供了內部迭代的方式,流可以直接調用遍歷方法。
流的操作分爲兩種,分別爲中間操作 和 終端操作。
當使用一個流的時候,通常包括三個基本步驟:
獲取一個數據源(source)→ 數據轉換→執行操作獲取想要的結果。
每次轉換原有 Stream 對象不改變,返回一個新的 Stream 對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道。
獲取流
java.util.stream.Stream 是Java 8新加入的最常用的流接口。(這並不是一個函數式接口。)獲取一個流非常簡單,有以下幾種常用的方式:
- 所有的 Collection 集合都可以通過 stream 默認方法獲取流;
- Stream 接口的靜態方法 of 可以獲取數組對應的流
根據Collection獲取流
首先, java.util.Collection 接口中加入了default方法 stream 用來獲取流,所以其所有實現類均可獲取流
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
Stream stream=list.stream();
Set<String> list2=new HashSet<>();
Stream stream1=list2.stream();
Vector<String> list3=new Vector<>();
Stream stream2=list3.stream();
}
}
根據Map獲取流
java.util.Map 接口不是 Collection 的子接口,且其K-V數據結構不符合流元素的單一特徵,所以獲取對應的流需要分key、value或entry等情況
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
Stream keyStream=map.keySet().stream();
Stream valueStream=map.values().stream();
Stream<Map.Entry<String,String>> entryStream=map.entrySet().stream();
}
}
根據數組獲取流
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
String[] array = { "張無忌", "張翠山", "張三丰", "張一元" };
Stream<String> stream=Stream.of(array);
}
}
備註: of 方法的參數其實是一個可變參數,所以支持數組。
常用方法
流模型的操作很豐富,這裏介紹一些常用的API。這些方法可以被分成兩種:
延遲方法:返回值類型仍然是 Stream 接口自身類型的方法,因此支持鏈式調用。(除了終結方法外,其餘方法均爲延遲方法。)
終結方法:返回值類型不再是 Stream 接口自身類型的方法,因此不再支持類似 StringBuilder 那樣的鏈式調用。本小節中,終結方法包括 count 和 forEach 方法。
備註:本小節之外的更多方法,請自行參考API文檔。
逐一處理:forEach
雖然方法名字叫 forEach ,但是與for循環中的“for-each”暱稱不同。
void forEach(Consumer<? super T> action);
該方法接收一個 Consumer 接口函數,會將每一個流元素交給該函數進行處理。
複習Consumer接口
java.util.function.Consumer<T>接口是一個消費型接口。
Consumer接口中包含抽象方法void accept(T t),意爲消費一個指定泛型的數據。
基本使用
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
public class Test {
public static void main(String[] args) {
String[] array = { "張無忌", "張翠山", "張三丰", "張一元" };
Stream<String> stream=Stream.of(array);
stream.forEach(s -> System.out.println(s));
}
}
過濾: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 方法將會捨棄元素。
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
Stream<String> stream=Stream.of(array);
Stream<String> result= stream.filter(s -> s.startsWith("張"));
result.forEach(s -> System.out.println(s));
}
}
在這裏通過Lambda表達式來指定了篩選的條件:必須姓張。
映射: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類型,而這種轉換的動作,就稱爲“映射”。
基本使用
package com.demo.java.study8;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-17 14:05
* @description:
* @Version: 1.0
*/
public class Test2 {
public static void main(String[] args) {
String[] array = { "10", "18", "20", "21" };
Stream<String> stream=Stream.of(array);
Stream result= stream.map(s -> Integer.parseInt(s));
result.forEach(s-> System.out.println(s));
}
}
這段代碼中, map 方法的參數通過方法引用,將字符串類型轉換成爲了int類型(並自動裝箱爲 Integer 類對象)。
統計個數:count
正如舊集合 Collection 當中的 size 方法一樣,流提供 count 方法來數一數其中的元素個數:
long count()
該方法返回一個long值代表元素個數(不再像舊集合那樣是int值)。基本使用:
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
Stream<String> stream=Stream.of(array);
Stream<String> result= stream.filter(s -> s.startsWith("張"));
System.out.println(result.count());
}
}
取用前幾個:limit
limit 方法可以對流進行截取,只取用前n個。方法簽名:
Stream<T> limit(long maxSize);
參數是一個long型,如果集合當前長度大於參數則進行截取;否則不進行操作。基本使用:
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
Stream<String> stream=Stream.of(array);
Stream<String> result= stream.limit(3);
result.forEach(s -> System.out.println(s));
}
}
輸出:
張無忌
李翠山
劉三豐
跳過前幾個:skip
如果希望跳過前幾個元素,可以使用 skip 方法獲取一個截取之後的新流:
Stream<T> skip(long n);
如果流的當前長度大於n,則跳過前n個;否則將會得到一個長度爲0的空流。基本使用:
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
Stream<String> stream=Stream.of(array);
Stream<String> result= stream.skip(2);
result.forEach(s -> System.out.println(s));
}
}
輸出:
劉三豐
張一元
組合:concat
如果有兩個流,希望合併成爲一個流,那麼可以使用 Stream 接口的靜態方法 concat :
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
備註:這是一個靜態方法,與 java.lang.String 當中的 concat 方法是不同的。
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
Stream<String> stream1=Stream.of(array);
Stream<String> stream2=Stream.of(array);
Stream<String> result1= stream1.skip(2);
Stream<String> result2=stream2.limit(2);
Stream stream=Stream.concat(result1,result2);
stream.forEach(s-> System.out.println(s));
}
}
輸出:
劉三豐
張一元
張無忌
李翠山
練習:集合元素處理
題目
現在有兩個 ArrayList 集合存儲隊伍當中的多個成員姓名,要求使用Stream流式處理方式依次進行以下若干操作步驟:
- 第一個隊伍只要名字爲3個字的成員姓名;存儲到一個新集合中。
- 第一個隊伍篩選之後只要前3個人;存儲到一個新集合中。
- 第二個隊伍只要姓張的成員姓名;存儲到一個新集合中。
- 第二個隊伍篩選之後不要前2個人;存儲到一個新集合中。
- 將兩個隊伍合併爲一個隊伍;存儲到一個新集合中。
- 根據姓名創建 Person 對象;存儲到一個新集合中。
- 打印整個隊伍的Person對象信息。
兩個隊伍(集合)的代碼如下:
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
//第一支隊伍
ArrayList<String> one = new ArrayList<>();
one.add("迪麗熱巴");
one.add("宋遠橋");
one.add("蘇星河");
one.add("石破天");
one.add("石中玉");
one.add("老子");
one.add("莊子");
one.add("洪七公");
//第二支隊伍
ArrayList<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("張無忌");
two.add("趙麗穎");
two.add("張三丰");
two.add("尼古拉斯趙四");
two.add("張天愛");
two.add("張二狗");
}
}
解決:
package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
public static void main(String[] args) {
//第一支隊伍
ArrayList<String> one = new ArrayList<>();
one.add("迪麗熱巴");
one.add("宋遠橋");
one.add("蘇星河");
one.add("石破天");
one.add("石中玉");
one.add("老子");
one.add("莊子");
one.add("洪七公");
//第二支隊伍
ArrayList<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("張無忌");
two.add("趙麗穎");
two.add("張三丰");
two.add("尼古拉斯趙四");
two.add("張天愛");
two.add("張二狗");
// 第一個隊伍只要名字爲3個字的成員姓名;
// 第一個隊伍篩選之後只要前3個人;
Stream<String> stream1=one.stream().filter(s -> s.length()==3).limit(3);
// 第二個隊伍只要姓張的成員姓名;
// 第二個隊伍篩選之後不要前2個人;
Stream<String> stream2= two.stream().filter(s -> s.startsWith("張")).skip(2);
// 將兩個隊伍合併爲一個隊伍;
// 根據姓名創建Person對象;
// 打印整個隊伍的Person對象信息。
Stream<String> stream3=Stream.concat(stream1,stream2);
stream3.map(Person::new).forEach(person -> System.out.println(person.toString()));
//stream3.map(Person::new).forEach(System.out::println);
}
}
輸出:
Person{name='宋遠橋'}
Person{name='蘇星河'}
Person{name='石破天'}
Person{name='張天愛'}
Person{name='張二狗'}
其他方法可另行參考博客:
https://blog.csdn.net/makyan/article/details/104533966