初識 Stream
Java 8 API添加了一個新的抽象稱為流 Stream忆首,可以讓你以一種聲明的方式處理數(shù)據(jù)盟蚣。使用一種類似用 SQL 語句從數(shù)據(jù)庫查詢數(shù)據(jù)的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。
Stream API可以極大提高Java程序員的生產(chǎn)力笤虫,讓程序員寫出高效率、干凈、簡潔的代碼见坑。這種風格將要處理的元素集合看作一種流捏检, 流在管道中傳輸荞驴, 并且可以在管道的節(jié)點上進行處理熊楼, 比如篩選, 排序能犯,聚合等鲫骗。元素流在管道中經(jīng)過中間操作(intermediate operation)的處理,最后由最終操作(terminal operation)得到前面處理的結(jié)果枕磁。
圖上的流程轉(zhuǎn)換為 Java 代碼為:
List<Integer> transactionsIds =
widgets.stream() // Source
.filter(b -> b.getColor() == RED) // Operations-filter
.sorted((x,y) -> x.getWeight() - y.getWeight()) // Operations-sorted
.mapToInt(Widget::getWeight) // Operations-map
.sum(); // Results-sum
什么是 Stream?
Stream(流)是一個來自數(shù)據(jù)源的元素隊列并支持聚合操作
- 無存儲:不是一種數(shù)據(jù)結(jié)構(gòu)术吝,它只是某種數(shù)據(jù)源的一個視圖计济,數(shù)據(jù)源可以是一個數(shù)組排苍,Java容器或I/O channel等。
- 為函數(shù)式編程而生:對 stream 的修改都不會修改其數(shù)據(jù)源传藏,比如對 stream 執(zhí)行過濾操作并不會刪除被過濾的元素幔翰,而是會產(chǎn)生一個不包含被過濾元素的新 stream。
(peek 對可變對象可以修改)
- 惰式執(zhí)行:stream 上的操作并不會立即執(zhí)行叫惊,只有等到用戶真正需要結(jié)果的時候才會執(zhí)行做修。
- 可消費性:stream 只能被“消費”一次,一旦遍歷過就會失效蔗坯,就像容器的迭代器那樣燎含,想要再次遍歷必須重新生成。
Stream操作還有兩個基礎的特征:
- Pipelining: 中間操作都會返回流對象本身绘梦。 這樣多個操作可以串聯(lián)成一個管道卸奉, 如同流式風格(fluent style)颖御。 這樣做可以對操作進行優(yōu)化, 比如延遲執(zhí)行(laziness)和短路( short-circuiting)疹鳄。
- 內(nèi)部迭代: 以前對集合遍歷都是通過Iterator或者For-Each的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代辑鲤。 Stream提供了內(nèi)部迭代的方式杠茬, 通過訪問者模式(Visitor)實現(xiàn)。
關(guān)于 Stream 的相關(guān)API實現(xiàn)原理, 感興趣的可以去看下: 淺析 Java8 Stream 原理
創(chuàng)建流
stream 常見的創(chuàng)建的方式主要有Arrays宁赤、Collection栓票、Stream 靜態(tài)方法等,這里代碼列舉其中最常見的幾種:
package com.company.designModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
public class Java8StreamTest {
public static void main(String[] args) {
// 1.數(shù)組創(chuàng)建流
Integer[] arrays = {9, 5, 2, 7};
Stream<Integer> arraysStream = Arrays.stream(arrays);
// 2.Collection 創(chuàng)建流
ArrayList<Object> list = new ArrayList<>();
list.add("Amy");
list.add("Angst");
Stream<Object> stream = list.stream();
Stream<Object> parallelStream = list.parallelStream(); // 這里 parallelStream 創(chuàng)建的是一個并行流
// 3. Stream.of
Stream.of(1, 24, 8, 6, 10, 4, 8, 3, 2, 8, 6).skip(2).limit(5).forEach(System.out::println);
// empty() 可以創(chuàng)建一個空的流
Stream<Object> emptyStream = Stream.empty();
// 4.Stream.generate()
Stream.generate(Math::random).limit(10).skip(1).forEach(System.out::print);
// 5.Stream.iterate()
Stream.iterate(0, item -> ++ item).limit(10).forEach(System.out::print);
// 6.Stream.builder()
Stream<Object> buildStream = Stream.builder().add("Amy").add("Angst").build();
// 7. Stream.concat() 合并創(chuàng)建一個新的流
Stream<Object> concatStream = Stream.concat(stream, parallelStream);
}
}
這里有幾個需要關(guān)注的點:
- 查看 Stream 源碼的話,你會發(fā)現(xiàn) of() 方法內(nèi)部其實調(diào)用了 Arrays.stream() 方法继找。源碼注釋有提到
“從數(shù)組創(chuàng)建的流是安全的”
/**
* Returns a sequential ordered stream whose elements are the specified values.
*
* @param <T> the type of stream elements
* @param values the elements of the new stream
* @return the new stream
*/
@SafeVarargs
@SuppressWarnings("varargs") // Creating a stream from an array is safe
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
- Stream.generate(): 生成一個無限長度的Stream逃沿,其元素的生成是通過給定的Supplier(這個接口可以看成一個對象的工廠,每次調(diào)用返回一個給定類型的對象), 其中值是隨機的边臼。這個無限長度 Stream 是懶加載假消,一般都會配合Stream的limit()方法來做分頁使用富拗。
- Stream.iterate(): 也是生成無限長度的Stream,和 generator 不同的是,其元素的生成是重復對給定的種子值(seed)調(diào)用用戶指定函數(shù)來生成的. 其中包含的元素可以認為是:seed葵擎,f(seed), f(f(seed))無限循環(huán)。
操作流
流的操作類型分為兩種:
-
中間操作(Intermediate):一個流可以后面跟隨零個或多個intermediate操作签餐。其目的主要是打開流氯檐,做出某種程度的數(shù)據(jù)映射/過濾,然后返回一個新的流糯崎,交給下一個操作使用(鏈式操作)河泳。這類操作都是惰性化的(lazy),就是說薄霜,僅僅調(diào)用到這類方法纸兔,并沒有真正開始流的遍歷。
- 有狀態(tài)(Stateless):指該操作只有拿到所有元素之后才能繼續(xù)下去
- 無狀態(tài)(Stateful):指元素的處理不受之前元素的影響
-
終止操作(Terminal):一個流只能有一個 terminal 操作崎坊,當這個操作執(zhí)行后负甸,流就被使用“光”了呻待,無法再被操作。所以,這必定是流的最后一個操作奏篙。Terminal 操作的執(zhí)行迫淹,才會真正開始流的遍歷,并且會生成一個結(jié)果肺稀,或者一個 side effect应民。
- 非短路操作: 指必須處理所有元素才能得到最終結(jié)果
- 短路操作: 指遇到某些符合條件的元素就可以得到最終結(jié)果,如 A || B繁仁,只要A為true黄虱,則無需判斷B的結(jié)果
代碼演示
Intermediate方法
- filter(): 用于篩選數(shù)據(jù),返回由匹配的元素組成的流晤揣。
- limit(): 切片限流可以當做分頁來使用默勾,返回被截斷的流母剥。
- skip():要跳過前面元素n的數(shù)量,返回由該流的剩余元素組成的流习霹。
- distinct():去重操作炫隶,返回由不同元素組成的流。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Vivian");
list.add("Amy");
list.add("Amy");
list.add("Angst");
list.add("Zx");
list.stream(). // 創(chuàng)建一個list流
distinct(). // 去重操作
skip(1). // 跳過第一個元素
filter(o -> o.startsWith("A")). // 過濾以A開頭的元素
limit(2). // 切片選擇保留幾個
forEach(System.out::println); // 輸出結(jié)果
}
結(jié)果輸出:
Amy
Angst
Process finished with exit code 0
- map():接收一個函數(shù)作為參數(shù)煞檩,該函數(shù)會被應用到每個元素上斟湃,并將其映射成一個新的元素檐薯。
- flatMap():接收一個函數(shù)作為參數(shù),將流中的每個值都換成另一個流墓猎,然后把所有流連接成一個流赚楚。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Vivian");
list.add("Amy");
list.add("Angst");
// 1.map
list.stream().map(String::toLowerCase).forEach(System.out::println);
list.stream().mapToInt(String::length).forEach(System.out::println);
list.stream().mapToDouble(String::length).forEach(System.out::println);
list.stream().mapToLong(String::length).forEach(System.out::println);
// 2. flatMap 同 map 作用類似宠页,區(qū)別就是將每個元素重新組成Stream膨俐,并將這些Stream 串行合并成一條Stream.
list.stream().flatMap(o-> Stream.of(o.toLowerCase())).forEach(System.out::println);
list.stream().flatMapToInt(o-> IntStream.of(o.length())).forEach(System.out::println);
list.stream().flatMapToDouble(o-> DoubleStream.of(o.length())).forEach(System.out::println);
list.stream().flatMapToLong(o-> LongStream.of(o.length())).forEach(System.out::println);
}
結(jié)果輸出:
vivian
amy
angst
6
3
5
6.0
3.0
5.0
6
3
5
vivian
amy
angst
6
3
5
6.0
3.0
5.0
6
3
5
Process finished with exit code 0
- sorted():自然排序,流中元素需實現(xiàn)Comparable接口, 也可以自定義 Comparator
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Vivian");
list.add("Amy");
list.add("Angst");
list.stream().sorted().forEach(System.out::println); // 默認排序
list.stream().sorted((x, y) -> y.length() - x.length()).forEach(System.out::println); // 自定義排序
}
結(jié)果輸出:
Amy
Angst
Vivian
Vivian
Angst
Amy
Process finished with exit code 0
- peek():如同于map()门烂,能得到流中的每一個元素兄淫。但map接收的是一個 Function 表達式,有返回值慨丐。而peek接收的是Consumer泄私,沒有返回值晌端。
public class Java8StreamTestDemo {
public static void main(String[] args) {
// 1.不可變對象
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Vivian");
arrayList.add("Amy");
arrayList.add("Angst");
// 修改元素首字母小寫,實際結(jié)果并沒有修改成功
arrayList.stream().peek(String::toLowerCase).forEach(o -> System.out.println("toLowerCase:" + o));
// 2.可變對象
ArrayList<Food> list = new ArrayList<>();
list.add(new Food("果汁"));
list.add(new Food("奶茶"));
list.add(new Food("牛奶"));
// peek()方法存在的主要目的是用調(diào)試蓬痒,通過 peek() 方法可以看到流中的數(shù)據(jù)經(jīng)過每個處理點時的狀態(tài)漆羔。
list.stream().peek(o -> System.out.println("debugger: "+ o.getCoca())).count();
// 除去用于調(diào)試演痒,peek()在需要修改元素內(nèi)部狀態(tài)的場景也非常有用,比如修改 Coca 的值 瓶埋,當然也可以使用map()和flatMap實現(xiàn).
list.stream().peek(o->o.setCoca("飯前:"+ o.getCoca())).forEach(System.out::println);
}
// 定義一個可變對象
static class Food {
private String coca="飲料";
public Food(String coca) { this.coca = coca; }
public String getCoca() { return coca; }
public void setCoca(String coca) { this.coca = coca; }
@Override
public String toString() {
return "Food{" +
"coca='" + coca + '\'' +
'}';
}
}
}
結(jié)果輸出:
toLowerCase:Vivian
toLowerCase:Amy
toLowerCase:Angst
debugger: 果汁
debugger: 奶茶
debugger: 牛奶
Food{coca='飯前:果汁'}
Food{coca='飯前:奶茶'}
Food{coca='飯前:牛奶'}
Process finished with exit code 0
注意
:map函數(shù)對Stream中元素執(zhí)行的是映射操作养筒,會以新的元素(map的結(jié)果)填充新的Stream端姚。嚴格的講map不是修改原來的元素。peek只能消費Stream中的元素巫湘,是否可以更該Stream中的元素,取決于Stream中的元素是否是不可變對象诀诊。如果是不可變對象阅嘶,則不可修改Stream中的元素;如果是可變對象抡蛙,則可以修改對象的值魂迄,但是無法修改對象的引用捣炬。
Terminal方法
- max(): 返回此流的最大元素
- min(): 返回此流的最小元素
- count(): 返回此流中的元素計數(shù)
- findFirst(): 返回此流中第一個元素
- findAny(): 取任意一個元素,正常情況下一般會取第一個元素浴捆,在并行流的情況下會隨機取一個元素
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Vivian");
arrayList.add("Amy");
arrayList.add("Angst");
System.out.println(arrayList.stream().map(String::length).max(Integer::compareTo));
System.out.println(arrayList.stream().map(String::length).min(Integer::compareTo));
System.out.println(arrayList.stream().map(String::length).count());
System.out.println(arrayList.stream().map(String::length).findFirst());
// 在 parallel 流中存在每次輸出的結(jié)果不一致
System.out.println(arrayList.stream().parallel().map(String::length).findAny());
}
結(jié)果輸出:
Optional[6]
Optional[3]
3
Optional[6]
Optional[3]
Process finished with exit code 0
- andMatch(): 返回流中的元素是否與所提供的匹配
- allMatch(): 返回流的所有元素是否與指定的條件匹配选泻。
- noneMatch(): 返回流中是否沒有匹配謂詞的元素
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Vivian");
arrayList.add("Amy");
arrayList.add("Angst");
// 匹配成功返回 true, 否則 false
System.out.println(arrayList.stream().noneMatch(o->o.equals("amy"))); // 空匹配
System.out.println(arrayList.stream().anyMatch(o->o.equals("Amy"))); // 任意匹配
System.out.println(arrayList.stream().allMatch(o->o.equals("Amy"))); // 全匹配
}
結(jié)果輸出:
true
true
false
Process finished with exit code 0
- reduce(): 用于對 stream 中元素進行聚合求值
// 規(guī)約操作
System.out.println(Stream.of(1, 9, 5, 2, 7, -1).reduce(0, Integer::sum));
23
Process finished with exit code 0
- forEach(): 遍歷元素
- iterator(): 返回一個迭代器
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Vivian");
arrayList.add("Amy");
arrayList.add("Angst");
// 遍歷元素
arrayList.stream().forEach(System.out::println);
// 返回的是一個可迭代對象
System.out.println(arrayList.stream().iterator());
}
結(jié)果輸出:
Vivian
Amy
Angst
java.util.Spliterators$1Adapter@7ba4f24f
Process finished with exit code 0
- toArray(): 返回一個數(shù)組
- collect(): 收集操作页眯,將一個對象的集合轉(zhuǎn)化成另一個對象的集合
// 把流轉(zhuǎn)換成數(shù)組
Object[] toArray = Stream.of(1, 9, 5, 2, 7, -1).toArray();
// 使用系統(tǒng)提供的收集器可以將最終的數(shù)據(jù)流收集到 List Set Map等容器中
List<Integer> list = Stream.of(1, 9, 5, 2, 7, -1).collect(Collectors.toList());
最后
以上就是本次的學習分享窝撵,Stream 的熟練使用可以讓你的代碼更加簡潔易于維護襟铭,以上案例中可能還涉及其他的知識點,比如函數(shù)式接口赐劣、lambda等哩都,感興趣的可以看下筆者其他的學習筆記。歡迎留言討論咐汞。