在Java 8中弓千,得益于Lambda所帶來的函數(shù)式編程,引入了一個全新的Stream概念,用于解決已有集合類庫既有的 弊端捺僻。
1.1 引言
傳統(tǒng)集合的多步遍歷代碼
幾乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或間接的遍歷操作恋技。而當(dāng)我們需要對集合中的元 素進行操作的時候拇舀,除了必需的添加、刪除蜻底、獲取外骄崩,典型的就是集合遍歷。例如:
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);
}
}
}
循環(huán)遍歷的弊端
Java 8的Lambda讓我們可以更加專注于做什么(What)薄辅,而不是怎么做(How)要拂,這點此前已經(jīng)結(jié)合內(nèi)部類進行 了對比說明。現(xiàn)在站楚,我們仔細體會一下上例代碼脱惰,可以發(fā)現(xiàn):
- for循環(huán)的語法就是“怎么做”
- for循環(huán)的循環(huán)體才是“做什么”
為什么使用循環(huán)?因為要進行遍歷窿春。但循環(huán)是遍歷的唯一方式嗎拉一?遍歷是指每一個元素逐一進行處理,而并不是從第一個到最后一個順次處理的循環(huán)旧乞。前者是目的蔚润,后者是方式。
試想一下尺栖,如果希望對集合中的元素進行篩選過濾:
1.將集合A根據(jù)條件一過濾為子集B抽碌;
2.然后再根據(jù)條件二過濾為子集C。
那怎么辦?在Java 8之前的做法可能為:
import java.util.ArrayList;
import java.util.List;
/*
使用傳統(tǒng)的方式,遍歷集合,對集合中的數(shù)據(jù)進行過濾
*/
public class Demo01List {
public static void main(String[] args) {
//創(chuàng)建一個List集合,存儲姓名
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三豐");
//對list集合中的元素進行過濾,只要以張開頭的元素,存儲到一個新的集合中
List<String> listA = new ArrayList<>();
for(String s : list){
if(s.startsWith("張")){
listA.add(s);
}
}
//對listA集合進行過濾,只要姓名長度為3的人,存儲到一個新集合中
List<String> listB = new ArrayList<>();
for (String s : listA) {
if(s.length()==3){
listB.add(s);
}
}
//遍歷listB集合
for (String s : listB) {
System.out.println(s);
}
}
}
這段代碼中含有三個循環(huán)货徙,每一個作用不同:
- 首先篩選所有姓張的人左权;
- 然后篩選名字有三個字的人;
- 后進行對結(jié)果進行打印輸出痴颊。
每當(dāng)我們需要對集合中的元素進行操作的時候赏迟,總是需要進行循環(huán)、循環(huán)蠢棱、再循環(huán)锌杀。這是理所當(dāng)然的么?不是泻仙。循環(huán)是做事情的方式糕再,而不是目的。另一方面玉转,使用線性循環(huán)就意味著只能遍歷一次突想。如果希望再次遍歷,只能再使 用另一個循環(huán)從頭開始究抓。
Stream的更優(yōu)寫法
import java.util.ArrayList;
import java.util.List;
/*
使用Stream流的方式,遍歷集合,對集合中的數(shù)據(jù)進行過濾
Stream流是JDK1.8之后出現(xiàn)的
關(guān)注的是做什么,而不是怎么做
*/
public class Demo02Stream {
public static void main(String[] args) {
//創(chuàng)建一個List集合,存儲姓名
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三豐");
//對list集合中的元素進行過濾,只要以張開頭的元素,存儲到一個新的集合中
//對listA集合進行過濾,只要姓名長度為3的人,存儲到一個新集合中
//遍歷listB集合
list.stream()
.filter(name->name.startsWith("張"))
.filter(name->name.length()==3)
.forEach(name-> System.out.println(name));
}
}
1.2 流式思想概述
流式思想示意圖
這張圖中展示了過濾猾担、映射、跳過刺下、計數(shù)等多步操作绑嘹,這是一種集合元素的處理方案,而方案就是一種“函數(shù)模型”橘茉。圖中的每一個方框都是一個“流”工腋,調(diào)用指定的方法,可以從一個流模型轉(zhuǎn)換為另一個流模型畅卓。而最右側(cè)的數(shù)字3是最終結(jié)果擅腰。
這里的 filter 、 map 髓介、 skip 都是在對函數(shù)模型進行操作,集合元素并沒有真正被處理筋现。只有當(dāng)終結(jié)方法 count 執(zhí)行的時候唐础,整個模型才會按照指定策略執(zhí)行操作。而這得益于Lambda的延遲執(zhí)行特性矾飞。
備注:“Stream流”其實是一個集合元素的函數(shù)模型一膨,它并不是集合,也不是數(shù)據(jù)結(jié)構(gòu)洒沦,其本身并不存儲任何 元素(或其地址值)
Stream(流)是一個來自數(shù)據(jù)源的元素隊列
- 元素是特定類型的對象豹绪,形成一個隊列。 Java中的Stream并不會存儲元素,而是按需計算瞒津。
- 數(shù)據(jù)源 流的來源蝉衣。 可以是集合,數(shù)組 等巷蚪。
和以前的Collection操作不同病毡, Stream操作還有兩個基礎(chǔ)的特征:
- Pipelining: 中間操作都會返回流對象本身。 這樣多個操作可以串聯(lián)成一個管道屁柏, 如同流式風(fēng)格(?uent style)啦膜。 這樣做可以對操作進行優(yōu)化, 比如延遲執(zhí)行(laziness)和短路( short-circuiting)淌喻。
- 內(nèi)部迭代: 以前對集合遍歷都是通過Iterator或者增強for的方式, 顯式的在集合外部進行迭代僧家, 這叫做外部迭代。 Stream提供了內(nèi)部迭代的方式裸删,流可以直接調(diào)用遍歷方法八拱。
當(dāng)使用一個流的時候,通常包括三個基本步驟:獲取一個數(shù)據(jù)源(source)→ 數(shù)據(jù)轉(zhuǎn)換→執(zhí)行操作獲取想要的結(jié)果烁落,每次轉(zhuǎn)換原有 Stream 對象不改變乘粒,返回一個新的 Stream 對象(可以有多次轉(zhuǎn)換),這就允許對其操作可以像鏈條一樣排列伤塌,變成一個管道灯萍。
1.3 獲取流
java.util.stream.Stream<T> 是Java 8新加入的常用的流接口。(這并不是一個函數(shù)式接口每聪。)
獲取一個流非常簡單旦棉,有以下幾種常用的方式:
- 所有的 Collection 集合都可以通過 stream 默認方法獲取流;
- Stream 接口的靜態(tài)方法 of 可以獲取數(shù)組對應(yīng)的流药薯。
根據(jù)Collection獲取流
java.util.Collection 接口中加入了default方法 stream 用來獲取流绑洛,所以其所有實現(xiàn)類均可獲取流。
根據(jù)Map獲取流
java.util.Map 接口不是 Collection 的子接口童本,且其K-V數(shù)據(jù)結(jié)構(gòu)不符合流元素的單一特征炫狱,所以獲取對應(yīng)的流 需要分key、value或entry等情況逗爹。
根據(jù)數(shù)組獲取流
如果使用的不是集合或映射而是數(shù)組蛋哭,由于數(shù)組對象不可能添加默認方法,所以 Stream 接口中提供了靜態(tài)方法 of 泵额。
import java.util.*;
import java.util.stream.Stream;
/*
java.util.stream.Stream<T>是Java 8新加入的最常用的流接口配深。(這并不是一個函數(shù)式接口。)
獲取一個流非常簡單嫁盲,有以下幾種常用的方式:
- 所有的Collection集合都可以通過stream默認方法獲取流篓叶;
default Stream<E> stream?()
- Stream接口的靜態(tài)方法of可以獲取數(shù)組對應(yīng)的流。
static <T> Stream<T> of?(T... values)
參數(shù)是一個可變參數(shù),那么我們就可以傳遞一個數(shù)組
*/
public class Demo01GetStream {
public static void main(String[] args) {
//把集合轉(zhuǎn)換為Stream流
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();
Map<String,String> map = new HashMap<>();
//獲取鍵,存儲到一個Set集合中
Set<String> keySet = map.keySet();
Stream<String> stream3 = keySet.stream();
//獲取值,存儲到一個Collection集合中
Collection<String> values = map.values();
Stream<String> stream4 = values.stream();
//獲取鍵值對(鍵與值的映射關(guān)系 entrySet)
Set<Map.Entry<String, String>> entries = map.entrySet();
Stream<Map.Entry<String, String>> stream5 = entries.stream();
//把數(shù)組轉(zhuǎn)換為Stream流
Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);
//可變參數(shù)可以傳遞數(shù)組
Integer[] arr = {1,2,3,4,5};
Stream<Integer> stream7 = Stream.of(arr);
String[] arr2 = {"a","bb","ccc"};
Stream<String> stream8 = Stream.of(arr2);
}
}
1.4 常用方法
流模型的操作很豐富,這里介紹一些常用的API缸托。這些方法可以被分成兩種:
- 延遲方法:返回值類型仍然是 Stream 接口自身類型的方法左敌,因此支持鏈?zhǔn)秸{(diào)用。(除了終結(jié)方法外嗦董,其余方 法均為延遲方法母谎。)
- 終結(jié)方法:返回值類型不再是 Stream 接口自身類型的方法,因此不再支持類似 StringBuilder 那樣的鏈?zhǔn)秸{(diào) 用京革。本小節(jié)中奇唤,終結(jié)方法包括 count 和 forEach 方法。
備注:本小節(jié)之外的更多方法匹摇,請自行參考API文檔咬扇。
逐一處理:forEach
forEach 方法用來遍歷流中的數(shù)據(jù)。
import java.util.stream.Stream;
/*
Stream流中的常用方法_forEach
void forEach(Consumer<? super T> action);
該方法接收一個Consumer接口函數(shù)廊勃,會將每一個流元素交給該函數(shù)進行處理懈贺。
Consumer接口是一個消費型的函數(shù)式接口,可以傳遞Lambda表達式,消費數(shù)據(jù)
簡單記:
forEach方法,用來遍歷流中的數(shù)據(jù)
是一個終結(jié)方法,遍歷之后就不能繼續(xù)調(diào)用Stream流中的其他方法
*/
public class Stream_forEach {
public static void main(String[] args) {
//獲取一個Stream流
Stream<String> stream = Stream.of("張三", "李四", "王五", "趙六", "田七");
//使用Stream流中的方法forEach對Stream流中的數(shù)據(jù)進行遍歷
/*stream.forEach((String name)->{
System.out.println(name);
});*/
stream.forEach(name->System.out.println(name));
}
}
過濾:filter
filter 方法將一個流轉(zhuǎn)換成另一個子集流。
import java.util.stream.Stream;
/*
Stream流中的常用方法_filter:用于對Stream流中的數(shù)據(jù)進行過濾
Stream<T> filter(Predicate<? super T> predicate);
filter方法的參數(shù)Predicate是一個函數(shù)式接口,所以可以傳遞Lambda表達式,對數(shù)據(jù)進行過濾
Predicate中的抽象方法:
boolean test(T t);
*/
public class Stream_filter {
public static void main(String[] args) {
//創(chuàng)建一個Stream流
Stream<String> stream = Stream.of("張三豐", "張翠山", "趙敏", "周芷若", "張無忌");
//對Stream流中的元素進行過濾,只要姓張的人
Stream<String> stream2 = stream.filter((String name)->{return name.startsWith("張");});
//遍歷stream2流
stream2.forEach(name-> System.out.println(name));
/*
Stream流屬于管道流,只能被消費(使用)一次
第一個Stream流調(diào)用完畢方法,數(shù)據(jù)就會流轉(zhuǎn)到下一個Stream上
而這時第一個Stream流已經(jīng)使用完畢,就會關(guān)閉了
所以第一個Stream流就不能再調(diào)用方法了
IllegalStateException: stream has already been operated upon or closed
*/
//遍歷stream流
stream.forEach(name-> System.out.println(name));
}
}
映射:map
map 方法將流中的元素映射到另一個流中坡垫。
import java.util.stream.Stream;
/*
Stream流中的常用方法_map:用于類型轉(zhuǎn)換
如果需要將流中的元素映射到另一個流中梭灿,可以使用map方法.
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
該接口需要一個Function函數(shù)式接口參數(shù),可以將當(dāng)前流中的T類型數(shù)據(jù)轉(zhuǎn)換為另一種R類型的流冰悠。
Function中的抽象方法:
R apply(T t);
*/
public class Stream_map {
public static void main(String[] args) {
//獲取一個String類型的Stream流
Stream<String> stream = Stream.of("1", "2", "3", "4");
//使用map方法,把字符串類型的整數(shù),轉(zhuǎn)換(映射)為Integer類型的整數(shù)
Stream<Integer> stream2 = stream.map((String s)->{
return Integer.parseInt(s);
});
//遍歷Stream2流
stream2.forEach(i-> System.out.println(i));
}
}
統(tǒng)計個數(shù):count
count 方法統(tǒng)計流中元素個數(shù)堡妒,返回 long 類型。
import java.util.ArrayList;
import java.util.stream.Stream;
/*
Stream流中的常用方法_count:用于統(tǒng)計Stream流中元素的個數(shù)
long count();
count方法是一個終結(jié)方法,返回值是一個long類型的整數(shù)
所以不能再繼續(xù)調(diào)用Stream流中的其他方法了
*/
public class Stream_count {
public static void main(String[] args) {
//獲取一個Stream流
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
Stream<Integer> stream = list.stream();
long count = stream.count();
System.out.println(count);//7
}
}
取用前幾個:limit
limit 方法可以對流進行截取溉卓,只取用前n個皮迟。
import java.util.stream.Stream;
/*
Stream流中的常用方法_limit:用于截取流中的元素
limit方法可以對流進行截取,只取用前n個桑寨。方法簽名:
Stream<T> limit(long maxSize);
參數(shù)是一個long型伏尼,如果集合當(dāng)前長度大于參數(shù)則進行截取尉尾;否則不進行操作
limit方法是一個延遲方法,只是對流中的元素進行截取,返回的是一個新的流,所以可以繼續(xù)調(diào)用Stream流中的其他方法
*/
public class Stream_limit {
public static void main(String[] args) {
//獲取一個Stream流
String[] arr = {"美羊羊","喜洋洋","懶洋洋","灰太狼","紅太狼"};
Stream<String> stream = Stream.of(arr);
//使用limit對Stream流中的元素進行截取,只要前3個元素
Stream<String> stream2 = stream.limit(3);
//遍歷stream2流
stream2.forEach(name-> System.out.println(name));
}
}
跳過前幾個:skip
skip 方法t跳過前幾個元素獲取一個截取之后的新流爆阶。
import java.util.stream.Stream;
/*
Stream流中的常用方法_skip:用于跳過元素
如果希望跳過前幾個元素,可以使用skip方法獲取一個截取之后的新流:
Stream<T> skip(long n);
如果流的當(dāng)前長度大于n沙咏,則跳過前n個辨图;否則將會得到一個長度為0的空流。
*/
public class Stream_skip {
public static void main(String[] args) {
//獲取一個Stream流
String[] arr = {"美羊羊","喜洋洋","懶洋洋","灰太狼","紅太狼"};
Stream<String> stream = Stream.of(arr);
//使用skip方法跳過前3個元素
Stream<String> stream2 = stream.skip(3);
//遍歷stream2流
stream2.forEach(name-> System.out.println(name));
}
}
組合:concat
Stream 接口的靜態(tài)方法 concat 把兩個流合并成一個流
/*
Stream流中的常用方法_concat:用于把流組合到一起
如果有兩個流芭碍,希望合并成為一個流徒役,那么可以使用Stream接口的靜態(tài)方法concat
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
*/
public class Stream_concat {
public static void main(String[] args) {
//創(chuàng)建一個Stream流
Stream<String> stream1 = Stream.of("張三豐", "張翠山", "趙敏", "周芷若", "張無忌");
//獲取一個Stream流
String[] arr = {"美羊羊","喜洋洋","懶洋洋","灰太狼","紅太狼"};
Stream<String> stream2 = Stream.of(arr);
//把以上兩個流組合為一個流
Stream<String> concat = Stream.concat(stream1, stream2);
//遍歷concat流
concat.forEach(name-> System.out.println(name));
}
}
1.5 練習(xí):集合元素處理(傳統(tǒng)方式)
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;
}
}
傳統(tǒng)方式
import java.util.ArrayList;
/*
練習(xí):集合元素處理(傳統(tǒng)方式)
現(xiàn)在有兩個ArrayList集合存儲隊伍當(dāng)中的多個成員姓名孽尽,要求使用傳統(tǒng)的for循環(huán)(或增強for循環(huán))依次進行以下若干操作步驟:
1. 第一個隊伍只要名字為3個字的成員姓名窖壕;存儲到一個新集合中。
2. 第一個隊伍篩選之后只要前3個人;存儲到一個新集合中瞻讽。
3. 第二個隊伍只要姓張的成員姓名鸳吸;存儲到一個新集合中。
4. 第二個隊伍篩選之后不要前2個人速勇;存儲到一個新集合中晌砾。
5. 將兩個隊伍合并為一個隊伍;存儲到一個新集合中烦磁。
6. 根據(jù)姓名創(chuàng)建Person對象养匈;存儲到一個新集合中。
7. 打印整個隊伍的Person對象信息都伪。
*/
public class StreamTest {
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("洪七公");
//1. 第一個隊伍只要名字為3個字的成員姓名呕乎;存儲到一個新集合中。
ArrayList<String> one1 = new ArrayList<>();
for (String name : one) {
if(name.length()==3){
one1.add(name);
}
}
//2. 第一個隊伍篩選之后只要前3個人陨晶;存儲到一個新集合中猬仁。
ArrayList<String> one2 = new ArrayList<>();
for (int i = 0; i <3 ; i++) {
one2.add(one1.get(i));//i = 0,1,2
}
//第二支隊伍
ArrayList<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("張無忌");
two.add("趙麗穎");
two.add("張三豐");
two.add("尼古拉斯趙四");
two.add("張?zhí)鞇?);
two.add("張二狗");
//3. 第二個隊伍只要姓張的成員姓名;存儲到一個新集合中先誉。
ArrayList<String> two1 = new ArrayList<>();
for (String name : two) {
if(name.startsWith("張")){
two1.add(name);
}
}
//4. 第二個隊伍篩選之后不要前2個人湿刽;存儲到一個新集合中。
ArrayList<String> two2 = new ArrayList<>();
for (int i = 2; i <two1.size() ; i++) {
two2.add(two1.get(i)); //i 不包含0 1
}
//5. 將兩個隊伍合并為一個隊伍褐耳;存儲到一個新集合中诈闺。
ArrayList<String> all = new ArrayList<>();
all.addAll(one2);
all.addAll(two2);
//6. 根據(jù)姓名創(chuàng)建Person對象;存儲到一個新集合中漱病。
ArrayList<Person> list = new ArrayList<>();
for (String name : all) {
list.add(new Person(name));
}
//7. 打印整個隊伍的Person對象信息买雾。
for (Person person : list) {
System.out.println(person);
}
}
}
1.6 練習(xí):集合元素處理(Stream方式)
import java.util.ArrayList;
import java.util.stream.Stream;
/*
練習(xí):集合元素處理(Stream方式)
將上一題當(dāng)中的傳統(tǒng)for循環(huán)寫法更換為Stream流式處理方式。
兩個集合的初始內(nèi)容不變杨帽,Person類的定義也不變漓穿。
*/
public class StreamTest {
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("洪七公");
//1. 第一個隊伍只要名字為3個字的成員姓名;存儲到一個新集合中注盈。
//2. 第一個隊伍篩選之后只要前3個人晃危;存儲到一個新集合中。
Stream<String> oneStream = one.stream().filter(name -> name.length() == 3).limit(3);
//第二支隊伍
ArrayList<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("張無忌");
two.add("趙麗穎");
two.add("張三豐");
two.add("尼古拉斯趙四");
two.add("張?zhí)鞇?);
two.add("張二狗");
//3. 第二個隊伍只要姓張的成員姓名老客;存儲到一個新集合中僚饭。
//4. 第二個隊伍篩選之后不要前2個人;存儲到一個新集合中胧砰。
Stream<String> twoStream = two.stream().filter(name -> name.startsWith("張")).skip(2);
//5. 將兩個隊伍合并為一個隊伍鳍鸵;存儲到一個新集合中。
//6. 根據(jù)姓名創(chuàng)建Person對象尉间;存儲到一個新集合中偿乖。
//7. 打印整個隊伍的Person對象信息击罪。
Stream.concat(oneStream,twoStream).map(name->new Person(name)).forEach(p-> System.out.println(p));
}
}