Stream流


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());

}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM